recaptcha 4.14.0 → 5.14.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,26 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'recaptcha/configuration'
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
23
17
 
18
+ class RecaptchaError < StandardError
19
+ end
20
+
21
+ class VerifyError < RecaptchaError
22
+ end
23
+
24
24
  # Gives access to the current Configuration.
25
25
  def self.configuration
26
26
  @configuration ||= Configuration.new
@@ -50,33 +50,123 @@ module Recaptcha
50
50
  original_config.each { |key, value| configuration.send("#{key}=", value) }
51
51
  end
52
52
 
53
- def self.get(verify_hash, options)
54
- http = if Recaptcha.configuration.proxy
55
- proxy_server = URI.parse(Recaptcha.configuration.proxy)
56
- Net::HTTP::Proxy(proxy_server.host, proxy_server.port, proxy_server.user, proxy_server.password)
53
+ def self.skip_env?(env)
54
+ configuration.skip_verify_env.include?(env || configuration.default_env)
55
+ end
56
+
57
+ def self.invalid_response?(resp)
58
+ resp.empty? || resp.length > configuration.response_limit
59
+ end
60
+
61
+ def self.verify_via_api_call(response, options)
62
+ if Recaptcha.configuration.enterprise
63
+ verify_via_api_call_enterprise(response, options)
57
64
  else
58
- Net::HTTP
65
+ verify_via_api_call_free(response, options)
59
66
  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
67
  end
68
68
 
69
- def self.i18n(key, default)
70
- if defined?(I18n)
71
- I18n.translate(key, default: default)
69
+ def self.verify_via_api_call_enterprise(response, options)
70
+ site_key = options.fetch(:site_key) { configuration.site_key! }
71
+ api_key = options.fetch(:enterprise_api_key) { configuration.enterprise_api_key! }
72
+ project_id = options.fetch(:enterprise_project_id) { configuration.enterprise_project_id! }
73
+
74
+ query_params = { 'key' => api_key }
75
+ body = { 'event' => { 'token' => response, 'siteKey' => site_key } }
76
+ body['event']['expectedAction'] = options[:action] if options.key?(:action)
77
+ body['event']['userIpAddress'] = options[:remote_ip] if options.key?(:remote_ip)
78
+
79
+ reply = api_verification_enterprise(query_params, body, project_id, timeout: options[:timeout])
80
+ score = reply.dig('riskAnalysis', 'score')
81
+ token_properties = reply['tokenProperties']
82
+ success = !token_properties.nil? &&
83
+ token_properties['valid'].to_s == 'true' &&
84
+ hostname_valid?(token_properties['hostname'], options[:hostname]) &&
85
+ action_valid?(token_properties['action'], options[:action]) &&
86
+ score_above_threshold?(score, options[:minimum_score]) &&
87
+ score_below_threshold?(score, options[:maximum_score])
88
+
89
+ if options[:with_reply] == true
90
+ [success, reply]
72
91
  else
73
- default
92
+ success
74
93
  end
75
94
  end
76
95
 
77
- class RecaptchaError < StandardError
96
+ def self.verify_via_api_call_free(response, options)
97
+ secret_key = options.fetch(:secret_key) { configuration.secret_key! }
98
+ verify_hash = { 'secret' => secret_key, 'response' => response }
99
+ verify_hash['remoteip'] = options[:remote_ip] if options.key?(:remote_ip)
100
+
101
+ reply = api_verification_free(verify_hash, timeout: options[:timeout])
102
+ success = reply['success'].to_s == 'true' &&
103
+ hostname_valid?(reply['hostname'], options[:hostname]) &&
104
+ action_valid?(reply['action'], options[:action]) &&
105
+ score_above_threshold?(reply['score'], options[:minimum_score]) &&
106
+ score_below_threshold?(reply['score'], options[:maximum_score])
107
+
108
+ if options[:with_reply] == true
109
+ [success, reply]
110
+ else
111
+ success
112
+ end
78
113
  end
79
114
 
