recaptcha 1.3.0 → 5.8.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.
@@ -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
@@ -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,51 +25,60 @@ 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 :skip_verify_env, :private_key, :public_key, :proxy, :handle_timeouts_gracefully, :use_ssl_by_default
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/v1beta1/projects'
38
+ }.freeze
39
+
40
+ attr_accessor :default_env, :skip_verify_env, :proxy, :secret_key, :site_key, :handle_timeouts_gracefully, :hostname
41
+ attr_accessor :enterprise, :enterprise_api_key, :enterprise_project_id
42
+ attr_writer :api_server_url, :verify_url
32
43
 
33
44
  def initialize #:nodoc:
34
- @skip_verify_env = SKIP_VERIFY_ENV
35
- @handle_timeouts_gracefully = HANDLE_TIMEOUTS_GRACEFULLY
36
- @use_ssl_by_default = USE_SSL_BY_DEFAULT
45
+ @default_env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || (Rails.env if defined? Rails.env)
46
+ @skip_verify_env = %w[test cucumber]
47
+ @handle_timeouts_gracefully = true
48
+
49
+ @secret_key = ENV['RECAPTCHA_SECRET_KEY']
50
+ @site_key = ENV['RECAPTCHA_SITE_KEY']
37
51
 
38
- @private_key = ENV['RECAPTCHA_PRIVATE_KEY']
39
- @public_key = ENV['RECAPTCHA_PUBLIC_KEY']
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
+
56
+ @verify_url = nil
57
+ @api_server_url = nil
40
58
  end
41
59
 
42
- def private_key!
43
- private_key || raise(RecaptchaError, "No private key specified.")
60
+ def secret_key!
61
+ secret_key || raise(RecaptchaError, "No secret key specified.")
44
62
  end
45
63
 
46
- def public_key!
47
- public_key || raise(RecaptchaError, "No public key specified.")
64
+ def site_key!
65
+ site_key || raise(RecaptchaError, "No site key specified.")
48
66
  end
49
67
 
50
- def api_server_url(ssl: nil)
51
- ssl = use_ssl_by_default if ssl.nil?
52
- key = (ssl ? 'secure_server_url' : 'server_url')
53
- CONFIG.fetch(key)
68
+ def enterprise_api_key!
69
+ enterprise_api_key || raise(RecaptchaError, "No Enterprise API key specified.")
54
70
  end
55
71
 
56
- def verify_url
57
- CONFIG.fetch('verify_url')
72
+ def enterprise_project_id!
73
+ enterprise_project_id || raise(RecaptchaError, "No Enterprise project ID specified.")
58
74
  end
59
75
 
60
- def api_version=(v)
61
- if v == 'v2'
62
- warn 'setting api_version is deprecated and will be removed shortly, only v2 is supported'
63
- else
64
- raise ArgumentError, "only v2 is supported, not #{v.inspect}"
65
- end
76
+ def api_server_url
77
+ @api_server_url || (enterprise ? DEFAULTS.fetch('enterprise_server_url') : DEFAULTS.fetch('free_server_url'))
66
78
  end
67
79
 
68
- def api_version
69
- warn 'getting api_version is deprecated and will be removed shortly, only v2 is supported'
70
- 'v2'
80
+ def verify_url
81
+ @verify_url || (enterprise ? DEFAULTS.fetch('enterprise_verify_url') : DEFAULTS.fetch('free_verify_url'))
71
82
  end
72
83
  end
73
84
  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,10 +1,4 @@
1
- require 'recaptcha'
1
+ # frozen_string_literal: true
2
2
 
3
- module Recaptcha
4
- class Railtie < Rails::Railtie
5
- initializer :recaptcha do
6
- ActionView::Base.send(:include, ::Recaptcha::ClientHelper)
7
- ActionController::Base.send(:include, ::Recaptcha::Verify)
8
- end
9
- end
10
- end
3
+ # deprecated, but let's not blow everyone up
4
+ require 'recaptcha'
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Recaptcha
4
+ class Railtie < Rails::Railtie
5
+ ActiveSupport.on_load(:action_view) do
6
+ include Recaptcha::Adapters::ViewMethods
7
+ end
8
+
9
+ ActiveSupport.on_load(:action_controller) do
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
33
+ end
34
+ end
35
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Recaptcha
2
- VERSION = "1.3.0"
4
+ VERSION = '5.8.1'
3
5
  end