recaptcha 4.11.0 → 5.12.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Recaptcha
4
+ module Adapters
5
+ module ControllerMethods
6
+ private
7
+
8
+ # Your private API can be specified in the +options+ hash or preferably
9
+ # using the Configuration.
10
+ def verify_recaptcha(options = {})
11
+ options = {model: options} unless options.is_a? Hash
12
+ return true if Recaptcha.skip_env?(options[:env])
13
+
14
+ model = options[:model]
15
+ attribute = options.fetch(:attribute, :base)
16
+ recaptcha_response = options[:response] || recaptcha_response_token(options[:action])
17
+
18
+ begin
19
+ verified = if Recaptcha.invalid_response?(recaptcha_response)
20
+ false
21
+ else
22
+ unless options[:skip_remote_ip]
23
+ remoteip = (request.respond_to?(:remote_ip) && request.remote_ip) || (env && env['REMOTE_ADDR'])
24
+ options = options.merge(remote_ip: remoteip.to_s) if remoteip
25
+ end
26
+
27
+ success, @_recaptcha_reply =
28
+ Recaptcha.verify_via_api_call(recaptcha_response, options.merge(with_reply: true))
29
+ success
30
+ end
31
+
32
+ if verified
33
+ flash.delete(:recaptcha_error) if recaptcha_flash_supported? && !model
34
+ true
35
+ else
36
+ recaptcha_error(
37
+ model,
38
+ attribute,
39
+ options.fetch(:message) { Recaptcha::Helpers.to_error_message(:verification_failed) }
40
+ )
41
+ false
42
+ end
43
+ rescue Timeout::Error
44
+ if Recaptcha.configuration.handle_timeouts_gracefully
45
+ recaptcha_error(
46
+ model,
47
+ attribute,
48
+ options.fetch(:message) { Recaptcha::Helpers.to_error_message(:recaptcha_unreachable) }
49
+ )
50
+ false
51
+ else
52
+ raise RecaptchaError, 'Recaptcha unreachable.'
53
+ end
54
+ rescue StandardError => e
55
+ raise RecaptchaError, e.message, e.backtrace
56
+ end
57
+ end
58
+
59
+ def verify_recaptcha!(options = {})
60
+ verify_recaptcha(options) || raise(VerifyError)
61
+ end
62
+
63
+ def recaptcha_reply
64
+ @_recaptcha_reply if defined?(@_recaptcha_reply)
65
+ end
66
+
67
+ def recaptcha_error(model, attribute, message)
68
+ if model
69
+ model.errors.add(attribute, message)
70
+ elsif recaptcha_flash_supported?
71
+ flash[:recaptcha_error] = message
72
+ end
73
+ end
74
+
75
+ def recaptcha_flash_supported?
76
+ request.respond_to?(:format) && request.format == :html && respond_to?(:flash)
77
+ end
78
+
79
+ # Extracts response token from params. params['g-recaptcha-response-data'] for recaptcha_v3 or
80
+ # params['g-recaptcha-response'] for recaptcha_tags and invisible_recaptcha_tags and should
81
+ # either be a string or a hash with the action name(s) as keys. If it is a hash, then `action`
82
+ # is used as the key.
83
+ # @return [String] A response token if one was passed in the params; otherwise, `''`
84
+ def recaptcha_response_token(action = nil)
85
+ response_param = params['g-recaptcha-response-data'] || params['g-recaptcha-response']
86
+ response_param = response_param[action] if action && response_param.respond_to?(:key?)
87
+
88
+ if response_param.is_a?(String)
89
+ response_param
90
+ else
91
+ ''
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Recaptcha
4
+ module Adapters
5
+ module ViewMethods
6
+ # Renders a [reCAPTCHA v3](https://developers.google.com/recaptcha/docs/v3) script and (by
7
+ # default) a hidden input to submit the response token. You can also call the functions
8
+ # directly if you prefer. You can use
9
+ # `Recaptcha::Helpers.recaptcha_v3_execute_function_name(action)` to get the name of the
10
+ # function to call.
11
+ def recaptcha_v3(options = {})
12
+ ::Recaptcha::Helpers.recaptcha_v3(options)
13
+ end
14
+
15
+ # Renders a reCAPTCHA [v2 Checkbox](https://developers.google.com/recaptcha/docs/display) widget
16
+ def recaptcha_tags(options = {})
17
+ ::Recaptcha::Helpers.recaptcha_tags(options)
18
+ end
19
+
20
+ # Renders a reCAPTCHA v2 [Invisible reCAPTCHA](https://developers.google.com/recaptcha/docs/invisible)
21
+ def invisible_recaptcha_tags(options = {})
22
+ ::Recaptcha::Helpers.invisible_recaptcha_tags(options)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -30,17 +30,33 @@ module Recaptcha
30
30
  # end