80
- class VerifyError < RecaptchaError
115
+ def self.hostname_valid?(hostname, validation)
116
+ validation ||= configuration.hostname
117
+
118
+ case validation
119
+ when nil, FalseClass then true
120
+ when String then validation == hostname
121
+ else validation.call(hostname)
122
+ end
123
+ end
124
+
125
+ def self.action_valid?(action, expected_action)
126
+ case expected_action
127
+ when nil, FalseClass then true
128
+ else action == expected_action.to_s
129
+ end
130
+ end
131
+
132
+ def self.score_above_threshold?(score, minimum_score)
133
+ !minimum_score || (score && score >= minimum_score)
134
+ end
135
+
136
+ def self.score_below_threshold?(score, maximum_score)
137
+ !maximum_score || (score && score <= maximum_score)
138
+ end
139
+
140
+ def self.http_client_for(uri:, timeout: nil)
141
+ timeout ||= DEFAULT_TIMEOUT
142
+ http = if configuration.proxy
143
+ proxy_server = URI.parse(configuration.proxy)
144
+ Net::HTTP::Proxy(proxy_server.host, proxy_server.port, proxy_server.user, proxy_server.password)
145
+ else
146
+ Net::HTTP
147
+ end
148
+ instance = http.new(uri.host, uri.port)
149
+ instance.read_timeout = instance.open_timeout = timeout
150
+ instance.use_ssl = true if uri.port == 443
151
+
152
+ instance
153
+ end
154
+
155
+ def self.api_verification_free(verify_hash, timeout: nil)
156
+ query = URI.encode_www_form(verify_hash)
157
+ uri = URI.parse("#{configuration.verify_url}?#{query}")
158
+ http_instance = http_client_for(uri: uri, timeout: timeout)
159
+ request = Net::HTTP::Get.new(uri.request_uri)
160
+ JSON.parse(http_instance.request(request).body)
161
+ end
162
+
163
+ def self.api_verification_enterprise(query_params, body, project_id, timeout: nil)
164
+ query = URI.encode_www_form(query_params)
165
+ uri = URI.parse("#{configuration.verify_url}/#{project_id}/assessments?#{query}")
166
+ http_instance = http_client_for(uri: uri, timeout: timeout)
167
+ request = Net::HTTP::Post.new(uri.request_uri)
168
+ request['Content-Type'] = 'application/json; charset=utf-8'
169
+ request.body = JSON.generate(body)
170
+ JSON.parse(http_instance.request(request).body)
81
171
  end
82
172
  end
@@ -0,0 +1,5 @@
1
+ en:
2
+ recaptcha:
3
+ errors:
4
+ verification_failed: reCAPTCHA verification failed, please try again.
5
+ recaptcha_unreachable: Oops, we failed to validate your reCAPTCHA response. Please try again.
@@ -0,0 +1,5 @@
1
+ fr:
2
+ recaptcha:
3
+ errors:
4
+ verification_failed: La vérification reCAPTCHA a échoué, veuillez essayer à nouveau.
5
+ recaptcha_unreachable: Oops, nous n'avons pas pu valider votre réponse reCAPTCHA. Veuillez essayer à nouveau.
@@ -0,0 +1,5 @@
1
+ ja:
2
+ recaptcha:
3
+ errors:
4
+ verification_failed: reCAPTCHA認証に失敗しました。もう一度お試しください。
5
+ recaptcha_unreachable: reCAPTCHAのレスポンスを検証できませんでした。もう一度お試しください。
@@ -0,0 +1,5 @@
1
+ nl:
2
+ recaptcha:
3
+ errors:
4
+ verification_failed: reCAPTCHA-verificatie mislukt, probeer het opnieuw.
5
+ recaptcha_unreachable: Oeps, we hebben uw reCAPTCHA-antwoord niet kunnen valideren. Probeer het opnieuw.
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: recaptcha
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.14.0
4
+ version: 5.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason L Perry
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-04-01 00:00:00.000000000 Z
11
+ date: 2023-04-19 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: json
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: mocha
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -52,20 +38,6 @@ dependencies:
52
38
  - - ">="
