recaptcha 0.3.6 → 5.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/recaptcha.rb CHANGED
@@ -1,15 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'uri'
6
+
1
7
  require 'recaptcha/configuration'
2
- require 'recaptcha/client_helper'
3
- require 'recaptcha/verify'
8
+ require 'recaptcha/helpers'
9
+ require 'recaptcha/adapters/controller_methods'
10
+ require 'recaptcha/adapters/view_methods'
11
+ if defined?(Rails)
12
+ require 'recaptcha/railtie'
13
+ end
4
14
 
5
15
  module Recaptcha
6
- RECAPTCHA_API_SERVER_URL = '//www.google.com/recaptcha/api'
7
- RECAPTCHA_API_SECURE_SERVER_URL = 'https://www.google.com/recaptcha/api'
8
- RECAPTCHA_VERIFY_URL = 'http://www.google.com/recaptcha/api/verify'
9
- USE_SSL_BY_DEFAULT = false
16
+ DEFAULT_TIMEOUT = 3
17
+ RESPONSE_LIMIT = 4000
10
18
 
11
- HANDLE_TIMEOUTS_GRACEFULLY = true
12
- SKIP_VERIFY_ENV = ['test', 'cucumber']
19
+ class RecaptchaError < StandardError
20
+ end
21
+
22
+ class VerifyError < RecaptchaError
23
+ end
13
24
 
14
25
  # Gives access to the current Configuration.
15
26
  def self.configuration
@@ -35,16 +46,79 @@ module Recaptcha
35
46
  configuration.send("#{key}=", value)
36
47
  end
37
48
 
38
- result = yield if block_given?
39
-
49
+ yield if block_given?
50
+ ensure
40
51
  original_config.each { |key, value| configuration.send("#{key}=", value) }
41
- result
42
52
  end
43
53
 
44
- class RecaptchaError < StandardError
54
+ def self.skip_env?(env)
55
+ configuration.skip_verify_env.include?(env || configuration.default_env)
45
56
  end
46
- end
47
57
 
48
- if defined?(Rails)
49
- require 'recaptcha/rails'
58
+ def self.invalid_response?(resp)
59
+ resp.empty? || resp.length > RESPONSE_LIMIT
60
+ end
61
+
62
+ def self.verify_via_api_call(response, options)
63
+ secret_key = options.fetch(:secret_key) { configuration.secret_key! }
64
+ verify_hash = { 'secret' => secret_key, 'response' => response }
65
+ verify_hash['remoteip'] = options[:remote_ip] if options.key?(:remote_ip)
66
+
67
+ reply = api_verification(verify_hash, timeout: options[:timeout])
68
+ success = reply['success'].to_s == 'true' &&
69
+ hostname_valid?(reply['hostname'], options[:hostname]) &&
70
+ action_valid?(reply['action'], options[:action]) &&
71
+ score_above_threshold?(reply['score'], options[:minimum_score])
72
+
73
+ if options[:with_reply] == true
74
+ return success, reply
75
+ else
76
+ return success
77
+ end
78
+ end
79
+
80
+ def self.hostname_valid?(hostname, validation)
81
+ validation ||= configuration.hostname
82
+
83
+ case validation
84
+ when nil, FalseClass then true
85
+ when String then validation == hostname
86
+ else validation.call(hostname)
87
+ end
88
+ end
89
+
90
+ def self.action_valid?(action, expected_action)
91
+ case expected_action
92
+ when nil, FalseClass then true
93
+ else action == expected_action
94
+ end
95
+ end
96
+
97
+ # Returns true iff score is greater or equal to (>=) minimum_score, or if no minimum_score was specified
98
+ def self.score_above_threshold?(score, minimum_score)
99
+ return true if minimum_score.nil?
100
+ return false if score.nil?
101
+
102
+ case minimum_score
103
+ when nil, FalseClass then true
104
+ else score >= minimum_score
105
+ end
106
+ end
107
+
108
+ def self.api_verification(verify_hash, timeout: nil)
109
+ timeout ||= DEFAULT_TIMEOUT
110
+ http = if configuration.proxy
111
+ proxy_server = URI.parse(configuration.proxy)
112
+ Net::HTTP::Proxy(proxy_server.host, proxy_server.port, proxy_server.user, proxy_server.password)
113
+ else
114
+ Net::HTTP
115
+ end
116
+ query = URI.encode_www_form(verify_hash)
117
+ uri = URI.parse(configuration.verify_url + '?' + query)
118
+ http_instance = http.new(uri.host, uri.port)
119
+ http_instance.read_timeout = http_instance.open_timeout = timeout
120
+ http_instance.use_ssl = true if uri.port == 443
121
+ request = Net::HTTP::Get.new(uri.request_uri)
122
+ JSON.parse(http_instance.request(request).body)
123
+ end
50
124
  end
@@ -0,0 +1,94 @@
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
+ if response_param&.respond_to?(:to_h) # Includes ActionController::Parameters
87
+ response_param[action].to_s
88
+ else
89
+ response_param.to_s
90
+ end
91
+ end
92
+ end
93
+ end
94
+ 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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Recaptcha
2
4
  # This class enables detailed configuration of the recaptcha services.
