recaptcha 4.14.0 → 5.2.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +340 -75
- data/lib/recaptcha.rb +68 -32
- data/lib/recaptcha/adapters/controller_methods.rb +87 -0
- data/lib/recaptcha/adapters/view_methods.rb +26 -0
- data/lib/recaptcha/configuration.rb +10 -4
- data/lib/recaptcha/helpers.rb +298 -0
- data/lib/recaptcha/railtie.rb +24 -4
- data/lib/recaptcha/version.rb +1 -1
- data/rails/locales/en.yml +5 -0
- metadata +9 -21
- data/lib/recaptcha/client_helper.rb +0 -157
- data/lib/recaptcha/verify.rb +0 -108
data/lib/recaptcha.rb
CHANGED
@@ -1,25 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
4
|
-
require 'uri'
|
3
|
+
require 'json'
|
5
4
|
require 'net/http'
|
5
|
+
require 'uri'
|
6
6
|
|
7
|
+
require 'recaptcha/configuration'
|
8
|
+
require 'recaptcha/helpers'
|
9
|
+
require 'recaptcha/adapters/controller_methods'
|
10
|
+
require 'recaptcha/adapters/view_methods'
|
7
11
|
if defined?(Rails)
|
8
12
|
require 'recaptcha/railtie'
|
9
|
-
else
|
10
|
-
require 'recaptcha/client_helper'
|
11
|
-
require 'recaptcha/verify'
|
12
13
|
end
|
13
14
|
|
14
15
|
module Recaptcha
|
15
|
-
CONFIG = {
|
16
|
-
'server_url' => 'https://www.google.com/recaptcha/api.js',
|
17
|
-
'verify_url' => 'https://www.google.com/recaptcha/api/siteverify'
|
18
|
-
}.freeze
|
19
|
-
|
20
|
-
USE_SSL_BY_DEFAULT = false
|
21
|
-
HANDLE_TIMEOUTS_GRACEFULLY = true
|
22
16
|
DEFAULT_TIMEOUT = 3
|
17
|
+
RESPONSE_LIMIT = 4000
|
18
|
+
|
19
|
+
class RecaptchaError < StandardError
|
20
|
+
end
|
21
|
+
|
22
|
+
class VerifyError < RecaptchaError
|
23
|
+
end
|
23
24
|
|
24
25
|
# Gives access to the current Configuration.
|
25
26
|
def self.configuration
|
@@ -50,33 +51,68 @@ module Recaptcha
|
|
50
51
|
original_config.each { |key, value| configuration.send("#{key}=", value) }
|
51
52
|
end
|
52
53
|
|
53
|
-
def self.
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
54
|
+
def self.skip_env?(env)
|
55
|
+
configuration.skip_verify_env.include?(env || configuration.default_env)
|
56
|
+
end
|
57
|
+
|
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
|
+
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
|
+
end
|
73
|
+
|
74
|
+
def self.hostname_valid?(hostname, validation)
|
75
|
+
validation ||= configuration.hostname
|
76
|
+
|
77
|
+
case validation
|
78
|
+
when nil, FalseClass then true
|
79
|
+
when String then validation == hostname
|
80
|
+
else validation.call(hostname)
|
59
81
|
end
|
60
|
-
query = URI.encode_www_form(verify_hash)
|
61
|
-
uri = URI.parse(Recaptcha.configuration.verify_url + '?' + query)
|
62
|
-
http_instance = http.new(uri.host, uri.port)
|
63
|
-
http_instance.read_timeout = http_instance.open_timeout = options[:timeout] || DEFAULT_TIMEOUT
|
64
|
-
http_instance.use_ssl = true if uri.port == 443
|
65
|
-
request = Net::HTTP::Get.new(uri.request_uri)
|
66
|
-
http_instance.request(request).body
|
67
82
|
end
|
68
83
|
|
69
|
-
def self.
|
70
|
-
|
71
|
-
|
72
|
-
else
|
73
|
-
default
|
84
|
+
def self.action_valid?(action, expected_action)
|
85
|
+
case expected_action
|
86
|
+
when nil, FalseClass then true
|
87
|
+
else action == expected_action
|
74
88
|
end
|
75
89
|
end
|
76
90
|
|
77
|
-
|
91
|
+
# Returns true iff score is greater or equal to (>=) minimum_score, or if no minimum_score was specified
|
92
|
+
def self.score_above_threshold?(score, minimum_score)
|
93
|
+
return true if minimum_score.nil?
|
94
|
+
return false if score.nil?
|
95
|
+
|
96
|
+
case minimum_score
|
97
|
+
when nil, FalseClass then true
|
98
|
+
else score >= minimum_score
|
99
|
+
end
|
78
100
|
end
|
79
101
|
|
80
|
-
|
102
|
+
def self.api_verification(verify_hash, timeout: nil)
|
103
|
+
timeout ||= DEFAULT_TIMEOUT
|
104
|
+
http = if configuration.proxy
|
105
|
+
proxy_server = URI.parse(configuration.proxy)
|
106
|
+
Net::HTTP::Proxy(proxy_server.host, proxy_server.port, proxy_server.user, proxy_server.password)
|
107
|
+
else
|
108
|
+
Net::HTTP
|
109
|
+
end
|
110
|
+
query = URI.encode_www_form(verify_hash)
|
111
|
+
uri = URI.parse(configuration.verify_url + '?' + query)
|
112
|
+
http_instance = http.new(uri.host, uri.port)
|
113
|
+
http_instance.read_timeout = http_instance.open_timeout = timeout
|
114
|
+
http_instance.use_ssl = true if uri.port == 443
|
115
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
116
|
+
JSON.parse(http_instance.request(request).body)
|
81
117
|
end
|
82
118
|
end
|
@@ -0,0 +1,87 @@
|
|
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
|
+
Recaptcha.verify_via_api_call(recaptcha_response, options)
|
28
|
+
end
|
29
|
+
|
30
|
+
if verified
|
31
|
+
flash.delete(:recaptcha_error) if recaptcha_flash_supported? && !model
|
32
|
+
true
|
33
|
+
else
|
34
|
+
recaptcha_error(
|
35
|
+
model,
|
36
|
+
attribute,
|
37
|
+
options.fetch(:message) { Recaptcha::Helpers.to_error_message(:verification_failed) }
|
38
|
+
)
|
39
|
+
false
|
40
|
+
end
|
41
|
+
rescue Timeout::Error
|
42
|
+
if Recaptcha.configuration.handle_timeouts_gracefully
|
43
|
+
recaptcha_error(
|
44
|
+
model,
|
45
|
+
attribute,
|
46
|
+
options.fetch(:message) { Recaptcha::Helpers.to_error_message(:recaptcha_unreachable) }
|
47
|
+
)
|
48
|
+
false
|
49
|
+
else
|
50
|
+
raise RecaptchaError, 'Recaptcha unreachable.'
|
51
|
+
end
|
52
|
+
rescue StandardError => e
|
53
|
+
raise RecaptchaError, e.message, e.backtrace
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def verify_recaptcha!(options = {})
|
58
|
+
verify_recaptcha(options) || raise(VerifyError)
|
59
|
+
end
|
60
|
+
|
61
|
+
def recaptcha_error(model, attribute, message)
|
62
|
+
if model
|
63
|
+
model.errors.add(attribute, message)
|
64
|
+
elsif recaptcha_flash_supported?
|
65
|
+
flash[:recaptcha_error] = message
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def recaptcha_flash_supported?
|
70
|
+
request.respond_to?(:format) && request.format == :html && respond_to?(:flash)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Extracts response token from params. params['g-recaptcha-response'] should either be a
|
74
|
+
# string or a hash with the action name(s) as keys. If it is a hash, then `action` is used as
|
75
|
+
# the key.
|
76
|
+
# @return [String] A response token if one was passed in the params; otherwise, `''`
|
77
|
+
def recaptcha_response_token(action = nil)
|
78
|
+
response_param = params['g-recaptcha-response']
|
79
|
+
if response_param&.respond_to?(:to_h) # Includes ActionController::Parameters
|
80
|
+
response_param[action].to_s
|
81
|
+
else
|
82
|
+
response_param.to_s
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
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,12 +30,18 @@ module Recaptcha
|
|
30
30
|
# end
|
31
31
|
#
|
32
32
|
class Configuration
|
33
|
-
|
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
|
34
39
|
attr_writer :api_server_url, :verify_url
|
35
40
|
|
36
41
|
def initialize #:nodoc:
|
42
|
+
@default_env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || (Rails.env if defined? Rails.env)
|
37
43
|
@skip_verify_env = %w[test cucumber]
|
38
|
-
@handle_timeouts_gracefully =
|
44
|
+
@handle_timeouts_gracefully = true
|
39
45
|
|
40
46
|
@secret_key = ENV['RECAPTCHA_SECRET_KEY']
|
41
47
|
@site_key = ENV['RECAPTCHA_SITE_KEY']
|
@@ -52,11 +58,11 @@ module Recaptcha
|
|
52
58
|
end
|
53
59
|
|
54
60
|
def api_server_url
|
55
|
-
@api_server_url ||
|
61
|
+
@api_server_url || DEFAULTS.fetch('server_url')
|
56
62
|
end
|
57
63
|
|
58
64
|
def verify_url
|
59
|
-
@verify_url ||
|
65
|
+
@verify_url || DEFAULTS.fetch('verify_url')
|
60
66
|
end
|
61
67
|
end
|
62
68
|
end
|
@@ -0,0 +1,298 @@
|
|
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-" + dasherize_action(action)
|
14
|
+
name = options.delete(:name) || "g-recaptcha-response[#{action}]"
|
15
|
+
options[:render] = site_key
|
16
|
+
options[:script_async] ||= false
|
17
|
+
options[:script_defer] ||= false
|
18
|
+
element = options.delete(:element)
|
19
|
+
element = element == false ? false : :input
|
20
|
+
if element == :input
|
21
|
+
callback = options.delete(:callback) || recaptcha_v3_default_callback_name(action)
|
22
|
+
end
|
23
|
+
options[:class] = "g-recaptcha-response #{options[:class]}"
|
24
|
+
|
25
|
+
html, tag_attributes = components(options)
|
26
|
+
if recaptcha_v3_inline_script?(options)
|
27
|
+
html << recaptcha_v3_inline_script(site_key, action, callback, id, options)
|
28
|
+
end
|
29
|
+
case element
|
30
|
+
when :input
|
31
|
+
html << %(<input type="hidden" name="#{name}" id="#{id}" #{tag_attributes}/>\n)
|
32
|
+
when false
|
33
|
+
# No tag
|
34
|
+
nil
|
35
|
+
else
|
36
|
+
raise(RecaptchaError, "ReCAPTCHA element `#{options[:element]}` is not valid.")
|
37
|
+
end
|
38
|
+
html.respond_to?(:html_safe) ? html.html_safe : html
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.recaptcha_tags(options)
|
42
|
+
if options.key?(:stoken)
|
43
|
+
raise(RecaptchaError, "Secure Token is deprecated. Please remove 'stoken' from your calls to recaptcha_tags.")
|
44
|
+
end
|
45
|
+
if options.key?(:ssl)
|
46
|
+
raise(RecaptchaError, "SSL is now always true. Please remove 'ssl' from your calls to recaptcha_tags.")
|
47
|
+
end
|
48
|
+
|
49
|
+
noscript = options.delete(:noscript)
|
50
|
+
|
51
|
+
html, tag_attributes, fallback_uri = components(options.dup)
|
52
|
+
html << %(<div #{tag_attributes}></div>\n)
|
53
|
+
|
54
|
+
if noscript != false
|
55
|
+
html << <<-HTML
|
56
|
+
<noscript>
|
57
|
+
<div>
|
58
|
+
<div style="width: 302px; height: 422px; position: relative;">
|
59
|
+
<div style="width: 302px; height: 422px; position: absolute;">
|
60
|
+
<iframe
|
61
|
+
src="#{fallback_uri}"
|
62
|
+
name="ReCAPTCHA"
|
63
|
+
style="width: 302px; height: 422px; border-style: none; border: 0; overflow: hidden;">
|
64
|
+
</iframe>
|
65
|
+
</div>
|
66
|
+
</div>
|
67
|
+
<div style="width: 300px; height: 60px; border-style: none;
|
68
|
+
bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px;
|
69
|
+
background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;">
|
70
|
+
<textarea id="g-recaptcha-response" name="g-recaptcha-response"
|
71
|
+
class="g-recaptcha-response"
|
72
|
+
style="width: 250px; height: 40px; border: 1px solid #c1c1c1;
|
73
|
+
margin: 10px 25px; padding: 0px; resize: none;">
|
74
|
+
</textarea>
|
75
|
+
</div>
|
76
|
+
</div>
|
77
|
+
</noscript>
|
78
|
+
HTML
|
79
|
+
end
|
80
|
+
|
81
|
+
html.respond_to?(:html_safe) ? html.html_safe : html
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.invisible_recaptcha_tags(custom)
|
85
|
+
options = {callback: 'invisibleRecaptchaSubmit', ui: :button}.merge(custom)
|
86
|
+
text = options.delete(:text)
|
87
|
+
html, tag_attributes = components(options.dup)
|
88
|
+
html << default_callback(options) if default_callback_required?(options)
|
89
|
+
|
90
|
+
case options[:ui]
|
91
|
+
when :button
|
92
|
+
html << %(<button type="submit" #{tag_attributes}>#{text}</button>\n)
|
93
|
+
when :invisible
|
94
|
+
html << %(<div data-size="invisible" #{tag_attributes}></div>\n)
|
95
|
+
when :input
|
96
|
+
html << %(<input type="submit" #{tag_attributes} value="#{text}"/>\n)
|
97
|
+
else
|
98
|
+
raise(RecaptchaError, "ReCAPTCHA ui `#{options[:ui]}` is not valid.")
|
99
|
+
end
|
100
|
+
html.respond_to?(:html_safe) ? html.html_safe : html
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.to_error_message(key)
|
104
|
+
default = DEFAULT_ERRORS.fetch(key) { raise ArgumentError "Unknown reCAPTCHA error - #{key}" }
|
105
|
+
to_message("recaptcha.errors.#{key}", default)
|
106
|
+
end
|
107
|
+
|
108
|
+
if defined?(I18n)
|
109
|
+
def self.to_message(key, default)
|
110
|
+
I18n.translate(key, default: default)
|
111
|
+
end
|
112
|
+
else
|
113
|
+
def self.to_message(_key, default)
|
114
|
+
default
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
private_class_method def self.components(options)
|
119
|
+
html = +''
|
120
|
+
attributes = {}
|
121
|
+
fallback_uri = +''
|
122
|
+
|
123
|
+
options = options.dup
|
124
|
+
env = options.delete(:env)
|
125
|
+
class_attribute = options.delete(:class)
|
126
|
+
site_key = options.delete(:site_key)
|
127
|
+
hl = options.delete(:hl)
|
128
|
+
onload = options.delete(:onload)
|
129
|
+
render = options.delete(:render)
|
130
|
+
script_async = options.delete(:script_async)
|
131
|
+
script_defer = options.delete(:script_defer)
|
132
|
+
nonce = options.delete(:nonce)
|
133
|
+
skip_script = (options.delete(:script) == false) || (options.delete(:external_script) == false)
|
134
|
+
ui = options.delete(:ui)
|
135
|
+
|
136
|
+
data_attribute_keys = [:badge, :theme, :type, :callback, :expired_callback, :error_callback, :size]
|
137
|
+
data_attribute_keys << :tabindex unless ui == :button
|
138
|
+
data_attributes = {}
|
139
|
+
data_attribute_keys.each do |data_attribute|
|
140
|
+
value = options.delete(data_attribute)
|
141
|
+
data_attributes["data-#{data_attribute.to_s.tr('_', '-')}"] = value if value
|
142
|
+
end
|
143
|
+
|
144
|
+
unless Recaptcha.skip_env?(env)
|
145
|
+
site_key ||= Recaptcha.configuration.site_key!
|
146
|
+
script_url = Recaptcha.configuration.api_server_url
|
147
|
+
query_params = hash_to_query(
|
148
|
+
hl: hl,
|
149
|
+
onload: onload,
|
150
|
+
render: render
|
151
|
+
)
|
152
|
+
script_url += "?#{query_params}" unless query_params.empty?
|
153
|
+
async_attr = "async" if script_async != false
|
154
|
+
defer_attr = "defer" if script_defer != false
|
155
|
+
nonce_attr = " nonce='#{nonce}'" if nonce
|
156
|
+
html << %(<script src="#{script_url}" #{async_attr} #{defer_attr} #{nonce_attr}></script>\n) unless skip_script
|
157
|
+
fallback_uri = %(#{script_url.chomp(".js")}/fallback?k=#{site_key})
|
158
|
+
attributes["data-sitekey"] = site_key
|
159
|
+
attributes.merge! data_attributes
|
160
|
+
end
|
161
|
+
|
162
|
+
# The remaining options will be added as attributes on the tag.
|
163
|
+
attributes["class"] = "g-recaptcha #{class_attribute}"
|
164
|
+
tag_attributes = attributes.merge(options).map { |k, v| %(#{k}="#{v}") }.join(" ")
|
165
|
+
|
166
|
+
[html, tag_attributes, fallback_uri]
|
167
|
+
end
|
168
|
+
|
169
|
+
# v3
|
170
|
+
|
171
|
+
# Renders a script that calls `grecaptcha.execute` for the given `site_key` and `action` and
|
172
|
+
# calls the `callback` with the resulting response token.
|
173
|
+
private_class_method def self.recaptcha_v3_inline_script(site_key, action, callback, id, options = {})
|
174
|
+
nonce = options[:nonce]
|
175
|
+
nonce_attr = " nonce='#{nonce}'" if nonce
|
176
|
+
|
177
|
+
<<-HTML
|
178
|
+
<script#{nonce_attr}>
|
179
|
+
// Define function so that we can call it again later if we need to reset it
|
180
|
+
// This executes reCAPTCHA and then calls our callback.
|
181
|
+
function #{recaptcha_v3_execute_function_name(action)}() {
|
182
|
+
grecaptcha.ready(function() {
|
183
|
+
grecaptcha.execute('#{site_key}', {action: '#{action}'}).then(function(token) {
|
184
|
+
//console.log('#{id}', token)
|
185
|
+
#{callback}('#{id}', token)
|
186
|
+
});
|
187
|
+
});
|
188
|
+
};
|
189
|
+
// Invoke immediately
|
190
|
+
#{recaptcha_v3_execute_function_name(action)}()
|
191
|
+
|
192
|
+
// Async variant so you can await this function from another async function (no need for
|
193
|
+
// an explicit callback function then!)
|
194
|
+
// Returns a Promise that resolves with the response token.
|
195
|
+
async function #{recaptcha_v3_async_execute_function_name(action)}() {
|
196
|
+
return new Promise((resolve, reject) => {
|
197
|
+
grecaptcha.ready(async function() {
|
198
|
+
resolve(await grecaptcha.execute('#{site_key}', {action: '#{action}'}))
|
199
|
+
});
|
200
|
+
})
|
201
|
+
};
|
202
|
+
|
203
|
+
#{recaptcha_v3_define_default_callback(callback) if recaptcha_v3_define_default_callback?(callback, action, options)}
|
204
|
+
</script>
|
205
|
+
HTML
|
206
|
+
end
|
207
|
+
|
208
|
+
private_class_method def self.recaptcha_v3_inline_script?(options)
|
209
|
+
!Recaptcha.skip_env?(options[:env]) &&
|
210
|
+
options[:script] != false &&
|
211
|
+
options[:inline_script] != false
|
212
|
+
end
|
213
|
+
|
214
|
+
private_class_method def self.recaptcha_v3_define_default_callback(callback)
|
215
|
+
<<-HTML
|
216
|
+
var #{callback} = function(id, token) {
|
217
|
+
var element = document.getElementById(id);
|
218
|
+
element.value = token;
|
219
|
+
}
|
220
|
+
HTML
|
221
|
+
end
|
222
|
+
|
223
|
+
# Returns true if we should be adding the default callback.
|
224
|
+
# That is, if the given callback name is the default callback name (for the given action) and we
|
225
|
+
# are not skipping inline scripts for any reason.
|
226
|
+
private_class_method def self.recaptcha_v3_define_default_callback?(callback, action, options)
|
227
|
+
callback == recaptcha_v3_default_callback_name(action) &&
|
228
|
+
recaptcha_v3_inline_script?(options)
|
229
|
+
end
|
230
|
+
|
231
|
+
# Returns the name of the JavaScript function that actually executes the reCAPTCHA code (calls
|
232
|
+
# grecaptcha.execute). You can call it again later to reset it.
|
233
|
+
def self.recaptcha_v3_execute_function_name(action)
|
234
|
+
"executeRecaptchaFor#{sanitize_action_for_js(action)}"
|
235
|
+
end
|
236
|
+
|
237
|
+
# Returns the name of an async JavaScript function that executes the reCAPTCHA code.
|
238
|
+
def self.recaptcha_v3_async_execute_function_name(action)
|
239
|
+
"#{recaptcha_v3_execute_function_name(action)}Async"
|
240
|
+
end
|
241
|
+
|
242
|
+
def self.recaptcha_v3_default_callback_name(action)
|
243
|
+
"setInputWithRecaptchaResponseTokenFor#{sanitize_action_for_js(action)}"
|
244
|
+
end
|
245
|
+
|
246
|
+
# v2
|
247
|
+
|
248
|
+
private_class_method def self.default_callback(options = {})
|
249
|
+
nonce = options[:nonce]
|
250
|
+
nonce_attr = " nonce='#{nonce}'" if nonce
|
251
|
+
|
252
|
+
<<-HTML
|
253
|
+
<script#{nonce_attr}>
|
254
|
+
var invisibleRecaptchaSubmit = function () {
|
255
|
+
var closestForm = function (ele) {
|
256
|
+
var curEle = ele.parentNode;
|
257
|
+
while (curEle.nodeName !== 'FORM' && curEle.nodeName !== 'BODY'){
|
258
|
+
curEle = curEle.parentNode;
|
259
|
+
}
|
260
|
+
return curEle.nodeName === 'FORM' ? curEle : null
|
261
|
+
};
|
262
|
+
|
263
|
+
var eles = document.getElementsByClassName('g-recaptcha');
|
264
|
+
if (eles.length > 0) {
|
265
|
+
var form = closestForm(eles[0]);
|
266
|
+
if (form) {
|
267
|
+
form.submit();
|
268
|
+
}
|
269
|
+
}
|
270
|
+
};
|
271
|
+
</script>
|
272
|
+
HTML
|
273
|
+
end
|
274
|
+
|
275
|
+
private_class_method def self.default_callback_required?(options)
|
276
|
+
options[:callback] == 'invisibleRecaptchaSubmit' &&
|
277
|
+
!Recaptcha.skip_env?(options[:env]) &&
|
278
|
+
options[:script] != false &&
|
279
|
+
options[:inline_script] != false
|
280
|
+
end
|
281
|
+
|
282
|
+
# Returns a camelized string that is safe for use in a JavaScript variable/function name.
|
283
|
+
# sanitize_action_for_js('my/action') => 'MyAction'
|
284
|
+
private_class_method def self.sanitize_action_for_js(action)
|
285
|
+
action.to_s.gsub(/\W/, '_').split(/\/|_/).map(&:capitalize).join
|
286
|
+
end
|
287
|
+
|
288
|
+
# Returns a dasherized string that is safe for use as an HTML ID
|
289
|
+
# dasherize_action('my/action') => 'my-action'
|
290
|
+
private_class_method def self.dasherize_action(action)
|
291
|
+
action.to_s.gsub(/\W/, '-').tr('_', '-')
|
292
|
+
end
|
293
|
+
|
294
|
+
private_class_method def self.hash_to_query(hash)
|
295
|
+
hash.delete_if { |_, val| val.nil? || val.empty? }.to_a.map { |pair| pair.join('=') }.join('&')
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|