31
31
  #
32
32
  class Configuration
33
- attr_accessor :skip_verify_env, :secret_key, :site_key, :proxy, :handle_timeouts_gracefully, :hostname
33
+ DEFAULTS = {
34
+ 'free_server_url' => 'https://www.recaptcha.net/recaptcha/api.js',
35
+ 'enterprise_server_url' => 'https://www.recaptcha.net/recaptcha/enterprise.js',
36
+ 'free_verify_url' => 'https://www.recaptcha.net/recaptcha/api/siteverify',
37
+ 'enterprise_verify_url' => 'https://recaptchaenterprise.googleapis.com/v1/projects'
38
+ }.freeze
39
+
40
+ attr_accessor :default_env, :skip_verify_env, :proxy, :secret_key, :site_key, :handle_timeouts_gracefully,
41
+ :hostname, :enterprise, :enterprise_api_key, :enterprise_project_id, :response_limit
34
42
  attr_writer :api_server_url, :verify_url
35
43
 
36
- def initialize #:nodoc:
44
+ def initialize # :nodoc:
45
+ @default_env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || (Rails.env if defined? Rails.env)
37
46
  @skip_verify_env = %w[test cucumber]
38
- @handle_timeouts_gracefully = HANDLE_TIMEOUTS_GRACEFULLY
47
+ @handle_timeouts_gracefully = true
39
48
 
40
49
  @secret_key = ENV['RECAPTCHA_SECRET_KEY']
41
50
  @site_key = ENV['RECAPTCHA_SITE_KEY']
51
+
52
+ @enterprise = ENV['RECAPTCHA_ENTERPRISE'] == 'true'
53
+ @enterprise_api_key = ENV['RECAPTCHA_ENTERPRISE_API_KEY']
54
+ @enterprise_project_id = ENV['RECAPTCHA_ENTERPRISE_PROJECT_ID']
55
+
42
56
  @verify_url = nil
43
57
  @api_server_url = nil
58
+
59
+ @response_limit = 4000
44
60
  end
45
61
 
46
62
  def secret_key!
@@ -51,12 +67,20 @@ module Recaptcha
51
67
  site_key || raise(RecaptchaError, "No site key specified.")
52
68
  end
53
69
 
70
+ def enterprise_api_key!
71
+ enterprise_api_key || raise(RecaptchaError, "No Enterprise API key specified.")
72
+ end
73
+
74
+ def enterprise_project_id!
75
+ enterprise_project_id || raise(RecaptchaError, "No Enterprise project ID specified.")
76
+ end
77
+
54
78
  def api_server_url
55
- @api_server_url || CONFIG.fetch('server_url')
79
+ @api_server_url || (enterprise ? DEFAULTS.fetch('enterprise_server_url') : DEFAULTS.fetch('free_server_url'))
56
80
  end
57
81
 
58
82
  def verify_url
59
- @verify_url || CONFIG.fetch('verify_url')
83
+ @verify_url || (enterprise ? DEFAULTS.fetch('enterprise_verify_url') : DEFAULTS.fetch('free_verify_url'))
60
84
  end
61
85
  end
62
86
  end