3
5
  #
@@ -15,7 +17,7 @@ module Recaptcha
15
17
  # Your are able to customize all attributes listed below. All values have
16
18
  # sensitive default and will very likely not need to be changed.
17
19
  #
18
- # Please note that the public and private key for the reCAPTCHA API Access
20
+ # Please note that the site and secret key for the reCAPTCHA API Access
19
21
  # have no useful default value. The keys may be set via the Shell enviroment
20
22
  # or using this configuration. Settings within this configuration always take
21
23
  # precedence.
@@ -23,36 +25,44 @@ module Recaptcha
23
25
  # Setting the keys with this Configuration
24
26
  #
25
27
  # Recaptcha.configure do |config|
26
- # config.public_key = '6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy'
27
- # config.private_key = '6Lc6BAAAAAAAAKN3DRm6VA_xxxxxxxxxxxxxxxxx'
28
+ # config.site_key = '6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy'
29
+ # config.secret_key = '6Lc6BAAAAAAAAKN3DRm6VA_xxxxxxxxxxxxxxxxx'
28
30
  # end
29
31
  #
30
32
  class Configuration
31
- attr_accessor :nonssl_api_server_url,
32
- :ssl_api_server_url,
33
- :verify_url,
34
- :skip_verify_env,
35
- :private_key,
36
- :public_key,
37
- :proxy,
38
- :handle_timeouts_gracefully,
39
- :use_ssl_by_default
33
+ DEFAULTS = {
34
+ 'server_url' => 'https://www.recaptcha.net/recaptcha/api.js',
35
+ 'verify_url' => 'https://www.recaptcha.net/recaptcha/api/siteverify'
36
+ }.freeze
37
+
38
+ attr_accessor :default_env, :skip_verify_env, :secret_key, :site_key, :proxy, :handle_timeouts_gracefully, :hostname
39
+ attr_writer :api_server_url, :verify_url
40
40
 
41
41
  def initialize #:nodoc:
42
- @nonssl_api_server_url = RECAPTCHA_API_SERVER_URL
43
- @ssl_api_server_url = RECAPTCHA_API_SECURE_SERVER_URL
44
- @verify_url = RECAPTCHA_VERIFY_URL
45
- @skip_verify_env = SKIP_VERIFY_ENV
46
- @handle_timeouts_gracefully = HANDLE_TIMEOUTS_GRACEFULLY
47
- @use_ssl_by_default = USE_SSL_BY_DEFAULT
48
-
49
- @private_key = ENV['RECAPTCHA_PRIVATE_KEY']
50
- @public_key = ENV['RECAPTCHA_PUBLIC_KEY']
42
+ @default_env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || (Rails.env if defined? Rails.env)
43
+ @skip_verify_env = %w[test cucumber]
44
+ @handle_timeouts_gracefully = true
45
+
46
+ @secret_key = ENV['RECAPTCHA_SECRET_KEY']
47
+ @site_key = ENV['RECAPTCHA_SITE_KEY']
48
+ @verify_url = nil
49
+ @api_server_url = nil
50
+ end
51
+
52
+ def secret_key!
53
+ secret_key || raise(RecaptchaError, "No secret key specified.")
54
+ end
55
+
56
+ def site_key!
57
+ site_key || raise(RecaptchaError, "No site key specified.")
58
+ end
59
+
60
+ def api_server_url
61
+ @api_server_url || DEFAULTS.fetch('server_url')
51
62
  end
52
63
 
53
- def api_server_url(ssl = nil) #:nodoc:
54
- ssl = use_ssl_by_default if ssl.nil?
55
- ssl ? ssl_api_server_url : nonssl_api_server_url
64
+ def verify_url
65
+ @verify_url || DEFAULTS.fetch('verify_url')
56
66
  end
57
67
  end
58
68
  end
