Class: Incline::Recaptcha

Inherits:
Object
  • Object
show all
Defined in:
lib/incline/recaptcha.rb

Overview

A helper class for reCAPTCHA.

To use reCAPTCHA, you will need to define recaptcha_public and recaptcha_private in your ‘config/secrets.yml’. If you need to use a proxy server, you will need to configure the proxy settings as well.

# config/secrets.yml
default: &default
  recaptcha_public: SomeBase64StringFromGoogle
  recaptcha_private: AnotherBase64StringFromGoogle
  recaptcha_proxy:
    host: 10.10.10.10
    port: 1000
    user: username
    password: top_secret

Defined Under Namespace

Classes: Tag

Constant Summary collapse

VALID_THEMES =

Gets the valid themes for the reCAPTCHA field.

[ :dark, :light ]
VALID_TYPES =

Gets the valid types for the reCAPTCHA field.

[ :audio, :image ]
VALID_SIZES =

Gets the valid sizes for the reCAPTCHA field.

[ :compact, :normal ]
DISABLED =

A string that will validated when reCAPTCHA is disabled.

'0.0.0.0|disabled'

Class Method Summary collapse

Class Method Details

.addObject

Generates the bare minimum code needed to include a reCAPTCHA challenge in a form.



156
157
158
159
160
# File 'lib/incline/recaptcha.rb', line 156

def self.add
  unless disabled?
    "<div class=\"g-recaptcha\" data-sitekey=\"#{CGI::escape_html(public_key)}\"></div>\n<script src=\"https://www.google.com/recaptcha/api.js\"></script><br>".html_safe
  end
end

.disabled?Boolean

Determines if recaptcha is disabled either due to a test environment or because :recaptcha_public or :recaptcha_private is not defined in secrets.yml.

Returns:

  • (Boolean)


131
132
133
# File 'lib/incline/recaptcha.rb', line 131

def self.disabled?
  temp_lock || public_key.blank? || private_key.blank? || (Rails.env.test? && !enabled_for_testing?)
end

.onload_callbacksObject

Contains a collection of onload callbacks for explicit reCAPTCHA fields.

Used by the Incline::Recaptcha::Tag helper.



275
276
277
278
# File 'lib/incline/recaptcha.rb', line 275

def self.onload_callbacks
  # FIXME: Should probably move this to the session.
  @onload_callbacks ||= []
end

.pause_for(&block) ⇒ Object

Pauses reCAPTCHA validation for the specified block of code.



297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/incline/recaptcha.rb', line 297

def self.pause_for(&block)
  # already paused, so just call the block.
  return block.call if paused?

  # otherwise pause and then call the block.
  self.temp_lock = true
  begin
    return block.call
  ensure
    self.temp_lock = false
  end
end

.paused?Boolean

Determines if reCAPTCHA validation is currently paused.

Returns:

  • (Boolean)


312
313
314
# File 'lib/incline/recaptcha.rb', line 312

def self.paused?
  temp_lock
end

.private_keyObject

Gets the private key.



143
144
145
# File 'lib/incline/recaptcha.rb', line 143

def self.private_key
  @private_key ||= Rails.application.secrets[:recaptcha_private].to_s.strip
end

.proxyObject

Gets the proxy configuration (if any).



149
150
151
# File 'lib/incline/recaptcha.rb', line 149

def self.proxy
  @proxy ||= (Rails.application.secrets[:recaptcha_proxy] || {}).symbolize_keys
end

.public_keyObject

Gets the public key.



137
138
139
# File 'lib/incline/recaptcha.rb', line 137

def self.public_key
  @public_key ||= Rails.application.secrets[:recaptcha_public].to_s.strip
end

.script_blockObject

Generates a script block to load reCAPTCHA and activate any reCAPTCHA fields.



282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/incline/recaptcha.rb', line 282

def self.script_block
  if onload_callbacks.any?
    ret = "<script type=\"text/javascript\">\n// <![CDATA[\nfunction recaptcha_onload() { "
    onload_callbacks.each { |onload| ret += CGI::escape_html(onload) + '(); ' }
    ret += "}\n// ]]>\n</script>\n<script type=\"text/javascript\" src=\"https://www.google.com/recaptcha/api.js?onload=recaptcha_onload&amp;render=explicit\" async defer></script>"

    # clear the cache.
    onload_callbacks.clear

    ret.html_safe
  end
end

.verify(options = {}) ⇒ Object

Verifies the response from a reCAPTCHA challenge.

Valid options:

model

Sets the model that this challenge is verifying.

attribute

If a model is provided, you can supply an attribute to retrieve the response data from. This attribute should return a hash with :response and :remote_ip keys. If this is provided, then the remaining options are ignored.

response

If specified, defines the response from the reCAPTCHA challenge that we want to verify. If not specified, then the request parameters (if any) are searched for the “g-recaptcha-response” value.

remote_ip

If specified, defines the remote IP of the user that was challenged. If not specified, then the remote IP from the request (if any) is used.

request

Specifies the request to use for information. This must be provided unless :response and :remote_ip are both specified. This is the default option if an object other than a Hash is provided to #verify.

Returns true on success, or false on failure.



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/incline/recaptcha.rb', line 185

def self.verify(options = {})
  return true if temp_lock
  
  options = { request: options } unless options.is_a?(::Hash)
  
  model = options[:model]

  response =
      if model && options[:attribute] && model.respond_to?(options[:attribute])
        model.send(options[:attribute])
      else
        nil
      end

  remote_ip = nil

  if response.is_a?(::Hash)
    remote_ip = response[:remote_ip]
    response = response[:response]
  end

  # model must respond to the 'errors' message and the result of that must respond to 'add'
  if !model || !model.respond_to?('errors') || !model.send('errors').respond_to?('add')
    model = nil
  end

  response ||= options[:response]
  remote_ip ||= options[:remote_ip]

  if response.blank? || remote_ip.blank?
    request = options[:request]
    raise ArgumentError, 'Either :request must be specified or both :response and :remote_ip must be specified.' unless request
    response = request.params['g-recaptcha-response']
    remote_ip = request.respond_to?(:remote_ip) ? request.send(:remote_ip) : ENV['REMOTE_ADDR']
  end

  if disabled?
    # In tests or environments where reCAPTCHA is disabled,
    # the response should be 'disabled' to verify successfully.
    return response == 'disabled'
  else
    begin
      if proxy.blank?
        http = Net::HTTP
      else
        http = Net::HTTP::Proxy(proxy.host, proxy.port, proxy.user, proxy.password)
      end

      verify_hash = {
          secret: private_key,
          remoteip: remote_ip,
          response: response
      }
      recaptcha = nil
      Timeout::timeout(5) do
        uri = URI.parse('https://www.google.com/recaptcha/api/siteverify')
        http_instance = http.new(uri.host, uri.port)
        if uri.port == 443
          http_instance.use_ssl = true
        end
        request = Net::HTTP::Post.new(uri.request_uri)
        request.set_form_data(verify_hash)
        recaptcha = http_instance.request(request)
      end
      answer = JSON.parse(recaptcha.body)

      unless answer['success'].to_s.downcase == 'true'
        if model
          model.errors.add(options[:attribute] || :base, 'Recaptcha verification failed.')
        end
        return false
      end

      return true
    rescue Timeout::Error
      if model
        model.errors.add(:base, 'Recaptcha unreachable.')
      end
    end
  end

  false
end