53
39
  - !ruby/object:Gem::Version
54
40
  version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: activesupport
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
41
  - !ruby/object:Gem::Dependency
70
42
  name: i18n
71
43
  requirement: !ruby/object:Gem::Requirement
@@ -161,17 +133,23 @@ files:
161
133
  - LICENSE
162
134
  - README.md
163
135
  - lib/recaptcha.rb
164
- - lib/recaptcha/client_helper.rb
136
+ - lib/recaptcha/adapters/controller_methods.rb
137
+ - lib/recaptcha/adapters/view_methods.rb
165
138
  - lib/recaptcha/configuration.rb
139
+ - lib/recaptcha/helpers.rb
166
140
  - lib/recaptcha/rails.rb
167
141
  - lib/recaptcha/railtie.rb
168
- - lib/recaptcha/verify.rb
169
142
  - lib/recaptcha/version.rb
143
+ - rails/locales/en.yml
144
+ - rails/locales/fr.yml
145
+ - rails/locales/ja.yml
146
+ - rails/locales/nl.yml
170
147
  homepage: http://github.com/ambethia/recaptcha
171
148
  licenses:
172
149
  - MIT
173
- metadata: {}
174
- post_install_message:
150
+ metadata:
151
+ source_code_uri: https://github.com/ambethia/recaptcha
152
+ post_install_message:
175
153
  rdoc_options: []
176
154
  require_paths:
177
155
  - lib
@@ -179,16 +157,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
179
157
  requirements:
180
158
  - - ">="
181
159
  - !ruby/object:Gem::Version
182
- version: 2.3.0
160
+ version: 2.7.0
183
161
  required_rubygems_version: !ruby/object:Gem::Requirement
184
162
  requirements:
185
163
  - - ">="
186
164
  - !ruby/object:Gem::Version
187
165
  version: '0'
188
166
  requirements: []
189
- rubyforge_project:
190
- rubygems_version: 2.7.6
191
- signing_key:
167
+ rubygems_version: 3.3.3
168
+ signing_key:
192
169
  specification_version: 4
193
170
  summary: Helpers for the reCAPTCHA API
194
171
  test_files: []