@@ -0,0 +1,332 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Recaptcha
4
+ module Helpers
5
+ DEFAULT_ERRORS = {
6
+ recaptcha_unreachable: 'Oops, we failed to validate your reCAPTCHA response. Please try again.',
7
+ verification_failed: 'reCAPTCHA verification failed, please try again.'
8
+ }.freeze
9
+
10
+ def self.recaptcha_v3(options = {})
11
+ site_key = options[:site_key] ||= Recaptcha.configuration.site_key!
12
+ action = options.delete(:action) || raise(Recaptcha::RecaptchaError, 'action is required')
13
+ id = options.delete(:id) || "g-recaptcha-response-data-#{dasherize_action(action)}"
14
+ name = options.delete(:name) || "g-recaptcha-response-data[#{action}]"
15
+ turbolinks = options.delete(:turbolinks)
16
+ options[:render] = site_key
17
+ options[:script_async] ||= false
18
+ options[:script_defer] ||= false
19
+ element = options.delete(:element)
20
+ element = element == false ? false : :input
21
+ if element == :input
22
+ callback = options.delete(:callback) || recaptcha_v3_default_callback_name(action)
23
+ end
24
+ options[:class] = "g-recaptcha-response #{options[:class]}"
25
+
26
+ if turbolinks
27
+ options[:onload] = recaptcha_v3_execute_function_name(action)
28
+ end
29
+ html, tag_attributes = components(options)
30
+ if turbolinks
31
+ html << recaptcha_v3_onload_script(site_key, action, callback, id, options)
32
+ elsif recaptcha_v3_inline_script?(options)
33
+ html << recaptcha_v3_inline_script(site_key, action, callback, id, options)
34
+ end
35
+ case element
36
+ when :input
37
+ html << %(<input type="hidden" name="#{name}" id="#{id}" #{tag_attributes}/>\n)
38
+ when false
39
+ # No tag
40
+ nil
41
+ else
42
+ raise(RecaptchaError, "ReCAPTCHA element `#{options[:element]}` is not valid.")
43
+ end
44
+ html.respond_to?(:html_safe) ? html.html_safe : html
45
+ end
46
+
47
+ def self.recaptcha_tags(options)
48
+ if options.key?(:stoken)
49
+ raise(RecaptchaError, "Secure Token is deprecated. Please remove 'stoken' from your calls to recaptcha_tags.")
50
+ end
51
+ if options.key?(:ssl)
52
+ raise(RecaptchaError, "SSL is now always true. Please remove 'ssl' from your calls to recaptcha_tags.")
53
+ end
54
+
55
+ noscript = options.delete(:noscript)
56
+
57
+ html, tag_attributes, fallback_uri = components(options.dup)
58
+ html << %(<div #{tag_attributes}></div>\n)
59
+
60
+ if noscript != false
61
+ html << <<-HTML
62
+ <noscript>
63
+ <div>
64
+ <div style="width: 302px; height: 422px; position: relative;">
65
+ <div style="width: 302px; height: 422px; position: absolute;">
66
+ <iframe
67
+ src="#{fallback_uri}"
68
+ name="ReCAPTCHA"
69
+ style="width: 302px; height: 422px; border-style: none; border: 0; overflow: hidden;">
70
+ </iframe>
71
+ </div>
72
+ </div>
73
+ <div style="width: 300px; height: 60px; border-style: none;
74
+ bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px;
75
+ background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;">
76
+ <textarea id="g-recaptcha-response" name="g-recaptcha-response"
77
+ class="g-recaptcha-response"
78
+ style="width: 250px; height: 40px; border: 1px solid #c1c1c1;
79
+ margin: 10px 25px; padding: 0px; resize: none;">
80
+ </textarea>
81
+ </div>
82
+ </div>
83
+ </noscript>
84
+ HTML
85
+ end
86
+
87
+ html.respond_to?(:html_safe) ? html.html_safe : html
88
+ end
89
+
90
+ def self.invisible_recaptcha_tags(custom)
91
+ options = {callback: 'invisibleRecaptchaSubmit', ui: :button}.merge(custom)
92
+ text = options.delete(:text)
93
+ html, tag_attributes = components(options.dup)
94
+ html << default_callback(options) if default_callback_required?(options)
95
+
96
+ case options[:ui]
97
+ when :button
98
+ html << %(<button type="submit" #{tag_attributes}>#{text}</button>\n)
99
+ when :invisible
100
+ html << %(<div data-size="invisible" #{tag_attributes}></div>\n)
101
+ when :input
102
+ html << %(<input type="submit" #{tag_attributes} value="#{text}"/>\n)
103
+ else
104
+ raise(RecaptchaError, "ReCAPTCHA ui `#{options[:ui]}` is not valid.")
105
+ end
106
+ html.respond_to?(:html_safe) ? html.html_safe : html
107
+ end
108
+
109
+ def self.to_error_message(key)
110
+ default = DEFAULT_ERRORS.fetch(key) { raise ArgumentError "Unknown reCAPTCHA error - #{key}" }
111
+ to_message("recaptcha.errors.#{key}", default)
112
+ end
113
+
114
+ if defined?(I18n)
115
+ def self.to_message(key, default)
116
+ I18n.translate(key, default: default)
117
+ end
118
+ else
119
+ def self.to_message(_key, default)
120
+ default
121
+ end
122
+ end
123
+
124
+ private_class_method def self.components(options)
125
+ html = +''
126
+ attributes = {}
127
+ fallback_uri = +''
128
+
129
+ options = options.dup
130
+ env = options.delete(:env)
131
+ class_attribute = options.delete(:class)
132
+ site_key = options.delete(:site_key)
133
+ hl = options.delete(:hl)
134
+ onload = options.delete(:onload)
135
+ render = options.delete(:render)
136
+ script_async = options.delete(:script_async)
137
+ script_defer = options.delete(:script_defer)
138
+ nonce = options.delete(:nonce)
139
+ skip_script = (options.delete(:script) == false) || (options.delete(:external_script) == false)
140
+ ui = options.delete(:ui)
141
+
142
+ data_attribute_keys = [:badge, :theme, :type, :callback, :expired_callback, :error_callback, :size]
143
+ data_attribute_keys << :tabindex unless ui == :button
144
+ data_attributes = {}
145
+ data_attribute_keys.each do |data_attribute|
146
+ value = options.delete(data_attribute)
147
+ data_attributes["data-#{data_attribute.to_s.tr('_', '-')}"] = value if value
148
+ end
149
+
150
+ unless Recaptcha.skip_env?(env)
151
+ site_key ||= Recaptcha.configuration.site_key!
152
+ script_url = Recaptcha.configuration.api_server_url
153
+ query_params = hash_to_query(
154
+ hl: hl,
155
+ onload: onload,
156
+ render: render
157
+ )
158
+ script_url += "?#{query_params}" unless query_params.empty?
159
+ async_attr = "async" if script_async != false
160
+ defer_attr = "defer" if script_defer != false
161
+ nonce_attr = " nonce='#{nonce}'" if nonce
162
+ html << %(<script src="#{script_url}" #{async_attr} #{defer_attr} #{nonce_attr}></script>\n) unless skip_script
163
+ fallback_uri = %(#{script_url.chomp(".js")}/fallback?k=#{site_key})
164
+ attributes["data-sitekey"] = site_key
165
+ attributes.merge! data_attributes
166
+ end
167
+
168
+ # The remaining options will be added as attributes on the tag.
169
+ attributes["class"] = "g-recaptcha #{class_attribute}"
170
+ tag_attributes = attributes.merge(options).map { |k, v| %(#{k}="#{v}") }.join(" ")
171
+
172
+ [html, tag_attributes, fallback_uri]
173
+ end
174
+
175
+ # v3
176
+
177
+ # Renders a script that calls `grecaptcha.execute` or
178
+ # `grecaptcha.enterprise.execute` for the given `site_key` and `action` and
179
+ # calls the `callback` with the resulting response token.
180
+ private_class_method def self.recaptcha_v3_inline_script(site_key, action, callback, id, options = {})
181
+ nonce = options[:nonce]
182
+ nonce_attr = " nonce='#{nonce}'" if nonce
183
+
184
+ <<-HTML
185
+ <script#{nonce_attr}>
186
+ // Define function so that we can call it again later if we need to reset it
187
+ // This executes reCAPTCHA and then calls our callback.
188
+ function #{recaptcha_v3_execute_function_name(action)}() {
189
+ #{recaptcha_ready_method_name}(function() {
190
+ #{recaptcha_execute_method_name}('#{site_key}', {action: '#{action}'}).then(function(token) {
191
+ #{callback}('#{id}', token)
192
+ });
193
+ });
194
+ };
195
+ // Invoke immediately
196
+ #{recaptcha_v3_execute_function_name(action)}()
197
+
198
+ // Async variant so you can await this function from another async function (no need for
199
+ // an explicit callback function then!)
200
+ // Returns a Promise that resolves with the response token.
201
+ async function #{recaptcha_v3_async_execute_function_name(action)}() {
202
+ return new Promise((resolve, reject) => {
203
+ #{recaptcha_ready_method_name}(async function() {
204
+ resolve(await #{recaptcha_execute_method_name}('#{site_key}', {action: '#{action}'}))
205
+ });
206
+ })
207
+ };
208
+
209
+ #{recaptcha_v3_define_default_callback(callback) if recaptcha_v3_define_default_callback?(callback, action, options)}
210
+ </script>
211
+ HTML
212
+ end
213
+
214
+ private_class_method def self.recaptcha_v3_onload_script(site_key, action, callback, id, options = {})
215
+ nonce = options[:nonce]
216
+ nonce_attr = " nonce='#{nonce}'" if nonce
217
+
218
+ <<-HTML
219
+ <script#{nonce_attr}>
220
+ function #{recaptcha_v3_execute_function_name(action)}() {
221
+ #{recaptcha_ready_method_name}(function() {
222
+ #{recaptcha_execute_method_name}('#{site_key}', {action: '#{action}'}).then(function(token) {
223
+ #{callback}('#{id}', token)
224
+ });
225
+ });
226
+ };
227
+ #{recaptcha_v3_define_default_callback(callback) if recaptcha_v3_define_default_callback?(callback, action, options)}
228
+ </script>
229
+ HTML
230
+ end
231
+
232
+ private_class_method def self.recaptcha_v3_inline_script?(options)
233
+ !Recaptcha.skip_env?(options[:env]) &&
234
+ options[:script] != false &&
235
+ options[:inline_script] != false
236
+ end
237
+
238
+ private_class_method def self.recaptcha_v3_define_default_callback(callback)
239
+ <<-HTML
240
+ var #{callback} = function(id, token) {
241
+ var element = document.getElementById(id);
242
+ element.value = token;
243
+ }
244
+ HTML
245
+ end
246
+
247
+ # Returns true if we should be adding the default callback.
248
+ # That is, if the given callback name is the default callback name (for the given action) and we
249
+ # are not skipping inline scripts for any reason.
250
+ private_class_method def self.recaptcha_v3_define_default_callback?(callback, action, options)
251
+ callback == recaptcha_v3_default_callback_name(action) &&
252
+ recaptcha_v3_inline_script?(options)
253
+ end
254
+
255
+ # Returns the name of the JavaScript function that actually executes the
256
+ # reCAPTCHA code (calls `grecaptcha.execute` or
257
+ # `grecaptcha.enterprise.execute`). You can call it again later to reset it.
258
+ def self.recaptcha_v3_execute_function_name(action)
259
+ "executeRecaptchaFor#{sanitize_action_for_js(action)}"
260
+ end
261
+
262
+ # Returns the name of an async JavaScript function that executes the reCAPTCHA code.
263
+ def self.recaptcha_v3_async_execute_function_name(action)
264
+ "#{recaptcha_v3_execute_function_name(action)}Async"
265
+ end
266
+
267
+ def self.recaptcha_v3_default_callback_name(action)
268
+ "setInputWithRecaptchaResponseTokenFor#{sanitize_action_for_js(action)}"
269
+ end
270
+
271
+ # v2
272
+
273
+ private_class_method def self.default_callback(options = {})
274
+ nonce = options[:nonce]
275
+ nonce_attr = " nonce='#{nonce}'" if nonce
276
+ selector_attr = options[:id] ? "##{options[:id]}" : ".g-recaptcha"
277
+
278
+ <<-HTML
279
+ <script#{nonce_attr}>
280
+ var invisibleRecaptchaSubmit = function () {
281
+ var closestForm = function (ele) {
282
+ var curEle = ele.parentNode;
283
+ while (curEle.nodeName !== 'FORM' && curEle.nodeName !== 'BODY'){
284
+ curEle = curEle.parentNode;
285
+ }
286
+ return curEle.nodeName === 'FORM' ? curEle : null
287
+ };
288
+
289
+ var el = document.querySelector("#{selector_attr}")
290
+ if (!!el) {
291
+ var form = closestForm(el);
292
+ if (form) {
293
+ form.submit();
294
+ }
295
+ }
296
+ };
297
+ </script>
298
+ HTML
299
+ end
300
+
301
+ def self.recaptcha_execute_method_name
302
+ Recaptcha.configuration.enterprise ? "grecaptcha.enterprise.execute" : "grecaptcha.execute"
303
+ end
304
+
305
+ def self.recaptcha_ready_method_name
306
+ Recaptcha.configuration.enterprise ? "grecaptcha.enterprise.ready" : "grecaptcha.ready"
307
+ end
308
+
309
+ private_class_method def self.default_callback_required?(options)
310
+ options[:callback] == 'invisibleRecaptchaSubmit' &&
311
+ !Recaptcha.skip_env?(options[:env]) &&
312
+ options[:script] != false &&
313
+ options[:inline_script] != false
314
+ end
315
+
316
+ # Returns a camelized string that is safe for use in a JavaScript variable/function name.
317
+ # sanitize_action_for_js('my/action') => 'MyAction'
318
+ private_class_method def self.sanitize_action_for_js(action)
319
+ action.to_s.gsub(/\W/, '_').split(/\/|_/).map(&:capitalize).join
320
+ end
321
+
322
+ # Returns a dasherized string that is safe for use as an HTML ID
323
+ # dasherize_action('my/action') => 'my-action'
324
+ private_class_method def self.dasherize_action(action)
325
+ action.to_s.gsub(/\W/, '-').tr('_', '-')
326
+ end
327
+
328
+ private_class_method def self.hash_to_query(hash)
329
+ hash.delete_if { |_, val| val.nil? || val.empty? }.to_a.map { |pair| pair.join('=') }.join('&')
330
+ end
331
+ end
332
+ end
@@ -1,3 +1,4 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  # deprecated, but let's not blow everyone up
3
4
  require 'recaptcha'
