recaptcha 0.3.6 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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