@@ -1,157 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Recaptcha
4
- module ClientHelper
5
- # Your public API can be specified in the +options+ hash or preferably
6
- # using the Configuration.
7
- def recaptcha_tags(options = {})
8
- if options.key?(:stoken)
9
- raise(RecaptchaError, "Secure Token is deprecated. Please remove 'stoken' from your calls to recaptcha_tags.")
10
- end
11
- if options.key?(:ssl)
12
- raise(RecaptchaError, "SSL is now always true. Please remove 'ssl' from your calls to recaptcha_tags.")
13
- end
14
-
15
- noscript = options.delete(:noscript)
16
-
17
- html, tag_attributes, fallback_uri = Recaptcha::ClientHelper.recaptcha_components(options)
18
- html << %(<div #{tag_attributes}></div>\n)
19
-
20
- if noscript != false
21
- html << <<-HTML
22
- <noscript>
23
- <div>
24
- <div style="width: 302px; height: 422px; position: relative;">
25
- <div style="width: 302px; height: 422px; position: absolute;">
26
- <iframe
27
- src="#{fallback_uri}"
28
- name="ReCAPTCHA"
29
- style="width: 302px; height: 422px; border-style: none; border: 0; overflow: hidden;">
30
- </iframe>
31
- </div>
32
- </div>
33
- <div style="width: 300px; height: 60px; border-style: none;
34
- bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px;
35
- background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;">
36
- <textarea id="g-recaptcha-response" name="g-recaptcha-response"
37
- class="g-recaptcha-response"
38
- style="width: 250px; height: 40px; border: 1px solid #c1c1c1;
39
- margin: 10px 25px; padding: 0px; resize: none;">
40
- </textarea>
41
- </div>
42
- </div>
43
- </noscript>
44
- HTML
45
- end
46
-
47
- html.respond_to?(:html_safe) ? html.html_safe : html
48
- end
49
-
50
- # Invisible reCAPTCHA implementation
51
- def invisible_recaptcha_tags(options = {})
52
- options = {callback: 'invisibleRecaptchaSubmit', ui: :button}.merge options
53
- text = options.delete(:text)
54
- html, tag_attributes = Recaptcha::ClientHelper.recaptcha_components(options)
55
- html << recaptcha_default_callback(options) if recaptcha_default_callback_required?(options)
56
- case options[:ui]
57
- when :button
58
- html << %(<button type="submit" #{tag_attributes}>#{text}</button>\n)
59
- when :invisible
60
- html << %(<div data-size="invisible" #{tag_attributes}></div>\n)
61
- when :input
62
- html << %(<input type="submit" #{tag_attributes} value="#{text}"/>\n)
63
- else
64
- raise(RecaptchaError, "ReCAPTCHA ui `#{options[:ui]}` is not valid.")
65
- end
66
- html.respond_to?(:html_safe) ? html.html_safe : html
67
- end
68
-
69
- def self.recaptcha_components(options = {})
70
- html = +''
71
- attributes = {}
72
- fallback_uri = +''
73
-
74
- # Since leftover options get passed directly through as tag
75
- # attributes, we must unconditionally delete all our options
76
- options = options.dup
77
- env = options.delete(:env)
78
- class_attribute = options.delete(:class)
79
- site_key = options.delete(:site_key)
80
- hl = options.delete(:hl)
81
- onload = options.delete(:onload)
82
- render = options.delete(:render)
83
- nonce = options.delete(:nonce)
84
- skip_script = (options.delete(:script) == false)
85
- ui = options.delete(:ui)
86
-
87
- data_attribute_keys = [:badge, :theme, :type, :callback, :expired_callback, :error_callback, :size]
88
- data_attribute_keys << :tabindex unless ui == :button
89
- data_attributes = {}
90
- data_attribute_keys.each do |data_attribute|
91
- value = options.delete(data_attribute)
92
- data_attributes["data-#{data_attribute.to_s.tr('_', '-')}"] = value if value
93
- end
94
-
95
- unless Recaptcha::Verify.skip?(env)
96
- site_key ||= Recaptcha.configuration.site_key!
97
- script_url = Recaptcha.configuration.api_server_url
98
- query_params = hash_to_query(
99
- hl: hl,
100
- onload: onload,
101
- render: render
102
- )
103
- script_url += "?#{query_params}" unless query_params.empty?
104
- nonce_attr = " nonce='#{nonce}'" if nonce
105
- html << %(<script src="#{script_url}" async defer#{nonce_attr}></script>\n) unless skip_script
106
- fallback_uri = %(#{script_url.chomp(".js")}/fallback?k=#{site_key})
107
- attributes["data-sitekey"] = site_key
108
- attributes.merge! data_attributes
109
- end
110
-
111
- # Append whatever that's left of options to be attributes on the tag.
112
- attributes["class"] = "g-recaptcha #{class_attribute}"
113
- tag_attributes = attributes.merge(options).map { |k, v| %(#{k}="#{v}") }.join(" ")
114
-
115
- [html, tag_attributes, fallback_uri]
116
- end
117
-
118
- private
119
-
120
- def recaptcha_default_callback(options = {})
121
- nonce = options[:nonce]
122
- nonce_attr = " nonce='#{nonce}'" if nonce
123
-
124
- <<-HTML
125
- <script#{nonce_attr}>
126
- var invisibleRecaptchaSubmit = function () {
127
- var closestForm = function (ele) {
128
- var curEle = ele.parentNode;
129
- while (curEle.nodeName !== 'FORM' && curEle.nodeName !== 'BODY'){
130
- curEle = curEle.parentNode;
131
- }
132
- return curEle.nodeName === 'FORM' ? curEle : null
133
- };
134
-
135
- var eles = document.getElementsByClassName('g-recaptcha');
136
- if (eles.length > 0) {
137
- var form = closestForm(eles[0]);
138
- if (form) {
139
- form.submit();
140
- }
141
- }
142
- };
143
- </script>
144
- HTML
145
- end
146
-
147
- def recaptcha_default_callback_required?(options)
148
- options[:callback] == 'invisibleRecaptchaSubmit' &&
149
- !Recaptcha::Verify.skip?(options[:env]) &&
150
- options[:script] != false
151
- end
152
-
153
- private_class_method def self.hash_to_query(hash)
154
- hash.delete_if { |_, val| val.nil? || val.empty? }.to_a.map { |pair| pair.join('=') }.join('&')
155
- end
156
- end
157
- end
@@ -1,108 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
-
5
- module Recaptcha
6
- module Verify
7
- G_RESPONSE_LIMIT = 4000
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::Verify.skip?(options[:env])
13
-
14
- model = options[:model]
15
- attribute = options[:attribute] || :base
16
- recaptcha_response = options[:response] || params['g-recaptcha-response'].to_s
17
-
18
- begin
19
- verified = if recaptcha_response.empty? || recaptcha_response.length > G_RESPONSE_LIMIT
20
- false
21
- else
22
- recaptcha_verify_via_api_call(request, recaptcha_response, options)
23
- end
24
-
25
- if verified
26
- flash.delete(:recaptcha_error) if recaptcha_flash_supported? && !model
27
- true
28
- else
29
- recaptcha_error(
30
- model,
31
- attribute,
32
- options[:message],
33
- "recaptcha.errors.verification_failed",
34
- "reCAPTCHA verification failed, please try again."
35
- )
36
- false
37
- end
38
- rescue Timeout::Error
39
- if Recaptcha.configuration.handle_timeouts_gracefully
40
- recaptcha_error(
41
- model,
42
- attribute,
43
- options[:message],
44
- "recaptcha.errors.recaptcha_unreachable",
45
- "Oops, we failed to validate your reCAPTCHA response. Please try again."
46
- )
47
- false
48
- else
49
- raise RecaptchaError, "Recaptcha unreachable."
50
- end
51
- rescue StandardError => e
52
- raise RecaptchaError, e.message, e.backtrace
53
- end
54
- end
55
-
56
- def verify_recaptcha!(options = {})
57
- verify_recaptcha(options) || raise(VerifyError)
58
- end
59
-
60
- def self.skip?(env)
61
- env ||= ENV['RAILS_ENV'] || ENV['RACK_ENV'] || (Rails.env if defined? Rails.env)
62
- Recaptcha.configuration.skip_verify_env.include? env
63
- end
64
-
65
- private
66
-
67
- def recaptcha_verify_via_api_call(request, recaptcha_response, options)
68
- secret_key = options[:secret_key] || Recaptcha.configuration.secret_key!
69
-
70
- verify_hash = {
71
- "secret" => secret_key,
72
- "response" => recaptcha_response
73
- }
74
-
75
- unless options[:skip_remote_ip]
76
- remoteip = (request.respond_to?(:remote_ip) && request.remote_ip) || (env && env['REMOTE_ADDR'])
77
- verify_hash["remoteip"] = remoteip.to_s
78
- end
79
-
80
- reply = JSON.parse(Recaptcha.get(verify_hash, options))
81
- reply['success'].to_s == "true" &&
82
- recaptcha_hostname_valid?(reply['hostname'], options[:hostname])
83
- end
84
-
85
- def recaptcha_hostname_valid?(hostname, validation)
86
- validation ||= Recaptcha.configuration.hostname
87
-
88
- case validation
89
- when nil, FalseClass then true
90
- when String then validation == hostname
91
- else validation.call(hostname)
92
- end
93
- end
94
-
95
- def recaptcha_error(model, attribute, message, key, default)
96
- message ||= Recaptcha.i18n(key, default)
97
- if model
98
- model.errors.add attribute, message
99
- else
100
- flash[:recaptcha_error] = message if recaptcha_flash_supported?
101
- end
102
- end
103
-
104
- def recaptcha_flash_supported?
105
- request.respond_to?(:format) && request.format == :html && respond_to?(:flash)
106
- end
107
- end
108
- end