@@ -3,13 +3,33 @@
3
3
  module Recaptcha
4
4
  class Railtie < Rails::Railtie
5
5
  ActiveSupport.on_load(:action_view) do
6
- require 'recaptcha/client_helper'
7
- include Recaptcha::ClientHelper
6
+ include Recaptcha::Adapters::ViewMethods
8
7
  end
9
8
 
10
9
  ActiveSupport.on_load(:action_controller) do
11
- require 'recaptcha/verify'
12
- include Recaptcha::Verify
10
+ include Recaptcha::Adapters::ControllerMethods
11
+ end
12
+
13
+ initializer 'recaptcha' do |app|
14
+ Recaptcha::Railtie.instance_eval do
15
+ pattern = pattern_from app.config.i18n.available_locales
16
+
17
+ add("rails/locales/#{pattern}.yml")
18
+ end
19
+ end
20
+
21
+ class << self
22
+ protected
23
+
24
+ def add(pattern)
25
+ files = Dir[File.join(File.dirname(__FILE__), '../..', pattern)]
26
+ I18n.load_path.concat(files)
27
+ end
28
+
29
+ def pattern_from(args)
30
+ array = Array(args || [])
31
+ array.blank? ? '*' : "{#{array.join ','}}"
32
+ end
13
33
  end
14
34
  end
15
35
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Recaptcha
4
- VERSION = '4.11.0'
4
+ VERSION = '5.12.0'
5
5
  end