@@ -0,0 +1,322 @@
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` for the given `site_key` and `action` and
178
+ # calls the `callback` with the resulting response token.
179
+ private_class_method def self.recaptcha_v3_inline_script(site_key, action, callback, id, options = {})
180
+ nonce = options[:nonce]
181
+ nonce_attr = " nonce='#{nonce}'" if nonce
182
+
183
+ <<-HTML
184
+ <script#{nonce_attr}>
185
+ // Define function so that we can call it again later if we need to reset it
186
+ // This executes reCAPTCHA and then calls our callback.
187
+ function #{recaptcha_v3_execute_function_name(action)}() {
188
+ grecaptcha.ready(function() {
189
+ grecaptcha.execute('#{site_key}', {action: '#{action}'}).then(function(token) {
190
+ #{callback}('#{id}', token)
191
+ });
192
+ });
193
+ };
194
+ // Invoke immediately
195
+ #{recaptcha_v3_execute_function_name(action)}()
196
+
197
+ // Async variant so you can await this function from another async function (no need for
198
+ // an explicit callback function then!)
199
+ // Returns a Promise that resolves with the response token.
200
+ async function #{recaptcha_v3_async_execute_function_name(action)}() {
201
+ return new Promise((resolve, reject) => {
202
+ grecaptcha.ready(async function() {
203
+ resolve(await grecaptcha.execute('#{site_key}', {action: '#{action}'}))
204
+ });
205
+ })
206
+ };
207
+
208
+ #{recaptcha_v3_define_default_callback(callback) if recaptcha_v3_define_default_callback?(callback, action, options)}
209
+ </script>
210
+ HTML
211
+ end
212
+
213
+ private_class_method def self.recaptcha_v3_onload_script(site_key, action, callback, id, options = {})
214
+ nonce = options[:nonce]
215
+ nonce_attr = " nonce='#{nonce}'" if nonce
216
+
217
+ <<-HTML
218
+ <script#{nonce_attr}>
219
+ function #{recaptcha_v3_execute_function_name(action)}() {
220
+ grecaptcha.ready(function() {
221
+ grecaptcha.execute('#{site_key}', {action: '#{action}'}).then(function(token) {
222
+ #{callback}('#{id}', token)
223
+ });
224
+ });
225
+ };
226
+ #{recaptcha_v3_define_default_callback(callback) if recaptcha_v3_define_default_callback?(callback, action, options)}
227
+ </script>
228
+ HTML
229
+ end
230
+
231
+ private_class_method def self.recaptcha_v3_inline_script?(options)
232
+ !Recaptcha.skip_env?(options[:env]) &&
233
+ options[:script] != false &&
234
+ options[:inline_script] != false
235
+ end
236
+
237
+ private_class_method def self.recaptcha_v3_define_default_callback(callback)
238
+ <<-HTML
239
+ var #{callback} = function(id, token) {
240
+ var element = document.getElementById(id);
241
+ element.value = token;
242
+ }
243
+ HTML
244
+ end
245
+
246
+ # Returns true if we should be adding the default callback.
247
+ # That is, if the given callback name is the default callback name (for the given action) and we
248
+ # are not skipping inline scripts for any reason.
249
+ private_class_method def self.recaptcha_v3_define_default_callback?(callback, action, options)
250
+ callback == recaptcha_v3_default_callback_name(action) &&
251
+ recaptcha_v3_inline_script?(options)
252
+ end
253
+
254
+ # Returns the name of the JavaScript function that actually executes the reCAPTCHA code (calls
255
+ # grecaptcha.execute). You can call it again later to reset it.
256
+ def self.recaptcha_v3_execute_function_name(action)
257
+ "executeRecaptchaFor#{sanitize_action_for_js(action)}"
258
+ end
259
+
260
+ # Returns the name of an async JavaScript function that executes the reCAPTCHA code.
261
+ def self.recaptcha_v3_async_execute_function_name(action)
262
+ "#{recaptcha_v3_execute_function_name(action)}Async"
263
+ end
264
+
265
+ def self.recaptcha_v3_default_callback_name(action)
266
+ "setInputWithRecaptchaResponseTokenFor#{sanitize_action_for_js(action)}"
267
+ end
268
+
269
+ # v2
270
+
271
+ private_class_method def self.default_callback(options = {})
272
+ nonce = options[:nonce]
273
+ nonce_attr = " nonce='#{nonce}'" if nonce
274
+ selector_attr = options[:id] ? "##{options[:id]}" : ".g-recaptcha"
275
+
276
+ <<-HTML
277
+ <script#{nonce_attr}>
278
+ var invisibleRecaptchaSubmit = function () {
279
+ var closestForm = function (ele) {
280
+ var curEle = ele.parentNode;
281
+ while (curEle.nodeName !== 'FORM' && curEle.nodeName !== 'BODY'){
282
+ curEle = curEle.parentNode;
283
+ }
284
+ return curEle.nodeName === 'FORM' ? curEle : null
285
+ };
286
+
287
+ var el = document.querySelector("#{selector_attr}")
288
+ if (!!el) {
289
+ var form = closestForm(el);
290
+ if (form) {
291
+ form.submit();
292
+ }
293
+ }
294
+ };
295
+ </script>
296
+ HTML
297
+ end
298
+
299
+ private_class_method def self.default_callback_required?(options)
300
+ options[:callback] == 'invisibleRecaptchaSubmit' &&
301
+ !Recaptcha.skip_env?(options[:env]) &&
302
+ options[:script] != false &&
303
+ options[:inline_script] != false
304
+ end
305
+
306
+ # Returns a camelized string that is safe for use in a JavaScript variable/function name.
307
+ # sanitize_action_for_js('my/action') => 'MyAction'
308
+ private_class_method def self.sanitize_action_for_js(action)
309
+ action.to_s.gsub(/\W/, '_').split(/\/|_/).map(&:capitalize).join
310
+ end
311
+
312
+ # Returns a dasherized string that is safe for use as an HTML ID
313
+ # dasherize_action('my/action') => 'my-action'
314
+ private_class_method def self.dasherize_action(action)
315
+ action.to_s.gsub(/\W/, '-').tr('_', '-')
316
+ end
317
+
318
+ private_class_method def self.hash_to_query(hash)
319
+ hash.delete_if { |_, val| val.nil? || val.empty? }.to_a.map { |pair| pair.join('=') }.join('&')
320
+ end
321
+ end
322
+ end