class Incline::Recaptcha

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

Constants

DISABLED

A string that will validated when reCAPTCHA is disabled.

VALID_SIZES

Gets the valid sizes for the reCAPTCHA field.

VALID_THEMES

Gets the valid themes for the reCAPTCHA field.

VALID_TYPES

Gets the valid types for the reCAPTCHA field.

Public Class Methods

add() click to toggle source

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

# 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?() click to toggle source

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

# 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_callbacks() click to toggle source

Contains a collection of onload callbacks for explicit reCAPTCHA fields.

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

# 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) click to toggle source

Pauses reCAPTCHA validation for the specified block of code.

# 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?() click to toggle source

Determines if reCAPTCHA validation is currently paused.

# File lib/incline/recaptcha.rb, line 312
def self.paused?
  temp_lock
end
private_key() click to toggle source

Gets the private key.

# File lib/incline/recaptcha.rb, line 143
def self.private_key
  @private_key ||= Rails.application.secrets[:recaptcha_private].to_s.strip
end
proxy() click to toggle source

Gets the proxy configuration (if any).

# File lib/incline/recaptcha.rb, line 149
def self.proxy
  @proxy ||= (Rails.application.secrets[:recaptcha_proxy] || {}).symbolize_keys
end
public_key() click to toggle source

Gets the public key.

# File lib/incline/recaptcha.rb, line 137
def self.public_key
  @public_key ||= Rails.application.secrets[:recaptcha_public].to_s.strip
end
script_block() click to toggle source

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

# 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 = {}) click to toggle source

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.

# 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

Private Class Methods

enable_for_testing(pub_key = nil, priv_key = nil) { || ... } click to toggle source
# File lib/incline/recaptcha.rb, line 318
def self.enable_for_testing(pub_key = nil, priv_key = nil)
  raise 'This method is only valid when testing.' unless Rails.env.test?
  
  @enabled_for_testing = true
  @public_key = pub_key
  @private_key = priv_key
  begin
    yield if block_given?
  ensure
    @private_key = nil
    @public_key = nil
    @enabled_for_testing = false
  end
end
enabled_for_testing?() click to toggle source
# File lib/incline/recaptcha.rb, line 333
def self.enabled_for_testing?
  @enabled_for_testing ||= false
end
temp_lock() click to toggle source
# File lib/incline/recaptcha.rb, line 337
def self.temp_lock
  @temp_lock ||= false
end
temp_lock=(bool) click to toggle source
# File lib/incline/recaptcha.rb, line 341
def self.temp_lock=(bool)
  @temp_lock = bool
end