Codebase list ruby-recaptcha / 973fdb8
Import upstream version 5.8.1 Debian Janitor 2 years ago
62 changed file(s) with 2479 addition(s) and 755 deletion(s). Raw diff Collapse all Expand all
0 Why and what is being done.
1
2 ## Pre-Merge Checklist
3 - [ ] CHANGELOG.md updated with short summary
0 name: CI
1
2 on:
3 pull_request:
4 push:
5 branches:
6 - master
7
8 jobs:
9 test:
10 name: Test ruby version matrix
11 runs-on: ubuntu-latest
12 strategy:
13 matrix:
14 ruby-version: ['2.4', '2.5', '2.6', '2.7', 'truffleruby-head']
15 steps:
16 - uses: actions/checkout@v2
17 - uses: ruby/setup-ruby@v1
18 with:
19 ruby-version: ${{ matrix.ruby-version }}
20 bundler-cache: true
21 - run: bundle exec rake test
22
23 rubocop:
24 name: Run rubocop
25 runs-on: ubuntu-latest
26 steps:
27 - uses: actions/checkout@v2
28 - uses: ruby/setup-ruby@v1
29 with:
30 ruby-version: '2.4'
31 bundler-cache: true
32 - run: bundle exec rake rubocop
55
66 .gems
77 .rbenv-gemsets
8
9 /demo/*/Gemfile.lock
00 AllCops:
1 TargetRubyVersion: 2.3
1 TargetRubyVersion: 2.4
22 Include:
3 - 'lib/**/*'
34 - 'Rakefile'
45 - 'Gemfile'
5 - 'Rakefile'
66 Exclude:
77 - 'vendor/**/*'
88 - 'demo/**/*'
4949 Style/NumericLiterals:
5050 Enabled: false
5151
52 Layout/FirstParameterIndentation:
52 Layout/IndentFirstArgument:
5353 Enabled: false
5454
55 Layout/IndentHash:
55 Layout/IndentFirstHashElement:
5656 Enabled: false
5757
5858 Layout/AlignParameters:
6565 Enabled: false
6666
6767 Metrics/PerceivedComplexity:
68 Enabled: false
69
70 Metrics/CyclomaticComplexity:
7168 Enabled: false
7269
7370 Style/DoubleNegation:
+0
-19
.travis.yml less more
0 language: ruby
1 cache: bundler
2 sudo: false
3 rvm:
4 - 2.3
5 - 2.4
6 - 2.5
7 env:
8 - TASK=test
9 matrix:
10 include:
11 - rvm: 2.3 # keep same as lowest ruby version
12 env: TASK=rubocop
13 script: bundle exec rake $TASK
14 branches:
15 only: master
16 matrix:
17 fast_finish: true
18
0 ## Next
1 * Gracefully handle invalid params
2
3 ## 5.8.0
4 * Add support for the enterprise API
5
6 ## 5.7.0
7 * french locale
8 * drop ruby 2.3
9
10 ## 5.6.0
11 * Allow multiple invisible recaptchas on a single page by setting custom selector
12
13 ## 5.5.0
14 * add `recaptcha_reply` controller method for better debugging/inspection
15
16 ## 5.4.1
17 * fix v2 vs 'data' postfix
18
19 ## 5.4.0
20 * added 'data' postfix to g-recaptcha-response attribute name to avoid collisions
21
22 ## 5.3.0
23 * turbolinks support
24
25 ## 5.2.0
26 * remove dependency on rails methods
27
28 ## 5.1.0
29 * Added default translations for rails/i18n
30 * use recaptcha.net for the script tag
31
32 ## 5.0.0
33 * Changed host to Recaptcha.net
34 * Add v3 API support
35 * Renamed `Recaptcha::ClientHelper` to `Recaptcha::Adapters::ViewMethods`
36 * Renamed `Recaptcha::Verify` to `Recaptcha::Adapters::ControllerMethods`
37
38 ## 4.12.0 - 2018-08-30
39 * add `input` option to `invisible_recaptcha_tags`'s `ui` setting
40
41 ## 4.11.1 - 2018-08-08
42 * leave `tabindex` attribute alone for `invisible_recaptcha_tags`
43
044 ## 4.11.0 - 2018-08-06
145 * prefer RAILS_ENV over RACK_ENV #286
246
59103 * support disabling stoken
60104 * support Rails.env
61105
106 ## 0.4.0 / 2015-03-22
107
108 * Add support for ReCaptcha v2 API
109 * V2 API requires `g-recaptcha-response` parameters; https://github.com/ambethia/recaptcha/pull/114
110
62111 ## 0.3.6 / 2012-01-07
63112
64113 * Many documentation changes
00 PATH
11 remote: .
22 specs:
3 recaptcha (4.11.1)
3 recaptcha (5.8.1)
44 json
55
66 GEM
77 remote: https://rubygems.org/
88 specs:
9 activesupport (5.2.0)
10 concurrent-ruby (~> 1.0, >= 1.0.2)
11 i18n (>= 0.7, < 2)
12 minitest (~> 5.1)
13 tzinfo (~> 1.1)
14 addressable (2.5.2)
9 addressable (2.6.0)
1510 public_suffix (>= 2.0.2, < 4.0)
1611 ast (2.4.0)
17 bump (0.6.1)
18 byebug (10.0.2)
12 bump (0.8.0)
13 byebug (11.0.1)
1914 coderay (1.1.2)
20 concurrent-ruby (1.0.5)
15 concurrent-ruby (1.1.5)
2116 crack (0.4.3)
2217 safe_yaml (~> 1.0.0)
23 hashdiff (0.3.7)
24 i18n (1.0.1)
18 hashdiff (0.3.9)
19 i18n (1.6.0)
2520 concurrent-ruby (~> 1.0)
26 jaro_winkler (1.5.1)
27 json (2.1.0)
21 jaro_winkler (1.5.2)
22 json (2.5.1)
2823 maxitest (3.1.0)
2924 minitest (>= 5.0.0, < 5.12.0)
3025 metaclass (0.0.4)
31 method_source (0.9.0)
26 method_source (0.9.2)
3227 minitest (5.11.3)
33 mocha (1.5.0)
28 mocha (1.8.0)
3429 metaclass (~> 0.0.1)
35 parallel (1.12.1)
36 parser (2.5.1.0)
30 parallel (1.17.0)
31 parser (2.6.3.0)
3732 ast (~> 2.4.0)
38 powerpack (0.1.2)
39 pry (0.11.3)
33 pry (0.12.2)
4034 coderay (~> 1.1.0)
4135 method_source (~> 0.9.0)
42 pry-byebug (3.6.0)
43 byebug (~> 10.0)
36 pry-byebug (3.7.0)
37 byebug (~> 11.0)
4438 pry (~> 0.10)
45 public_suffix (3.0.2)
39 public_suffix (3.0.3)
4640 rainbow (3.0.0)
47 rake (12.3.1)
48 rubocop (0.57.2)
41 rake (12.3.2)
42 rubocop (0.68.1)
4943 jaro_winkler (~> 1.5.1)
5044 parallel (~> 1.10)
51 parser (>= 2.5)
52 powerpack (~> 0.1)
45 parser (>= 2.5, != 2.5.1.1)
5346 rainbow (>= 2.2.2, < 4.0)
5447 ruby-progressbar (~> 1.7)
55 unicode-display_width (~> 1.0, >= 1.0.1)
56 ruby-progressbar (1.9.0)
57 safe_yaml (1.0.4)
58 thread_safe (0.3.6)
59 tzinfo (1.2.5)
60 thread_safe (~> 0.1)
61 unicode-display_width (1.4.0)
62 webmock (3.4.2)
48 unicode-display_width (>= 1.4.0, < 1.6)
49 ruby-progressbar (1.10.0)
50 safe_yaml (1.0.5)
51 unicode-display_width (1.5.0)
52 webmock (3.5.1)
6353 addressable (>= 2.3.6)
6454 crack (>= 0.3.2)
6555 hashdiff
6858 ruby
6959
7060 DEPENDENCIES
71 activesupport
7261 bump
7362 i18n
7463 maxitest
8069 webmock
8170
8271 BUNDLED WITH
83 1.16.1
72 2.1.4
0
01 # reCAPTCHA
2 [![Gem Version](https://badge.fury.io/rb/recaptcha.svg)](https://badge.fury.io/rb/recaptcha)
13
24 Author: Jason L Perry (http://ambethia.com)<br/>
35 Copyright: Copyright (c) 2007-2013 Jason L Perry<br/>
57 Info: https://github.com/ambethia/recaptcha<br/>
68 Bugs: https://github.com/ambethia/recaptcha/issues<br/>
79
8 This plugin adds helpers for the [reCAPTCHA API](https://www.google.com/recaptcha). In your
9 views you can use the `recaptcha_tags` method to embed the needed javascript,
10 and you can validate in your controllers with `verify_recaptcha` or `verify_recaptcha!`,
11 which throws an error on failiure.
10 This gem provides helper methods for the [reCAPTCHA API](https://www.google.com/recaptcha). In your
11 views you can use the `recaptcha_tags` method to embed the needed javascript, and you can validate
12 in your controllers with `verify_recaptcha` or `verify_recaptcha!`, which raises an error on
13 failure.
14
15
16 # Table of Contents
17 1. [Obtaining a key](#obtaining-a-key)
18 2. [Rails Installation](#rails-installation)
19 3. [Sinatra / Rack / Ruby Installation](#sinatra--rack--ruby-installation)
20 4. [reCAPTCHA V2 API & Usage](#recaptcha-v2-api-and-usage)
21 - [`recaptcha_tags`](#recaptcha_tags)
22 - [`verify_recaptcha`](#verify_recaptcha)
23 - [`invisible_recaptcha_tags`](#invisible_recaptcha_tags)
24 5. [reCAPTCHA V3 API & Usage](#recaptcha-v3-api-and-usage)
25 - [`recaptcha_v3`](#recaptcha_v3)
26 - [`verify_recaptcha` (use with v3)](#verify_recaptcha-use-with-v3)
27 - [`recaptcha_reply`](#recaptcha_reply)
28 6. [I18n Support](#i18n-support)
29 7. [Testing](#testing)
30 8. [Alternative API Key Setup](#alternative-api-key-setup)
31
32 ## Obtaining a key
33
34 Go to the [reCAPTCHA admin console](https://www.google.com/recaptcha/admin) to obtain a reCAPTCHA API key.
35
36 The reCAPTCHA type(s) that you choose for your key will determine which methods to use below.
37
38 | reCAPTCHA type | Methods to use | Description |
39 |----------------------------------------------|----------------|-------------|
40 | v3 | [`recaptcha_v3`](#recaptcha_v3) | Verify requests with a [score](https://developers.google.com/recaptcha/docs/v3#score)
41 | v2 Checkbox<br/>("I'm not a robot" Checkbox) | [`recaptcha_tags`](#recaptcha_tags) | Validate requests with the "I'm not a robot" checkbox |
42 | v2 Invisible<br/>(Invisible reCAPTCHA badge) | [`invisible_recaptcha_tags`](#invisible_recaptcha_tags) | Validate requests in the background |
43
44 Note: You can _only_ use methods that match your key's type. You cannot use v2 methods with a v3
45 key or use `recaptcha_tags` with a v2 Invisible key, for example. Otherwise you will get an
46 error like "Invalid key type" or "This site key is not enabled for the invisible captcha."
47
48 Note: Enter `localhost` or `127.0.0.1` as the domain if using in development with `localhost:3000`.
1249
1350 ## Rails Installation
1451
15 [obtain a reCAPTCHA API key](https://www.google.com/recaptcha/admin). Note: Use localhost or 127.0.0.1 in domain if using localhost:3000.
16
17 ```Ruby
52 ```ruby
1853 gem "recaptcha"
1954 ```
2055
21 Keep keys out of the code base with environment variables.<br/>
22 Set in production and locally use [dotenv](https://github.com/bkeepers/dotenv), make sure to add it above recaptcha.
23
24 Otherwise see [Alternative API key setup](#alternative-api-key-setup).
25
26 ```
27 export RECAPTCHA_SITE_KEY = '6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy'
56 You can keep keys out of the code base with environment variables or with Rails [secrets](https://api.rubyonrails.org/classes/Rails/Application.html#method-i-secrets).<br/>
57
58 In development, you can use the [dotenv](https://github.com/bkeepers/dotenv) gem. (Make sure to add it above `gem 'recaptcha'`.)
59
60 See [Alternative API key setup](#alternative-api-key-setup) for more ways to configure or override
61 keys. See also the
62 [Configuration](https://www.rubydoc.info/github/ambethia/recaptcha/master/Recaptcha/Configuration)
63 documentation.
64
65 ```shell
66 export RECAPTCHA_SITE_KEY = '6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy'
2867 export RECAPTCHA_SECRET_KEY = '6Lc6BAAAAAAAAKN3DRm6VA_xxxxxxxxxxxxxxxxx'
2968 ```
3069
31 Add `recaptcha_tags` to the forms you want to protect.
32
33 ```Erb
70 If you have an Enterprise API key:
71
72 ```shell
73 export RECAPTCHA_ENTERPRISE = 'true'
74 export RECAPTCHA_ENTERPRISE_API_KEY = 'AIzvFyE3TU-g4K_Kozr9F1smEzZSGBVOfLKyupA'
75 export RECAPTCHA_ENTERPRISE_PROJECT_ID = 'my-project'
76 ```
77
78 Add `recaptcha_tags` to the forms you want to protect:
79
80 ```erb
3481 <%= form_for @foo do |f| %>
35 # ... other tags
82 # …
3683 <%= recaptcha_tags %>
37 # ... other tags
84 # …
3885 <% end %>
3986 ```
4087
41 And, add `verify_recaptcha` logic to each form action that you've protected.
42
43 ```Ruby
88 Then, add `verify_recaptcha` logic to each form action that you've protected:
89
90 ```ruby
4491 # app/controllers/users_controller.rb
4592 @user = User.new(params[:user].permit(:name))
4693 if verify_recaptcha(model: @user) && @user.save
4996 render 'new'
5097 end
5198 ```
99 Please note that this setup uses [`reCAPTCHA_v2`](#recaptcha-v2-api-and-usage). For a `recaptcha_v3` use, please refer to [`reCAPTCHA_v3 setup`](#examples).
52100
53101 ## Sinatra / Rack / Ruby installation
54102
56104
57105 - add `gem 'recaptcha'` to `Gemfile`
58106 - set env variables
59 - `include Recaptcha::ClientHelper` where you need `recaptcha_tags`
60 - `include Recaptcha::Verify` where you need `verify_recaptcha`
61
62 ## recaptcha_tags
107 - `include Recaptcha::Adapters::ViewMethods` where you need `recaptcha_tags`
108 - `include Recaptcha::Adapters::ControllerMethods` where you need `verify_recaptcha`
109
110
111 ## reCAPTCHA v2 API and Usage
112
113 ### `recaptcha_tags`
114
115 Use this when your key's reCAPTCHA type is "v2 Checkbox".
116
117 The following options are available:
118
119 | Option | Description |
120 |---------------------|-------------|
121 | `:theme` | Specify the theme to be used per the API. Available options: `dark` and `light`. (default: `light`) |
122 | `:ajax` | Render the dynamic AJAX captcha per the API. (default: `false`) |
123 | `:site_key` | Override site API key from configuration |
124 | `:error` | Override the error code returned from the reCAPTCHA API (default: `nil`) |
125 | `:size` | Specify a size (default: `nil`) |
126 | `:nonce` | Optional. Sets nonce attribute for script. Can be generated via `SecureRandom.base64(32)`. (default: `nil`) |
127 | `:id` | Specify an html id attribute (default: `nil`) |
128 | `:callback` | Optional. Name of success callback function, executed when the user submits a successful response |
129 | `:expired_callback` | Optional. Name of expiration callback function, executed when the reCAPTCHA response expires and the user needs to re-verify. |
130 | `:error_callback` | Optional. Name of error callback function, executed when reCAPTCHA encounters an error (e.g. network connectivity) |
131 | `:noscript` | Include `<noscript>` content (default: `true`)|
132
133 [JavaScript resource (api.js) parameters](https://developers.google.com/recaptcha/docs/invisible#js_param):
134
135 | Option | Description |
136 |---------------------|-------------|
137 | `:onload` | Optional. The name of your callback function to be executed once all the dependencies have loaded. (See [explicit rendering](https://developers.google.com/recaptcha/docs/display#explicit_render)) |
138 | `:render` | Optional. Whether to render the widget explicitly. Defaults to `onload`, which will render the widget in the first g-recaptcha tag it finds. (See [explicit rendering](https://developers.google.com/recaptcha/docs/display#explicit_render)) |
139 | `:hl` | Optional. Forces the widget to render in a specific language. Auto-detects the user's language if unspecified. (See [language codes](https://developers.google.com/recaptcha/docs/language)) |
140 | `:script` | Alias for `:external_script`. If you do not need to add a script tag by helper you can set the option to `false`. It's necessary when you add a script tag manualy (default: `true`). |
141 | `:external_script` | Set to `false` to avoid including a script tag for the external `api.js` resource. Useful when including multiple `recaptcha_tags` on the same page. |
142 | `:script_async` | Set to `false` to load the external `api.js` resource synchronously. (default: `true`) |
143 | `:script_defer` | Set to `true` to defer loading of external `api.js` until HTML documen has been parsed. (default: `true`) |
144
145 Any unrecognized options will be added as attributes on the generated tag.
146
147 You can also override the html attributes for the sizes of the generated `textarea` and `iframe`
148 elements, if CSS isn't your thing. Inspect the [source of `recaptcha_tags`](https://github.com/ambethia/recaptcha/blob/master/lib/recaptcha/helpers.rb)
149 to see these options.
150
151 Note that you cannot submit/verify the same response token more than once or you will get a
152 `timeout-or-duplicate` error code. If you need reset the captcha and generate a new response token,
153 then you need to call `grecaptcha.reset()`.
154
155 ### `verify_recaptcha`
156
157 This method returns `true` or `false` after processing the response token from the reCAPTCHA widget.
158 This is usually called from your controller, as seen [above](#rails-installation).
159
160 Passing in the ActiveRecord object via `model: object` is optional. If you pass a `model`—and the
161 captcha fails to verify—an error will be added to the object for you to use (available as
162 `object.errors`).
163
164 Why isn't this a model validation? Because that violates MVC. You can use it like this, or how ever
165 you like.
63166
64167 Some of the options available:
65168
66 | Option | Description |
67 |-------------------|-------------|
68 | :noscript | Include <noscript> content (default `true`)|
69 | :theme | Specify the theme to be used per the API. Available options: `dark` and `light`. (default `light`)|
70 | :ajax | Render the dynamic AJAX captcha per the API. (default `false`)|
71 | :site_key | Override site API key |
72 | :error | Override the error code returned from the reCAPTCHA API (default `nil`)|
73 | :size | Specify a size (default `nil`)|
74 | :hl | Optional. Forces the widget to render in a specific language. Auto-detects the user's language if unspecified. (See [language codes](https://developers.google.com/recaptcha/docs/language)) |
75 | :nonce | Optional. Sets nonce attribute for script. Can be generated via `SecureRandom.base64(32)`. (default `nil`)|
76 | :id | Specify an html id attribute (default `nil`)|
77 | :script | If you do not need to add a script tag by helper you can set the option to false. It's necessary when you add a script tag manualy (default `true`)|
78 | :callback | Optional. Name of success callback function, executed when the user submits a successful response |
79 | :expired_callback | Optional. Name of expiration callback function, executed when the reCAPTCHA response expires and the user needs to re-verify. |
80 | :error_callback | Optional. Name of error callback function, executed when reCAPTCHA encounters an error (e.g. network connectivity) |
81
82 You can also override the html attributes for the sizes of the generated `textarea` and `iframe`
83 elements, if CSS isn't your thing. Inspect the source of `recaptcha_tags` to see these options.
84
85 ## verify_recaptcha
86
87 This method returns `true` or `false` after processing the parameters from the reCAPTCHA widget. Why
88 isn't this a model validation? Because that violates MVC. You can use it like this, or how ever you
89 like. Passing in the ActiveRecord object is optional, if you do--and the captcha fails to verify--an
90 error will be added to the object for you to use.
91
92 Some of the options available:
93
94 | Option | Description |
95 |--------------|-------------|
96 | :model | Model to set errors.
97 | :attribute | Model attribute to receive errors. (default :base)
98 | :message | Custom error message.
99 | :secret_key | Override secret API key.
100 | :timeout | The number of seconds to wait for reCAPTCHA servers before give up. (default `3`)
101 | :response | Custom response parameter. (default: params['g-recaptcha-response'])
102 | :hostname | Expected hostname or a callable that validates the hostname, see [domain validation](https://developers.google.com/recaptcha/docs/domain_validation) and [hostname](https://developers.google.com/recaptcha/docs/verify#api-response) docs. (default: `nil`, but can be changed by setting `config.hostname`)
103 | :env | Current environment. The request to verify will be skipped if the environment is specified in configuration under `skip_verify_env`
104
105 ## invisible_recaptcha_tags
106
107 Make sure to read [Invisible reCAPTCHA](https://developers.google.com/recaptcha/docs/invisible).
169 | Option | Description |
170 |---------------------------|-------------|
171 | `:model` | Model to set errors.
172 | `:attribute` | Model attribute to receive errors. (default: `:base`)
173 | `:message` | Custom error message.
174 | `:secret_key` | Override the secret API key from the configuration.
175 | `:enterprise_api_key` | Override the Enterprise API key from the configuration.
176 | `:enterprise_project_id ` | Override the Enterprise project ID from the configuration.
177 | `:timeout` | The number of seconds to wait for reCAPTCHA servers before give up. (default: `3`)
178 | `:response` | Custom response parameter. (default: `params['g-recaptcha-response-data']`)
179 | `:hostname` | Expected hostname or a callable that validates the hostname, see [domain validation](https://developers.google.com/recaptcha/docs/domain_validation) and [hostname](https://developers.google.com/recaptcha/docs/verify#api-response) docs. (default: `nil`, but can be changed by setting `config.hostname`)
180 | `:env` | Current environment. The request to verify will be skipped if the environment is specified in configuration under `skip_verify_env`
181
182
183 ### `invisible_recaptcha_tags`
184
185 Use this when your key's reCAPTCHA type is "v2 Invisible".
186
187 For more information, refer to: [Invisible reCAPTCHA](https://developers.google.com/recaptcha/docs/invisible).
188
189 This is similar to `recaptcha_tags`, with the following additional options that are only available
190 on `invisible_recaptcha_tags`:
191
192 | Option | Description |
193 |---------------------|-------------|
194 | `:ui` | The type of UI to render for this "invisible" widget. (default: `:button`)<br/>`:button`: Renders a `<button type="submit">` tag with `options[:text]` as the button text.<br/>`:invisible`: Renders a `<div>` tag.<br/>`:input`: Renders a `<input type="submit">` tag with `options[:text]` as the button text. |
195 | `:text` | The text to show for the button. (default: `"Submit"`)
196 | `:inline_script` | If you do not need this helper to add an inline script tag, you can set the option to `false` (default: `true`).
197
198 It also accepts most of the options that `recaptcha_tags` accepts, including the following:
199
200 | Option | Description |
201 |---------------------|-------------|
202 | `:site_key` | Override site API key from configuration |
203 | `:nonce` | Optional. Sets nonce attribute for script tag. Can be generated via `SecureRandom.base64(32)`. (default: `nil`) |
204 | `:id` | Specify an html id attribute (default: `nil`) |
205 | `:script` | Same as setting both `:inline_script` and `:external_script`. If you only need one or the other, use `:inline_script` and `:external_script` instead. |
206 | `:callback` | Optional. Name of success callback function, executed when the user submits a successful response |
207 | `:expired_callback` | Optional. Name of expiration callback function, executed when the reCAPTCHA response expires and the user needs to re-verify. |
208 | `:error_callback` | Optional. Name of error callback function, executed when reCAPTCHA encounters an error (e.g. network connectivity) |
209
210 [JavaScript resource (api.js) parameters](https://developers.google.com/recaptcha/docs/invisible#js_param):
211
212 | Option | Description |
213 |---------------------|-------------|
214 | `:onload` | Optional. The name of your callback function to be executed once all the dependencies have loaded. (See [explicit rendering](https://developers.google.com/recaptcha/docs/display#explicit_render)) |
215 | `:render` | Optional. Whether to render the widget explicitly. Defaults to `onload`, which will render the widget in the first g-recaptcha tag it finds. (See [explicit rendering](https://developers.google.com/recaptcha/docs/display#explicit_render)) |
216 | `:hl` | Optional. Forces the widget to render in a specific language. Auto-detects the user's language if unspecified. (See [language codes](https://developers.google.com/recaptcha/docs/language)) |
217 | `:external_script` | Set to `false` to avoid including a script tag for the external `api.js` resource. Useful when including multiple `recaptcha_tags` on the same page. |
218 | `:script_async` | Set to `false` to load the external `api.js` resource synchronously. (default: `true`) |
219 | `:script_defer` | Set to `false` to defer loading of external `api.js` until HTML documen has been parsed. (default: `true`) |
108220
109221 ### With a single form on a page
110222
111223 1. The `invisible_recaptcha_tags` generates a submit button for you.
112224
113 ```Erb
225 ```erb
114226 <%= form_for @foo do |f| %>
115227 # ... other tags
116228 <%= invisible_recaptcha_tags text: 'Submit form' %>
124236 1. You will need a custom callback function, which is called after verification with Google's reCAPTCHA service. This callback function must submit the form. Optionally, `invisible_recaptcha_tags` currently implements a JS function called `invisibleRecaptchaSubmit` that is called when no `callback` is passed. Should you wish to override `invisibleRecaptchaSubmit`, you will need to use `invisible_recaptcha_tags script: false`, see lib/recaptcha/client_helper.rb for details.
125237 2. The `invisible_recaptcha_tags` generates a submit button for you.
126238
127 ```Erb
239 ```erb
128240 <%= form_for @foo, html: {id: 'invisible-recaptcha-form'} do |f| %>
129241 # ... other tags
130242 <%= invisible_recaptcha_tags callback: 'submitInvisibleRecaptchaForm', text: 'Submit form' %>
131243 <% end %>
132244 ```
133245
134 ```Javascript
246 ```javascript
135247 // app/assets/javascripts/application.js
136248 var submitInvisibleRecaptchaForm = function () {
137249 document.getElementById("invisible-recaptcha-form").submit();
144256
145257 1. Specify `ui` option
146258
147 ```Erb
259 ```erb
148260 <%= form_for @foo, html: {id: 'invisible-recaptcha-form'} do |f| %>
149261 # ... other tags
150262 <button type="button" id="submit-btn">
154266 <% end %>
155267 ```
156268
157 ```Javascript
269 ```javascript
158270 // app/assets/javascripts/application.js
159271 document.getElementById('submit-btn').addEventListener('click', function (e) {
160272 // do some validation
169281 };
170282 ```
171283
284
285 ## reCAPTCHA v3 API and Usage
286
287 The main differences from v2 are:
288 1. you must specify an [action](https://developers.google.com/recaptcha/docs/v3#actions) in both frontend and backend
289 1. you can choose the minimum score required for you to consider the verification a success
290 (consider the user a human and not a robot)
291 1. reCAPTCHA v3 is invisible (except for the reCAPTCHA badge) and will never interrupt your users;
292 you have to choose which scores are considered an acceptable risk, and choose what to do (require
293 two-factor authentication, show a v3 challenge, etc.) if the score falls below the threshold you
294 choose
295
296 For more information, refer to the [v3 documentation](https://developers.google.com/recaptcha/docs/v3).
297
298 ### Examples
299
300 With v3, you can let all users log in without any intervention at all if their score is above some
301 threshold, and only show a v2 checkbox recaptcha challenge (fall back to v2) if it is below the
302 threshold:
303
304 ```erb
305 …
306 <% if @show_checkbox_recaptcha %>
307 <%= recaptcha_tags %>
308 <% else %>
309 <%= recaptcha_v3(action: 'login', site_key: ENV['RECAPTCHA_SITE_KEY_V3']) %>
310 <% end %>
311 …
312 ```
313
314 ```ruby
315 # app/controllers/sessions_controller.rb
316 def create
317 success = verify_recaptcha(action: 'login', minimum_score: 0.5, secret_key: ENV['RECAPTCHA_SECRET_KEY_V3'])
318 checkbox_success = verify_recaptcha unless success
319 if success || checkbox_success
320 # Perform action
321 else
322 if !success
323 @show_checkbox_recaptcha = true
324 end
325 render 'new'
326 end
327 end
328 ```
329
330 (You can also find this [example](demo/rails/app/controllers/v3_captchas_controller.rb) in the demo app.)
331
332 Another example:
333
334 ```erb
335 <%= form_for @user do |f| %>
336 …
337 <%= recaptcha_v3(action: 'registration') %>
338 …
339 <% end %>
340 ```
341
342 ```ruby
343 # app/controllers/users_controller.rb
344 def create
345 @user = User.new(params[:user].permit(:name))
346 recaptcha_valid = verify_recaptcha(model: @user, action: 'registration')
347 if recaptcha_valid
348 if @user.save
349 redirect_to @user
350 else
351 render 'new'
352 end
353 else
354 # Score is below threshold, so user may be a bot. Show a challenge, require multi-factor
355 # authentication, or do something else.
356 render 'new'
357 end
358 end
359 ```
360
361
362 ### `recaptcha_v3`
363
364 Adds an inline script tag that calls `grecaptcha.execute` for the given `site_key` and `action` and
365 calls the `callback` with the resulting response token. You need to verify this token with
366 [`verify_recaptcha`](#verify_recaptcha-use-with-v3) in your controller in order to get the
367 [score](https://developers.google.com/recaptcha/docs/v3#score).
368
369 By default, this inserts a hidden `<input type="hidden" class="g-recaptcha-response">` tag. The
370 value of this input will automatically be set to the response token (by the default callback
371 function). This lets you include `recaptcha_v3` within a `<form>` tag and have it automatically
372 submit the token as part of the form submission.
373
374 Note: reCAPTCHA actually already adds its own hidden tag, like `<textarea
375 id="g-recaptcha-response-data-100000" name="g-recaptcha-response-data" class="g-recaptcha-response">`,
376 immediately ater the reCAPTCHA badge in the bottom right of the page — but since it is not inside of
377 any `<form>` element, and since it already passes the token to the callback, this hidden `textarea`
378 isn't helpful to us.
379
380 If you need to submit the response token to the server in a different way than via a regular form
381 submit, such as via [Ajax](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) or [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API),
382 then you can either:
383 1. just extract the token out of the hidden `<input>` or `<textarea>` (both of which will have a
384 predictable name/id), like `document.getElementById('g-recaptcha-response-data-my-action').value`, or
385 2. write and specify a custom `callback` function. You may also want to pass `element: false` if you
386 don't have a use for the hidden input element.
387
388 Note that you cannot submit/verify the same response token more than once or you
389 will get a `timeout-or-duplicate` error code. If you need reset the captcha and
390 generate a new response token, then you need to call `grecaptcha.execute(…)` or
391 `grecaptcha.enterprise.execute(…)` again. This helper provides a JavaScript
392 method (for each action) named `executeRecaptchaFor{action}` to make this
393 easier. That is the same method that is invoked immediately. It simply calls
394 `grecaptcha.execute` or `grecaptcha.enterprise.execute` again and then calls the
395 `callback` function with the response token.
396
397 You will also get a `timeout-or-duplicate` error if too much time has passed between getting the
398 response token and verifying it. This can easily happen with large forms that take the user a couple
399 minutes to complete. Unlike v2, where you can use the `expired-callback` to be notified when the
400 response expires, v3 appears to provide no such callback. See also
401 [1](https://github.com/google/recaptcha/issues/281) and
402 [2](https://stackoverflow.com/questions/54437745/recaptcha-v3-how-to-deal-with-expired-token-after-idle).
403
404 To deal with this, it is recommended to call the "execute" in your form's submit handler (or
405 immediately before sending to the server to verify if not using a form) rather than using the
406 response token that gets generated when the page first loads. The `executeRecaptchaFor{action}`
407 function mentioned above can be used if you want it to invoke a callback, or the
408 `executeRecaptchaFor{action}Async` variant if you want a `Promise` that you can `await`. See
409 [demo/rails/app/views/v3_captchas/index.html.erb](demo/rails/app/views/v3_captchas/index.html.erb)
410 for an example of this.
411
412 This helper is similar to the [`recaptcha_tags`](#recaptcha_tags)/[`invisible_recaptcha_tags`](#invisible_recaptcha_tags) helpers
413 but only accepts the following options:
414
415 | Option | Description |
416 |---------------------|-------------|
417 | `:site_key` | Override site API key |
418 | `:action` | The name of the [reCAPTCHA action](https://developers.google.com/recaptcha/docs/v3#actions). Actions may only contain alphanumeric characters and slashes, and must not be user-specific. |
419 | `:nonce` | Optional. Sets nonce attribute for script. Can be generated via `SecureRandom.base64(32)`. (default: `nil`) |
420 | `:callback` | Name of callback function to call with the token. When `element` is `:input`, this defaults to a function named `setInputWithRecaptchaResponseTokenFor#{sanitize_action(action)}` that sets the value of the hidden input to the token. |
421 | `:id` | Specify a unique `id` attribute for the `<input>` element if using `element: :input`. (default: `"g-recaptcha-response-data-"` + `action`) |
422 | `:name` | Specify a unique `name` attribute for the `<input>` element if using `element: :input`. (default: `g-recaptcha-response-data[action]`) |
423 | `:script` | Same as setting both `:inline_script` and `:external_script`. (default: `true`). |
424 | `:inline_script` | If `true`, adds an inline script tag that calls `grecaptcha.execute` for the given `site_key` and `action` and calls the `callback` with the resulting response token. Pass `false` if you want to handle calling `grecaptcha.execute` yourself. (default: `true`) |
425 | `:element` | The element to render, if any (default: `:input`)<br/>`:input`: Renders a hidden `<input type="hidden">` tag. The value of this will be set to the response token by the default `setInputWithRecaptchaResponseTokenFor{action}` callback.<br/>`false`: Doesn't render any tag. You'll have to add a custom callback that does something with the token. |
426 | `:turbolinks` | If `true`, calls the js function which executes reCAPTCHA after all the dependencies have been loaded. This cannot be used with the js param `:onload`. This makes reCAPTCHAv3 usable with turbolinks. |
427
428 [JavaScript resource (api.js) parameters](https://developers.google.com/recaptcha/docs/invisible#js_param):
429
430 | Option | Description |
431 |---------------------|-------------|
432 | `:onload` | Optional. The name of your callback function to be executed once all the dependencies have loaded. (See [explicit rendering](https://developers.google.com/recaptcha/docs/display#explicit_render))|
433 | `:external_script` | Set to `false` to avoid including a script tag for the external `api.js` resource. Useful when including multiple `recaptcha_tags` on the same page.
434 | `:script_async` | Set to `true` to load the external `api.js` resource asynchronously. (default: `false`) |
435 | `:script_defer` | Set to `true` to defer loading of external `api.js` until HTML documen has been parsed. (default: `false`) |
436
437 If using `element: :input`, any unrecognized options will be added as attributes on the generated
438 `<input>` element.
439
440 ### `verify_recaptcha` (use with v3)
441
442 This works the same as for v2, except that you may pass an `action` and `minimum_score` if you wish
443 to validate that the action matches or that the score is above the given threshold, respectively.
444
445 ```ruby
446 result = verify_recaptcha(action: 'action/name')
447 ```
448
449 | Option | Description |
450 |------------------|-------------|
451 | `:action` | The name of the [reCAPTCHA action](https://developers.google.com/recaptcha/docs/v3#actions) that we are verifying. Set to `false` or `nil` to skip verifying that the action matches.
452 | `:minimum_score` | Provide a threshold to meet or exceed. Threshold should be a float between 0 and 1 which will be tested as `score >= minimum_score`. (Default: `nil`) |
453
454 ### Multiple actions on the same page
455
456 According to https://developers.google.com/recaptcha/docs/v3#placement,
457
458 > Note: You can execute reCAPTCHA as many times as you'd like with different actions on the same page.
459
460 You will need to verify each action individually with a separate call to `verify_recaptcha`.
461
462 ```ruby
463 result_a = verify_recaptcha(action: 'a')
464 result_b = verify_recaptcha(action: 'b')
465 ```
466
467 Because the response tokens for multiple actions may be submitted together in the same request, they
468 are passed as a hash under `params['g-recaptcha-response-data']` with the action as the key.
469
470 It is recommended to pass `external_script: false` on all but one of the calls to
471 `recaptcha` since you only need to include the script tag once for a given `site_key`.
472
473 ## `recaptcha_reply`
474
475 After `verify_recaptcha` has been called, you can call `recaptcha_reply` to get the raw reply from recaptcha. This can allow you to get the exact score returned by recaptcha should you need it.
476
477 ```ruby
478 if verify_recaptcha(action: 'login')
479 redirect_to @user
480 else
481 score = recaptcha_reply['score']
482 Rails.logger.warn("User #{@user.id} was denied login because of a recaptcha score of #{score}")
483 render 'new'
484 end
485 ```
486
487 `recaptcha_reply` will return `nil` if the the reply was not yet fetched.
488
172489 ## I18n support
173 reCAPTCHA passes two types of error explanation to a linked model. It will use the I18n gem
174 to translate the default error message if I18n is available. To customize the messages to your locale,
175 add these keys to your I18n backend:
176
177 `recaptcha.errors.verification_failed` error message displayed if the captcha words didn't match
178 `recaptcha.errors.recaptcha_unreachable` displayed if a timeout error occured while attempting to verify the captcha
179
180 Also you can translate API response errors to human friendly by adding translations to the locale (`config/locales/en.yml`):
181
182 ```Yaml
490
491 reCAPTCHA supports the I18n gem (it comes with English translations)
492 To override or add new languages, add to `config/locales/*.yml`
493
494 ```yaml
495 # config/locales/en.yml
183496 en:
184497 recaptcha:
185498 errors:
186 verification_failed: 'Fail'
499 verification_failed: 'reCAPTCHA was incorrect, please try again.'
500 recaptcha_unreachable: 'reCAPTCHA verification server error, please try again.'
187501 ```
188502
189503 ## Testing
190504
191505 By default, reCAPTCHA is skipped in "test" and "cucumber" env. To enable it during test:
192506
193 ```Ruby
507 ```ruby
194508 Recaptcha.configuration.skip_verify_env.delete("test")
195509 ```
196510
198512
199513 ### Recaptcha.configure
200514
201 ```Ruby
515 ```ruby
202516 # config/initializers/recaptcha.rb
203517 Recaptcha.configure do |config|
204518 config.site_key = '6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy'
205519 config.secret_key = '6Lc6BAAAAAAAAKN3DRm6VA_xxxxxxxxxxxxxxxxx'
520
206521 # Uncomment the following line if you are using a proxy server:
207522 # config.proxy = 'http://myproxy.com.au:8080'
523
524 # Uncomment the following lines if you are using the Enterprise API:
525 # config.enterprise = true
526 # config.enterprise_api_key = 'AIzvFyE3TU-g4K_Kozr9F1smEzZSGBVOfLKyupA'
527 # config.enterprise_project_id = 'my-project'
208528 end
209529 ```
210530
211531 ### Recaptcha.with_configuration
212532
213 For temporary overwrites (not thread safe).
214
215 ```Ruby
533 For temporary overwrites (not thread-safe).
534
535 ```ruby
216536 Recaptcha.with_configuration(site_key: '12345') do
217537 # Do stuff with the overwritten site_key.
218538 end
222542
223543 Pass in keys as options at runtime, for code base with multiple reCAPTCHA setups:
224544
225 ```Ruby
545 ```ruby
226546 recaptcha_tags site_key: '6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy'
227547
228548 # and
0 # reCAPTCHA keys for testing purposes
1 # Make your own at https://www.google.com/recaptcha
0 # v2 reCAPTCHA keys for testing purposes
1 # (https://developers.google.com/recaptcha/docs/faq#id-like-to-run-automated-tests-with-recaptcha-what-should-i-do)
2 # You can make your own at https://www.google.com/recaptcha
23 RECAPTCHA_SITE_KEY=6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
34 RECAPTCHA_SECRET_KEY=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe
00 source 'https://rubygems.org'
11
22 gem 'dotenv-rails', groups: [:development, :test]
3 gem 'rails', '4.2.5'
3 gem 'rails', '~> 5.2'
44 gem 'sqlite3'
55 gem 'recaptcha', require: 'recaptcha/rails', path: '../..'
6
7 group :development do
8 gem 'listen'
9 gem 'byebug'
10 end
+0
-115
demo/rails/Gemfile.lock less more
0 PATH
1 remote: ../..
2 specs:
3 recaptcha (3.3.0)
4 json
5
6 GEM
7 remote: https://rubygems.org/
8 specs:
9 actionmailer (4.2.5)
10 actionpack (= 4.2.5)
11 actionview (= 4.2.5)
12 activejob (= 4.2.5)
13 mail (~> 2.5, >= 2.5.4)
14 rails-dom-testing (~> 1.0, >= 1.0.5)
15 actionpack (4.2.5)
16 actionview (= 4.2.5)
17 activesupport (= 4.2.5)
18 rack (~> 1.6)
19 rack-test (~> 0.6.2)
20 rails-dom-testing (~> 1.0, >= 1.0.5)
21 rails-html-sanitizer (~> 1.0, >= 1.0.2)
22 actionview (4.2.5)
23 activesupport (= 4.2.5)
24 builder (~> 3.1)
25 erubis (~> 2.7.0)
26 rails-dom-testing (~> 1.0, >= 1.0.5)
27 rails-html-sanitizer (~> 1.0, >= 1.0.2)
28 activejob (4.2.5)
29 activesupport (= 4.2.5)
30 globalid (>= 0.3.0)
31 activemodel (4.2.5)
32 activesupport (= 4.2.5)
33 builder (~> 3.1)
34 activerecord (4.2.5)
35 activemodel (= 4.2.5)
36 activesupport (= 4.2.5)
37 arel (~> 6.0)
38 activesupport (4.2.5)
39 i18n (~> 0.7)
40 json (~> 1.7, >= 1.7.7)
41 minitest (~> 5.1)
42 thread_safe (~> 0.3, >= 0.3.4)
43 tzinfo (~> 1.1)
44 arel (6.0.3)
45 builder (3.2.2)
46 dotenv (2.0.2)
47 dotenv-rails (2.0.2)
48 dotenv (= 2.0.2)
49 railties (~> 4.0)
50 erubis (2.7.0)
51 globalid (0.3.6)
52 activesupport (>= 4.1.0)
53 i18n (0.7.0)
54 json (1.8.3)
55 loofah (2.0.3)
56 nokogiri (>= 1.5.9)
57 mail (2.6.3)
58 mime-types (>= 1.16, < 3)
59 mime-types (2.99)
60 mini_portile (0.6.2)
61 minitest (5.8.3)
62 nokogiri (1.6.6.4)
63 mini_portile (~> 0.6.0)
64 rack (1.6.4)
65 rack-test (0.6.3)
66 rack (>= 1.0)
67 rails (4.2.5)
68 actionmailer (= 4.2.5)
69 actionpack (= 4.2.5)
70 actionview (= 4.2.5)
71 activejob (= 4.2.5)
72 activemodel (= 4.2.5)
73 activerecord (= 4.2.5)
74 activesupport (= 4.2.5)
75 bundler (>= 1.3.0, < 2.0)
76 railties (= 4.2.5)
77 sprockets-rails
78 rails-deprecated_sanitizer (1.0.3)
79 activesupport (>= 4.2.0.alpha)
80 rails-dom-testing (1.0.7)
81 activesupport (>= 4.2.0.beta, < 5.0)
82 nokogiri (~> 1.6.0)
83 rails-deprecated_sanitizer (>= 1.0.1)
84 rails-html-sanitizer (1.0.2)
85 loofah (~> 2.0)
86 railties (4.2.5)
87 actionpack (= 4.2.5)
88 activesupport (= 4.2.5)
89 rake (>= 0.8.7)
90 thor (>= 0.18.1, < 2.0)
91 rake (10.4.2)
92 sprockets (3.4.0)
93 rack (> 1, < 3)
94 sprockets-rails (2.3.3)
95 actionpack (>= 3.0)
96 activesupport (>= 3.0)
97 sprockets (>= 2.8, < 4.0)
98 sqlite3 (1.3.11)
99 thor (0.19.1)
100 thread_safe (0.3.5)
101 tzinfo (1.2.2)
102 thread_safe (~> 0.1)
103
104 PLATFORMS
105 ruby
106
107 DEPENDENCIES
108 dotenv-rails
109 rails (= 4.2.5)
110 recaptcha!
111 sqlite3
112
113 BUNDLED WITH
114 1.12.5
0 To run the v2 examples, start the server with `rails s`. Then go to <http://localhost:3000/captchas>.
1
2 To run the v3 examples, you will need a v3 key, which you can get from
3 https://www.google.com/recaptcha/admin. Unlike v2, where they provide a standard [key you can use for
4 testing](https://developers.google.com/recaptcha/docs/faq#id-like-to-run-automated-tests-with-recaptcha-what-should-i-do)
5 (and which is in the [.env](.env) file, no such standard testing key exists for v3, so you need to
6 obtain your own.
7
8 Then set these environment variables:
9
10 export RECAPTCHA_SITE_KEY=your_v3_key
11 export RECAPTCHA_SECRET_KEY=your_v3_key
12
13 and start the server with `rails s`. Then go to <http://localhost:3000/v3_captchas>.
14
15 To run the example of v3 with v2 fallback, you can set:
16
17 ```
18 unset RECAPTCHA_SITE_KEY
19 unset RECAPTCHA_SECRET_KEY
20 export RECAPTCHA_SITE_KEY_V3=your_v3_key
21 export RECAPTCHA_SECRET_KEY_V3=your_v3_key
22 ```
23
24 and start the server again with `rails s`. Then go to <http://localhost:3000/v3_captchas?with_v2_fallback=1>.
+0
-12
demo/rails/app/controllers/captcha_controller.rb less more
0 class CaptchaController < ApplicationController
1 def index
2 end
3
4 def create
5 if verify_recaptcha
6 render text: 'YES'
7 else
8 render text: 'NO'
9 end
10 end
11 end
0 class CaptchasController < ApplicationController
1 def index
2 end
3
4 def create
5 if verify_recaptcha
6 render plain: 'YES'
7 else
8 render plain: 'NO'
9 end
10 end
11 end
0 class V3CaptchasController < ApplicationController
1 def index
2 end
3
4 def create
5 if verify_recaptcha(action: 'demo', minimum_score: 0.5)
6 render plain: 'YES'
7 else
8 render plain: 'NO'
9 end
10 end
11
12 def create_multi
13 if verify_recaptcha(action: 'demo_a', minimum_score: 0.5) &&
14 verify_recaptcha(action: 'demo_b', minimum_score: 0.5)
15 render plain: 'YES'
16 else
17 render plain: 'NO'
18 end
19 end
20
21 def create_with_v2_fallback
22 success = verify_recaptcha(action: 'login', minimum_score: 0.2, secret_key: ENV['RECAPTCHA_SECRET_KEY_V3'], **response_option)
23 checkbox_success = verify_recaptcha unless success
24 if success || checkbox_success
25 render plain: 'Success'
26 else
27 if !success
28 @show_checkbox_recaptcha = true
29 end
30 render 'index'
31 end
32 end
33
34 private
35
36 # This is only used to be able to simulate a failure. You wouldn't need this in a production app.
37 def response_option
38 if params[:commit] =~ /fail/i
39 # Simulate a failure
40 # Note that this doesn't work for v2 with the default testing key because it always returns
41 # success for that key.
42 response_option = {response: 'bogus'}
43 else
44 # Use the response token that was submitted
45 response_option = {}
46 end
47 end
48
49 end
50
+0
-51
demo/rails/app/views/captcha/index.html.erb less more
0 <% if params[:multi] %>
1 <script type="text/javascript">
2 var verifyCallback = function(response) {
3 alert(response);
4 };
5 var widgetId1;
6 var widgetId2;
7 var onloadCallback = function() {
8 // Renders the HTML element with id 'example1' as a reCAPTCHA widget.
9 // The id of the reCAPTCHA widget is assigned to 'widgetId1'.
10 widgetId1 = grecaptcha.render('example1', {
11 'sitekey' : "<%= Recaptcha.configuration.site_key %>",
12 'theme' : 'light'
13 });
14 widgetId2 = grecaptcha.render(document.getElementById('example2'), {
15 'sitekey' : "<%= Recaptcha.configuration.site_key %>"
16 });
17 grecaptcha.render('example3', {
18 'sitekey' : "<%= Recaptcha.configuration.site_key %>",
19 'callback' : verifyCallback,
20 'theme' : 'dark'
21 });
22 };
23 </script>
24
25 <%= form_tag "/captchas" do |f| %>
26 <div id="example1"></div>
27 <%= submit_tag %>
28 <% end %>
29 <%= form_tag "/captchas" do %>
30 <div id="example2"></div>
31 <%= submit_tag %>
32 <% end %>
33 <%= form_tag "/captchas" do %>
34 <div id="example3"></div>
35 <%= submit_tag %>
36 <% end %>
37 <script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script>
38 <% elsif params[:invisible] %>
39 <%= form_tag "/captchas", id: "invisible-recaptcha-form" do %>
40 <%= invisible_recaptcha_tags text: 'Save changes' %>
41 <% end %>
42 <% else %>
43 <%= form_tag "/captchas" do %>
44 <%= recaptcha_tags %>
45 <%= submit_tag %>
46 <% end %>
47 <% end %>
48 <%= link_to 'Single ?', '?' if params[:multi] or params[:invisible] %>
49 <%= link_to 'Multi ?', '?multi=1' unless params[:multi] %>
50 <%= link_to 'Invisible ?', '?invisible=1' unless params[:invisible] %>
0 <% if params[:multi] %>
1 <script type="text/javascript">
2 var verifyCallback = function(response) {
3 alert(response);
4 };
5 var widgetId1;
6 var widgetId2;
7 var onloadCallback = function() {
8 // Renders the HTML element with id 'example1' as a reCAPTCHA widget.
9 // The id of the reCAPTCHA widget is assigned to 'widgetId1'.
10 widgetId1 = grecaptcha.render('example1', {
11 'sitekey' : "<%= Recaptcha.configuration.site_key %>",
12 'theme' : 'light'
13 });
14 widgetId2 = grecaptcha.render(document.getElementById('example2'), {
15 'sitekey' : "<%= Recaptcha.configuration.site_key %>"
16 });
17 grecaptcha.render('example3', {
18 'sitekey' : "<%= Recaptcha.configuration.site_key %>",
19 'callback' : verifyCallback,
20 'theme' : 'dark'
21 });
22 };
23 </script>
24
25 <%= form_tag "/captchas" do |f| %>
26 <div id="example1"></div>
27 <%= submit_tag %>
28 <% end %>
29 <%= form_tag "/captchas" do %>
30 <div id="example2"></div>
31 <%= submit_tag %>
32 <% end %>
33 <%= form_tag "/captchas" do %>
34 <div id="example3"></div>
35 <%= submit_tag %>
36 <% end %>
37 <script src="<%= Recaptcha.configuration.api_server_url %>?onload=onloadCallback&render=explicit" async defer></script>
38 <% elsif params[:invisible] %>
39 <%= form_tag "/captchas", id: "invisible-recaptcha-form" do %>
40 <%= invisible_recaptcha_tags text: 'Save changes' %>
41 <% end %>
42 <% else %>
43 <%= form_tag "/captchas" do %>
44 <%= recaptcha_tags %>
45 <%= submit_tag %>
46 <% end %>
47 <% end %>
48 <%= link_to 'Single ?', '?' if params[:multi] or params[:invisible] %>
49 <%= link_to 'Multi ?', '?multi=1' unless params[:multi] %>
50 <%= link_to 'Invisible ?', '?invisible=1' unless params[:invisible] %>
0 <% if params[:multi] %>
1 <%= form_tag "/#{controller_name}/create_multi" do |f| %>
2 <%= recaptcha_v3 action: 'demo_a' %>
3 <%= recaptcha_v3 action: 'demo_b', external_script: false %>
4 <%= submit_tag %>
5 <% end %>
6
7 <% elsif action_name == 'create_with_v2_fallback' || params[:with_v2_fallback] %>
8 <% if @recaptcha_verify_returned %>
9 <%= @recaptcha_verify_result.pretty_inspect %>
10 <% end %>
11 <%= form_tag "/#{controller_name}/create_with_v2_fallback" do %>
12 <% if @show_checkbox_recaptcha %>
13 <p>Automatic v3 verification failed. Falling back to a v3 challange...</p>
14 <%= recaptcha_tags %>
15 <%= submit_tag 'Submit' %>
16 <% else %>
17 <%= recaptcha_v3 action: 'login', site_key: ENV['RECAPTCHA_SITE_KEY_V3'] %>
18 <%= submit_tag 'Succes' %>
19 <%= submit_tag 'Failure' %>
20 <% end %>
21 <% end %>
22
23 <% elsif params[:custom_callback] %>
24 <script type="text/javascript">
25 var myCallback = function(action, response) {
26 alert(`Response for ${action} action: ${response}`);
27 };
28 </script>
29 <%= recaptcha_v3 action: 'demo', callback: 'myCallback' %>
30
31 <% elsif params[:execute_on_submit] %>
32 <%= form_tag "/#{controller_name}" do %>
33 <%= recaptcha_v3 inline_script: true, action: 'demo' %>
34 <%= submit_tag %>
35 <% end %>
36
37 <script type="text/javascript">
38 const form = document.forms.item(0)
39 const hiddenInput = document.getElementById('g-recaptcha-response-data-demo')
40 form.addEventListener('submit', async function(e) {
41 e.preventDefault();
42 console.log('Submitted form. Getting fresh reCAPTCHA response token…')
43 const response = await window.executeRecaptchaForDemoAsync()
44 console.log(`Response was: ${response}`)
45 hiddenInput.value = response
46 form.submit()
47 });
48 </script>
49
50 <% else # Single %>
51 <%= form_tag "/#{controller_name}" do %>
52 <%= recaptcha_v3 action: 'demo' %>
53 <%= submit_tag %>
54 <% end %>
55 <% end %>
56
57 <%= link_to 'Single', '?' if params[:multi] || params[:custom_callback] || params[:execute_on_submit] %>
58 <%= link_to 'with v2 fallback', '/v3_captchas/?with_v2_fallback=1' unless params[:with_v2_fallback] %>
59 <%= link_to 'Multi', '?multi=1' unless params[:multi] %>
60 <%= link_to 'Custom callback', '?custom_callback=1' unless params[:custom_callback] %>
61 <%= link_to 'Execute on submit', '?execute_on_submit=1' unless params[:execute_on_submit] %>
00 #!/usr/bin/env ruby
1 ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
1 ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
22 load Gem.bin_path('bundler', 'bundle')
00 #!/usr/bin/env ruby
1 begin
2 load File.expand_path('../spring', __FILE__)
3 rescue LoadError => e
4 raise unless e.message.include?('spring')
5 end
6 APP_PATH = File.expand_path('../../config/application', __FILE__)
1 APP_PATH = File.expand_path('../config/application', __dir__)
72 require_relative '../config/boot'
83 require 'rails/commands'
00 #!/usr/bin/env ruby
1 begin
2 load File.expand_path('../spring', __FILE__)
3 rescue LoadError => e
4 raise unless e.message.include?('spring')
5 end
61 require_relative '../config/boot'
72 require 'rake'
83 Rake.application.run
00 #!/usr/bin/env ruby
1 require 'pathname'
1 require 'fileutils'
2 include FileUtils
23
34 # path to your application root.
4 APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
5 APP_ROOT = File.expand_path('..', __dir__)
56
6 Dir.chdir APP_ROOT do
7 def system!(*args)
8 system(*args) || abort("\n== Command #{args} failed ==")
9 end
10
11 chdir APP_ROOT do
712 # This script is a starting point to setup your application.
8 # Add necessary setup steps to this file:
13 # Add necessary setup steps to this file.
914
10 puts "== Installing dependencies =="
11 system "gem install bundler --conservative"
12 system "bundle check || bundle install"
15 puts '== Installing dependencies =='
16 system! 'gem install bundler --conservative'
17 system('bundle check') || system!('bundle install')
18
19 # Install JavaScript dependencies if using Yarn
20 # system('bin/yarn')
1321
1422 # puts "\n== Copying sample files =="
15 # unless File.exist?("config/database.yml")
16 # system "cp config/database.yml.sample config/database.yml"
23 # unless File.exist?('config/database.yml')
24 # cp 'config/database.yml.sample', 'config/database.yml'
1725 # end
1826
1927 puts "\n== Preparing database =="
20 system "bin/rake db:setup"
28 system! 'bin/rails db:setup'
2129
2230 puts "\n== Removing old logs and tempfiles =="
23 system "rm -f log/*"
24 system "rm -rf tmp/cache"
31 system! 'bin/rails log:clear tmp:clear'
2532
2633 puts "\n== Restarting application server =="
27 system "touch tmp/restart.txt"
34 system! 'bin/rails restart'
2835 end
0 #!/usr/bin/env ruby
1 require 'fileutils'
2 include FileUtils
3
4 # path to your application root.
5 APP_ROOT = File.expand_path('..', __dir__)
6
7 def system!(*args)
8 system(*args) || abort("\n== Command #{args} failed ==")
9 end
10
11 chdir APP_ROOT do
12 # This script is a way to update your development environment automatically.
13 # Add necessary update steps to this file.
14
15 puts '== Installing dependencies =='
16 system! 'gem install bundler --conservative'
17 system('bundle check') || system!('bundle install')
18
19 # Install JavaScript dependencies if using Yarn
20 # system('bin/yarn')
21
22 puts "\n== Updating database =="
23 system! 'bin/rails db:migrate'
24
25 puts "\n== Removing old logs and tempfiles =="
26 system! 'bin/rails log:clear tmp:clear'
27
28 puts "\n== Restarting application server =="
29 system! 'bin/rails restart'
30 end
0 #!/usr/bin/env ruby
1 APP_ROOT = File.expand_path('..', __dir__)
2 Dir.chdir(APP_ROOT) do
3 begin
4 exec "yarnpkg", *ARGV
5 rescue Errno::ENOENT
6 $stderr.puts "Yarn executable was not detected in the system."
7 $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
8 exit 1
9 end
10 end
0 require File.expand_path('../boot', __FILE__)
0 require_relative 'boot'
11
22 require 'rails/all'
33
77
88 module Foo
99 class Application < Rails::Application
10 # Initialize configuration defaults for originally generated Rails version.
11 config.load_defaults 5.0
12
1013 # Settings in config/environments/* take precedence over those specified here.
11 # Application configuration should go into files in config/initializers
12 # -- all .rb files in that directory are automatically loaded.
13
14 # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
15 # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
16 # config.time_zone = 'Central Time (US & Canada)'
17
18 # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
19 # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
20 # config.i18n.default_locale = :de
21
22 # Do not swallow errors in after_commit/after_rollback callbacks.
23 config.active_record.raise_in_transactional_callbacks = true
14 # Application configuration can go into files in config/initializers
15 # -- all .rb files in that directory are automatically loaded after loading
16 # the framework and any gems in your application.
2417 end
2518 end
0 ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
0 ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
11
22 require 'bundler/setup' # Set up gems listed in the Gemfile.
0 development:
1 adapter: async
2
3 test:
4 adapter: async
5
6 production:
7 adapter: redis
8 url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
9 channel_prefix: foo_production
00 # Load the Rails application.
1 require File.expand_path('../application', __FILE__)
1 require_relative 'application'
22
33 # Initialize the Rails application.
44 Rails.application.initialize!
88 # Do not eager load code on boot.
99 config.eager_load = false
1010
11 # Show full error reports and disable caching.
12 config.consider_all_requests_local = true
13 config.action_controller.perform_caching = false
11 # Show full error reports.
12 config.consider_all_requests_local = true
13
14 # Enable/disable caching. By default caching is disabled.
15 # Run rails dev:cache to toggle caching.
16 if Rails.root.join('tmp', 'caching-dev.txt').exist?
17 config.action_controller.perform_caching = true
18
19 config.cache_store = :memory_store
20 config.public_file_server.headers = {
21 'Cache-Control' => "public, max-age=#{2.days.to_i}"
22 }
23 else
24 config.action_controller.perform_caching = false
25
26 config.cache_store = :null_store
27 end
28
29 # Store uploaded files on the local file system (see config/storage.yml for options)
30 config.active_storage.service = :local
1431
1532 # Don't care if the mailer can't send.
1633 config.action_mailer.raise_delivery_errors = false
34
35 config.action_mailer.perform_caching = false
1736
1837 # Print deprecation notices to the Rails logger.
1938 config.active_support.deprecation = :log
2140 # Raise an error on page load if there are pending migrations.
2241 config.active_record.migration_error = :page_load
2342
43 # Highlight code that triggered database queries in logs.
44 config.active_record.verbose_query_logs = true
45
2446 # Debug mode disables concatenation and preprocessing of assets.
2547 # This option may cause significant delays in view rendering with a large
2648 # number of complex assets.
2749 config.assets.debug = true
2850
29 # Asset digests allow you to set far-future HTTP expiration dates on all assets,
30 # yet still be able to expire them through the digest params.
31 config.assets.digest = true
32
33 # Adds additional error checking when serving assets at runtime.
34 # Checks for improperly declared sprockets dependencies.
35 # Raises helpful error messages.
36 config.assets.raise_runtime_errors = true
51 # Suppress logger output for asset requests.
52 config.assets.quiet = true
3753
3854 # Raises error for missing translations
3955 # config.action_view.raise_on_missing_translations = true
56
57 # Use an evented file watcher to asynchronously detect changes in source code,
58 # routes, locales, etc. This feature depends on the listen gem.
59 config.file_watcher = ActiveSupport::EventedFileUpdateChecker
4060 end
0 Rails.application.configure do
1 # Settings specified here will take precedence over those in config/application.rb.
2
3 # Code is not reloaded between requests.
4 config.cache_classes = true
5
6 # Eager load code on boot. This eager loads most of Rails and
7 # your application in memory, allowing both threaded web servers
8 # and those relying on copy on write to perform better.
9 # Rake tasks automatically ignore this option for performance.
10 config.eager_load = true
11
12 # Full error reports are disabled and caching is turned on.
13 config.consider_all_requests_local = false
14 config.action_controller.perform_caching = true
15
16 # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
17 # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
18 # config.require_master_key = true
19
20 # Disable serving static files from the `/public` folder by default since
21 # Apache or NGINX already handles this.
22 config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
23
24 # Compress JavaScripts and CSS.
25 config.assets.js_compressor = :uglifier
26 # config.assets.css_compressor = :sass
27
28 # Do not fallback to assets pipeline if a precompiled asset is missed.
29 config.assets.compile = false
30
31 # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
32
33 # Enable serving of images, stylesheets, and JavaScripts from an asset server.
34 # config.action_controller.asset_host = 'http://assets.example.com'
35
36 # Specifies the header that your server uses for sending files.
37 # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
38 # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
39
40 # Store uploaded files on the local file system (see config/storage.yml for options)
41 config.active_storage.service = :local
42
43 # Mount Action Cable outside main process or domain
44 # config.action_cable.mount_path = nil
45 # config.action_cable.url = 'wss://example.com/cable'
46 # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
47
48 # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
49 # config.force_ssl = true
50
51 # Use the lowest log level to ensure availability of diagnostic information
52 # when problems arise.
53 config.log_level = :debug
54
55 # Prepend all log lines with the following tags.
56 config.log_tags = [ :request_id ]
57
58 # Use a different cache store in production.
59 # config.cache_store = :mem_cache_store
60
61 # Use a real queuing backend for Active Job (and separate queues per environment)
62 # config.active_job.queue_adapter = :resque
63 # config.active_job.queue_name_prefix = "foo_#{Rails.env}"
64
65 config.action_mailer.perform_caching = false
66
67 # Ignore bad email addresses and do not raise email delivery errors.
68 # Set this to true and configure the email server for immediate delivery to raise delivery errors.
69 # config.action_mailer.raise_delivery_errors = false
70
71 # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
72 # the I18n.default_locale when a translation cannot be found).
73 config.i18n.fallbacks = true
74
75 # Send deprecation notices to registered listeners.
76 config.active_support.deprecation = :notify
77
78 # Use default logging formatter so that PID and timestamp are not suppressed.
79 config.log_formatter = ::Logger::Formatter.new
80
81 # Use a different logger for distributed setups.
82 # require 'syslog/logger'
83 # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
84
85 if ENV["RAILS_LOG_TO_STDOUT"].present?
86 logger = ActiveSupport::Logger.new(STDOUT)
87 logger.formatter = config.log_formatter
88 config.logger = ActiveSupport::TaggedLogging.new(logger)
89 end
90
91 # Do not dump schema after migrations.
92 config.active_record.dump_schema_after_migration = false
93 end
0 Rails.application.configure do
1 # Settings specified here will take precedence over those in config/application.rb.
2
3 # The test environment is used exclusively to run your application's
4 # test suite. You never need to work with it otherwise. Remember that
5 # your test database is "scratch space" for the test suite and is wiped
6 # and recreated between test runs. Don't rely on the data there!
7 config.cache_classes = true
8
9 # Do not eager load code on boot. This avoids loading your whole application
10 # just for the purpose of running a single test. If you are using a tool that
11 # preloads Rails for running tests, you may have to set it to true.
12 config.eager_load = false
13
14 # Configure public file server for tests with Cache-Control for performance.
15 config.public_file_server.enabled = true
16 config.public_file_server.headers = {
17 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
18 }
19
20 # Show full error reports and disable caching.
21 config.consider_all_requests_local = true
22 config.action_controller.perform_caching = false
23
24 # Raise exceptions instead of rendering exception templates.
25 config.action_dispatch.show_exceptions = false
26
27 # Disable request forgery protection in test environment.
28 config.action_controller.allow_forgery_protection = false
29
30 # Store uploaded files on the local file system in a temporary directory
31 config.active_storage.service = :test
32
33 config.action_mailer.perform_caching = false
34
35 # Tell Action Mailer not to deliver emails to the real world.
36 # The :test delivery method accumulates sent emails in the
37 # ActionMailer::Base.deliveries array.
38 config.action_mailer.delivery_method = :test
39
40 # Print deprecation notices to the stderr.
41 config.active_support.deprecation = :stderr
42
43 # Raises error for missing translations
44 # config.action_view.raise_on_missing_translations = true
45 end
0 # Be sure to restart your server when you modify this file.
1
2 # ActiveSupport::Reloader.to_prepare do
3 # ApplicationController.renderer.defaults.merge!(
4 # http_host: 'example.org',
5 # https: false
6 # )
7 # end
22 # Version of your assets, change this if you want to expire all your assets.
33 Rails.application.config.assets.version = '1.0'
44
5 # Add additional assets to the asset load path
5 # Add additional assets to the asset load path.
66 # Rails.application.config.assets.paths << Emoji.images_path
7 # Add Yarn node_modules folder to the asset load path.
8 Rails.application.config.assets.paths << Rails.root.join('node_modules')
79
810 # Precompile additional assets.
9 # application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
10 # Rails.application.config.assets.precompile += %w( search.js )
11 # application.js, application.css, and all non-JS/CSS in the app/assets
12 # folder are already added.
13 # Rails.application.config.assets.precompile += %w( admin.js admin.css )
0 # Be sure to restart your server when you modify this file.
1
2 # Define an application-wide content security policy
3 # For further information see the following documentation
4 # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
5
6 # Rails.application.config.content_security_policy do |policy|
7 # policy.default_src :self, :https
8 # policy.font_src :self, :https, :data
9 # policy.img_src :self, :https, :data
10 # policy.object_src :none
11 # policy.script_src :self, :https
12 # policy.style_src :self, :https
13
14 # # Specify URI for violation reports
15 # # policy.report_uri "/csp-violation-report-endpoint"
16 # end
17
18 # If you are using UJS then enable automatic nonce generation
19 # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
20
21 # Report CSP violations to a specified URI
22 # For further information see the following documentation:
23 # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
24 # Rails.application.config.content_security_policy_report_only = true
00 # Be sure to restart your server when you modify this file.
11
2 # Specify a serializer for the signed and encrypted cookie jars.
3 # Valid options are :json, :marshal, and :hybrid.
24 Rails.application.config.action_dispatch.cookies_serializer = :json
0 # Be sure to restart your server when you modify this file.
1 #
2 # This file contains migration options to ease your Rails 5.2 upgrade.
3 #
4 # Once upgraded flip defaults one by one to migrate to the new default.
5 #
6 # Read the Guide for Upgrading Ruby on Rails for more info on each option.
7
8 # Make Active Record use stable #cache_key alongside new #cache_version method.
9 # This is needed for recyclable cache keys.
10 # Rails.application.config.active_record.cache_versioning = true
11
12 # Use AES-256-GCM authenticated encryption for encrypted cookies.
13 # Also, embed cookie expiry in signed or encrypted cookies for increased security.
14 #
15 # This option is not backwards compatible with earlier Rails versions.
16 # It's best enabled when your entire app is migrated and stable on 5.2.
17 #
18 # Existing cookies will be converted on read then written with the new scheme.
19 # Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true
20
21 # Use AES-256-GCM authenticated encryption as default cipher for encrypting messages
22 # instead of AES-256-CBC, when use_authenticated_message_encryption is set to true.
23 # Rails.application.config.active_support.use_authenticated_message_encryption = true
24
25 # Add default protection from forgery to ActionController::Base instead of in
26 # ApplicationController.
27 # Rails.application.config.action_controller.default_protect_from_forgery = true
28
29 # Store boolean values are in sqlite3 databases as 1 and 0 instead of 't' and
30 # 'f' after migrating old data.
31 # Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true
32
33 # Use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header.
34 # Rails.application.config.active_support.use_sha1_digests = true
35
36 # Make `form_with` generate id attributes for any generated HTML tags.
37 # Rails.application.config.action_view.form_with_generates_ids = true
44
55 # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
66 ActiveSupport.on_load(:action_controller) do
7 wrap_parameters format: [:json] if respond_to?(:wrap_parameters)
7 wrap_parameters format: [:json]
88 end
99
1010 # To enable root element in JSON for ActiveRecord objects.
1111 # ActiveSupport.on_load(:active_record) do
12 # self.include_root_in_json = true
12 # self.include_root_in_json = true
1313 # end
1515 #
1616 # This would use the information in config/locales/es.yml.
1717 #
18 # The following keys must be escaped otherwise they will not be retrieved by
19 # the default I18n backend:
20 #
21 # true, false, on, off, yes, no
22 #
23 # Instead, surround them with single quotes.
24 #
25 # en:
26 # 'true': 'foo'
27 #
1828 # To learn more, please read the Rails Internationalization guide
1929 # available at http://guides.rubyonrails.org/i18n.html.
2030
00 Rails.application.routes.draw do
1 root to: "captcha#index"
2 post "/captchas" => "captcha#create"
1 root to: redirect('/captchas')
2 resources :captchas, only: [:index, :create]
3 resources :v3_captchas, only: [:index, :create] do
4 collection do
5 post :create_multi
6 post :create_with_v2_fallback
7 end
8 end
39 resources :users
410 end
0 test:
1 service: Disk
2 root: <%= Rails.root.join("tmp/storage") %>
3
4 local:
5 service: Disk
6 root: <%= Rails.root.join("storage") %>
7
8 # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
9 # amazon:
10 # service: S3
11 # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
12 # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
13 # region: us-east-1
14 # bucket: your_own_bucket
15
16 # Remember not to checkin your GCS keyfile to a repository
17 # google:
18 # service: GCS
19 # project: your_project
20 # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
21 # bucket: your_own_bucket
22
23 # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
24 # microsoft:
25 # service: AzureStorage
26 # storage_account_name: your_account_name
27 # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
28 # container: your_container_name
29
30 # mirror:
31 # service: Mirror
32 # primary: local
33 # mirrors: [ amazon, google, microsoft ]
0 class AddUser < ActiveRecord::Migration
0 class AddUser < ActiveRecord::Migration[4.2]
11 def change
22 create_table :users do |t|
33 t.string :name
0 # encoding: UTF-8
10 # This file is auto-generated from the current state of the database. Instead
21 # of editing this file, please use the migrations feature of Active Record to
32 # incrementally modify your database, and then regenerate this schema definition.
109 #
1110 # It's strongly recommended that you check this file into your version control system.
1211
13 ActiveRecord::Schema.define(version: 20151226015155) do
12 ActiveRecord::Schema.define(version: 2015_12_26_015155) do
1413
1514 create_table "users", force: :cascade do |t|
1615 t.string "name"
+0
-28
demo/sinatra/Gemfile.lock less more
0 PATH
1 remote: ../..
2 specs:
3 recaptcha (0.6.0)
4 json
5
6 GEM
7 remote: https://rubygems.org/
8 specs:
9 json (1.8.1)
10 rack (1.6.4)
11 rack-protection (1.5.3)
12 rack
13 sinatra (1.4.6)
14 rack (~> 1.4)
15 rack-protection (~> 1.4)
16 tilt (>= 1.3, < 3)
17 tilt (2.0.1)
18
19 PLATFORMS
20 ruby
21
22 DEPENDENCIES
23 recaptcha!
24 sinatra
25
26 BUNDLED WITH
27 1.10.6
77 config.secret_key = '6Le7oRETAAAAAL5a8yOmEdmDi3b2pH7mq5iH1bYK'
88 end
99
10 include Recaptcha::ClientHelper
11 include Recaptcha::Verify
10 include Recaptcha::Adapters::ControllerMethods
11 include Recaptcha::Adapters::ViewMethods
1212
1313 get '/' do
1414 <<-HTML
0 # frozen_string_literal: true
1
2 module Recaptcha
3 module Adapters
4 module ControllerMethods
5 private
6
7 # Your private API can be specified in the +options+ hash or preferably
8 # using the Configuration.
9 def verify_recaptcha(options = {})
10 options = {model: options} unless options.is_a? Hash
11 return true if Recaptcha.skip_env?(options[:env])
12
13 model = options[:model]
14 attribute = options.fetch(:attribute, :base)
15 recaptcha_response = options[:response] || recaptcha_response_token(options[:action])
16
17 begin
18 verified = if Recaptcha.invalid_response?(recaptcha_response)
19 false
20 else
21 unless options[:skip_remote_ip]
22 remoteip = (request.respond_to?(:remote_ip) && request.remote_ip) || (env && env['REMOTE_ADDR'])
23 options = options.merge(remote_ip: remoteip.to_s) if remoteip
24 end
25
26 success, @_recaptcha_reply =
27 Recaptcha.verify_via_api_call(recaptcha_response, options.merge(with_reply: true))
28 success
29 end
30
31 if verified
32 flash.delete(:recaptcha_error) if recaptcha_flash_supported? && !model
33 true
34 else
35 recaptcha_error(
36 model,
37 attribute,
38 options.fetch(:message) { Recaptcha::Helpers.to_error_message(:verification_failed) }
39 )
40 false
41 end
42 rescue Timeout::Error
43 if Recaptcha.configuration.handle_timeouts_gracefully
44 recaptcha_error(
45 model,
46 attribute,
47 options.fetch(:message) { Recaptcha::Helpers.to_error_message(:recaptcha_unreachable) }
48 )
49 false
50 else
51 raise RecaptchaError, 'Recaptcha unreachable.'
52 end
53 rescue StandardError => e
54 raise RecaptchaError, e.message, e.backtrace
55 end
56 end
57
58 def verify_recaptcha!(options = {})
59 verify_recaptcha(options) || raise(VerifyError)
60 end
61
62 def recaptcha_reply
63 @_recaptcha_reply if defined?(@_recaptcha_reply)
64 end
65
66 def recaptcha_error(model, attribute, message)
67 if model
68 model.errors.add(attribute, message)
69 elsif recaptcha_flash_supported?
70 flash[:recaptcha_error] = message
71 end
72 end
73
74 def recaptcha_flash_supported?
75 request.respond_to?(:format) && request.format == :html && respond_to?(:flash)
76 end
77
78 # Extracts response token from params. params['g-recaptcha-response-data'] for recaptcha_v3 or
79 # params['g-recaptcha-response'] for recaptcha_tags and invisible_recaptcha_tags and should
80 # either be a string or a hash with the action name(s) as keys. If it is a hash, then `action`
81 # is used as the key.
82 # @return [String] A response token if one was passed in the params; otherwise, `''`
83 def recaptcha_response_token(action = nil)
84 response_param = params['g-recaptcha-response-data'] || params['g-recaptcha-response']
85 response_param = response_param[action] if action && response_param.respond_to?(:key?)
86
87 if response_param.is_a?(String)
88 response_param
89 else
90 ''
91 end
92 end
93 end
94 end
95 end
0 # frozen_string_literal: true
1
2 module Recaptcha
3 module Adapters
4 module ViewMethods
5 # Renders a [reCAPTCHA v3](https://developers.google.com/recaptcha/docs/v3) script and (by
6 # default) a hidden input to submit the response token. You can also call the functions
7 # directly if you prefer. You can use
8 # `Recaptcha::Helpers.recaptcha_v3_execute_function_name(action)` to get the name of the
9 # function to call.
10 def recaptcha_v3(options = {})
11 ::Recaptcha::Helpers.recaptcha_v3(options)
12 end
13
14 # Renders a reCAPTCHA [v2 Checkbox](https://developers.google.com/recaptcha/docs/display) widget
15 def recaptcha_tags(options = {})
16 ::Recaptcha::Helpers.recaptcha_tags(options)
17 end
18
19 # Renders a reCAPTCHA v2 [Invisible reCAPTCHA](https://developers.google.com/recaptcha/docs/invisible)
20 def invisible_recaptcha_tags(options = {})
21 ::Recaptcha::Helpers.invisible_recaptcha_tags(options)
22 end
23 end
24 end
25 end
+0
-144
lib/recaptcha/client_helper.rb less more
0 # frozen_string_literal: true
1
2 module Recaptcha
3 module ClientHelper
4 # Your public API can be specified in the +options+ hash or preferably
5 # using the Configuration.
6 def recaptcha_tags(options = {})
7 if options.key?(:stoken)
8 raise(RecaptchaError, "Secure Token is deprecated. Please remove 'stoken' from your calls to recaptcha_tags.")
9 end
10 if options.key?(:ssl)
11 raise(RecaptchaError, "SSL is now always true. Please remove 'ssl' from your calls to recaptcha_tags.")
12 end
13
14 noscript = options.delete(:noscript)
15
16 html, tag_attributes, fallback_uri = Recaptcha::ClientHelper.recaptcha_components(options)
17 html << %(<div #{tag_attributes}></div>\n)
18
19 if noscript != false
20 html << <<-HTML
21 <noscript>
22 <div>
23 <div style="width: 302px; height: 422px; position: relative;">
24 <div style="width: 302px; height: 422px; position: absolute;">
25 <iframe
26 src="#{fallback_uri}"
27 scrolling="no" name="ReCAPTCHA"
28 style="width: 302px; height: 422px; border-style: none; border: 0;">
29 </iframe>
30 </div>
31 </div>
32 <div style="width: 300px; height: 60px; border-style: none;
33 bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px;
34 background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;">
35 <textarea id="g-recaptcha-response" name="g-recaptcha-response"
36 class="g-recaptcha-response"
37 style="width: 250px; height: 40px; border: 1px solid #c1c1c1;
38 margin: 10px 25px; padding: 0px; resize: none;">
39 </textarea>
40 </div>
41 </div>
42 </noscript>
43 HTML
44 end
45
46 html.respond_to?(:html_safe) ? html.html_safe : html
47 end
48
49 # Invisible reCAPTCHA implementation
50 def invisible_recaptcha_tags(options = {})
51 options = {callback: 'invisibleRecaptchaSubmit', ui: :button}.merge options
52 text = options.delete(:text)
53 html, tag_attributes = Recaptcha::ClientHelper.recaptcha_components(options)
54 html << recaptcha_default_callback(options) if recaptcha_default_callback_required?(options)
55 case options[:ui]
56 when :button
57 html << %(<button type="submit" #{tag_attributes}>#{text}</button>\n)
58 when :invisible
59 html << %(<div data-size="invisible" #{tag_attributes}></div>\n)
60 else
61 raise(RecaptchaError, "ReCAPTCHA ui `#{options[:ui]}` is not valid.")
62 end
63 html.respond_to?(:html_safe) ? html.html_safe : html
64 end
65
66 def self.recaptcha_components(options = {})
67 html = ''.dup
68 attributes = {}
69 fallback_uri = ''.dup
70
71 # Since leftover options get passed directly through as tag
72 # attributes, we must unconditionally delete all our options
73 options = options.dup
74 env = options.delete(:env)
75 class_attribute = options.delete(:class)
76 site_key = options.delete(:site_key)
77 hl = options.delete(:hl).to_s
78 nonce = options.delete(:nonce)
79 skip_script = (options.delete(:script) == false)
80 ui = options.delete(:ui)
81
82 data_attribute_keys = [:badge, :theme, :type, :callback, :expired_callback, :error_callback, :size]
83 data_attribute_keys << :tabindex unless ui == :button
84 data_attributes = {}
85 data_attribute_keys.each do |data_attribute|
86 value = options.delete(data_attribute)
87 data_attributes["data-#{data_attribute.to_s.tr('_', '-')}"] = value if value
88 end
89
90 unless Recaptcha::Verify.skip?(env)
91 site_key ||= Recaptcha.configuration.site_key!
92 script_url = Recaptcha.configuration.api_server_url
93 script_url += "?hl=#{hl}" unless hl == ""
94 nonce_attr = " nonce='#{nonce}'" if nonce
95 html << %(<script src="#{script_url}" async defer#{nonce_attr}></script>\n) unless skip_script
96 fallback_uri = %(#{script_url.chomp(".js")}/fallback?k=#{site_key})
97 attributes["data-sitekey"] = site_key
98 attributes.merge! data_attributes
99 end
100
101 # Append whatever that's left of options to be attributes on the tag.
102 attributes["class"] = "g-recaptcha #{class_attribute}"
103 tag_attributes = attributes.merge(options).map { |k, v| %(#{k}="#{v}") }.join(" ")
104
105 [html, tag_attributes, fallback_uri]
106 end
107
108 private
109
110 def recaptcha_default_callback(options = {})
111 nonce = options[:nonce]
112 nonce_attr = " nonce='#{nonce}'" if nonce
113
114 <<-HTML
115 <script#{nonce_attr}>
116 var invisibleRecaptchaSubmit = function () {
117 var closestForm = function (ele) {
118 var curEle = ele.parentNode;
119 while (curEle.nodeName !== 'FORM' && curEle.nodeName !== 'BODY'){
120 curEle = curEle.parentNode;
121 }
122 return curEle.nodeName === 'FORM' ? curEle : null
123 };
124
125 var eles = document.getElementsByClassName('g-recaptcha');
126 if (eles.length > 0) {
127 var form = closestForm(eles[0]);
128 if (form) {
129 form.submit();
130 }
131 }
132 };
133 </script>
134 HTML
135 end
136
137 def recaptcha_default_callback_required?(options)
138 options[:callback] == 'invisibleRecaptchaSubmit' &&
139 !Recaptcha::Verify.skip?(options[:env]) &&
140 options[:script] != false
141 end
142 end
143 end
2929 # end
3030 #
3131 class Configuration
32 attr_accessor :skip_verify_env, :secret_key, :site_key, :proxy, :handle_timeouts_gracefully, :hostname
32 DEFAULTS = {
33 'free_server_url' => 'https://www.recaptcha.net/recaptcha/api.js',
34 'enterprise_server_url' => 'https://www.recaptcha.net/recaptcha/enterprise.js',
35 'free_verify_url' => 'https://www.recaptcha.net/recaptcha/api/siteverify',
36 'enterprise_verify_url' => 'https://recaptchaenterprise.googleapis.com/v1beta1/projects'
37 }.freeze
38
39 attr_accessor :default_env, :skip_verify_env, :proxy, :secret_key, :site_key, :handle_timeouts_gracefully, :hostname
40 attr_accessor :enterprise, :enterprise_api_key, :enterprise_project_id
3341 attr_writer :api_server_url, :verify_url
3442
3543 def initialize #:nodoc:
44 @default_env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || (Rails.env if defined? Rails.env)
3645 @skip_verify_env = %w[test cucumber]
37 @handle_timeouts_gracefully = HANDLE_TIMEOUTS_GRACEFULLY
46 @handle_timeouts_gracefully = true
3847
3948 @secret_key = ENV['RECAPTCHA_SECRET_KEY']
4049 @site_key = ENV['RECAPTCHA_SITE_KEY']
50
51 @enterprise = ENV['RECAPTCHA_ENTERPRISE'] == 'true'
52 @enterprise_api_key = ENV['RECAPTCHA_ENTERPRISE_API_KEY']
53 @enterprise_project_id = ENV['RECAPTCHA_ENTERPRISE_PROJECT_ID']
54
4155 @verify_url = nil
4256 @api_server_url = nil
4357 end
5064 site_key || raise(RecaptchaError, "No site key specified.")
5165 end
5266
67 def enterprise_api_key!
68 enterprise_api_key || raise(RecaptchaError, "No Enterprise API key specified.")
69 end
70
71 def enterprise_project_id!
72 enterprise_project_id || raise(RecaptchaError, "No Enterprise project ID specified.")
73 end
74
5375 def api_server_url
54 @api_server_url || CONFIG.fetch('server_url')
76 @api_server_url || (enterprise ? DEFAULTS.fetch('enterprise_server_url') : DEFAULTS.fetch('free_server_url'))
5577 end
5678
5779 def verify_url
58 @verify_url || CONFIG.fetch('verify_url')
80 @verify_url || (enterprise ? DEFAULTS.fetch('enterprise_verify_url') : DEFAULTS.fetch('free_verify_url'))
5981 end
6082 end
6183 end
0 # frozen_string_literal: true
1
2 module Recaptcha
3 module Helpers
4 DEFAULT_ERRORS = {
5 recaptcha_unreachable: 'Oops, we failed to validate your reCAPTCHA response. Please try again.',
6 verification_failed: 'reCAPTCHA verification failed, please try again.'
7 }.freeze
8
9 def self.recaptcha_v3(options = {})
10 site_key = options[:site_key] ||= Recaptcha.configuration.site_key!
11 action = options.delete(:action) || raise(Recaptcha::RecaptchaError, 'action is required')
12 id = options.delete(:id) || "g-recaptcha-response-data-" + dasherize_action(action)
13 name = options.delete(:name) || "g-recaptcha-response-data[#{action}]"
14 turbolinks = options.delete(:turbolinks)
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 if turbolinks
26 options[:onload] = recaptcha_v3_execute_function_name(action)
27 end
28 html, tag_attributes = components(options)
29 if turbolinks
30 html << recaptcha_v3_onload_script(site_key, action, callback, id, options)
31 elsif recaptcha_v3_inline_script?(options)
32 html << recaptcha_v3_inline_script(site_key, action, callback, id, options)
33 end
34 case element
35 when :input
36 html << %(<input type="hidden" name="#{name}" id="#{id}" #{tag_attributes}/>\n)
37 when false
38 # No tag
39 nil
40 else
41 raise(RecaptchaError, "ReCAPTCHA element `#{options[:element]}` is not valid.")
42 end
43 html.respond_to?(:html_safe) ? html.html_safe : html
44 end
45
46 def self.recaptcha_tags(options)
47 if options.key?(:stoken)
48 raise(RecaptchaError, "Secure Token is deprecated. Please remove 'stoken' from your calls to recaptcha_tags.")
49 end
50 if options.key?(:ssl)
51 raise(RecaptchaError, "SSL is now always true. Please remove 'ssl' from your calls to recaptcha_tags.")
52 end
53
54 noscript = options.delete(:noscript)
55
56 html, tag_attributes, fallback_uri = components(options.dup)
57 html << %(<div #{tag_attributes}></div>\n)
58
59 if noscript != false
60 html << <<-HTML
61 <noscript>
62 <div>
63 <div style="width: 302px; height: 422px; position: relative;">
64 <div style="width: 302px; height: 422px; position: absolute;">
65 <iframe
66 src="#{fallback_uri}"
67 name="ReCAPTCHA"
68 style="width: 302px; height: 422px; border-style: none; border: 0; overflow: hidden;">
69 </iframe>
70 </div>
71 </div>
72 <div style="width: 300px; height: 60px; border-style: none;
73 bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px;
74 background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;">
75 <textarea id="g-recaptcha-response" name="g-recaptcha-response"
76 class="g-recaptcha-response"
77 style="width: 250px; height: 40px; border: 1px solid #c1c1c1;
78 margin: 10px 25px; padding: 0px; resize: none;">
79 </textarea>
80 </div>
81 </div>
82 </noscript>
83 HTML
84 end
85
86 html.respond_to?(:html_safe) ? html.html_safe : html
87 end
88
89 def self.invisible_recaptcha_tags(custom)
90 options = {callback: 'invisibleRecaptchaSubmit', ui: :button}.merge(custom)
91 text = options.delete(:text)
92 html, tag_attributes = components(options.dup)
93 html << default_callback(options) if default_callback_required?(options)
94
95 case options[:ui]
96 when :button
97 html << %(<button type="submit" #{tag_attributes}>#{text}</button>\n)
98 when :invisible
99 html << %(<div data-size="invisible" #{tag_attributes}></div>\n)
100 when :input
101 html << %(<input type="submit" #{tag_attributes} value="#{text}"/>\n)
102 else
103 raise(RecaptchaError, "ReCAPTCHA ui `#{options[:ui]}` is not valid.")
104 end
105 html.respond_to?(:html_safe) ? html.html_safe : html
106 end
107
108 def self.to_error_message(key)
109 default = DEFAULT_ERRORS.fetch(key) { raise ArgumentError "Unknown reCAPTCHA error - #{key}" }
110 to_message("recaptcha.errors.#{key}", default)
111 end
112
113 if defined?(I18n)
114 def self.to_message(key, default)
115 I18n.translate(key, default: default)
116 end
117 else
118 def self.to_message(_key, default)
119 default
120 end
121 end
122
123 private_class_method def self.components(options)
124 html = +''
125 attributes = {}
126 fallback_uri = +''
127
128 options = options.dup
129 env = options.delete(:env)
130 class_attribute = options.delete(:class)
131 site_key = options.delete(:site_key)
132 hl = options.delete(:hl)
133 onload = options.delete(:onload)
134 render = options.delete(:render)
135 script_async = options.delete(:script_async)
136 script_defer = options.delete(:script_defer)
137 nonce = options.delete(:nonce)
138 skip_script = (options.delete(:script) == false) || (options.delete(:external_script) == false)
139 ui = options.delete(:ui)
140
141 data_attribute_keys = [:badge, :theme, :type, :callback, :expired_callback, :error_callback, :size]
142 data_attribute_keys << :tabindex unless ui == :button
143 data_attributes = {}
144 data_attribute_keys.each do |data_attribute|
145 value = options.delete(data_attribute)
146 data_attributes["data-#{data_attribute.to_s.tr('_', '-')}"] = value if value
147 end
148
149 unless Recaptcha.skip_env?(env)
150 site_key ||= Recaptcha.configuration.site_key!
151 script_url = Recaptcha.configuration.api_server_url
152 query_params = hash_to_query(
153 hl: hl,
154 onload: onload,
155 render: render
156 )
157 script_url += "?#{query_params}" unless query_params.empty?
158 async_attr = "async" if script_async != false
159 defer_attr = "defer" if script_defer != false
160 nonce_attr = " nonce='#{nonce}'" if nonce
161 html << %(<script src="#{script_url}" #{async_attr} #{defer_attr} #{nonce_attr}></script>\n) unless skip_script
162 fallback_uri = %(#{script_url.chomp(".js")}/fallback?k=#{site_key})
163 attributes["data-sitekey"] = site_key
164 attributes.merge! data_attributes
165 end
166
167 # The remaining options will be added as attributes on the tag.
168 attributes["class"] = "g-recaptcha #{class_attribute}"
169 tag_attributes = attributes.merge(options).map { |k, v| %(#{k}="#{v}") }.join(" ")
170
171 [html, tag_attributes, fallback_uri]
172 end
173
174 # v3
175
176 # Renders a script that calls `grecaptcha.execute` or
177 # `grecaptcha.enterprise.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 #{recaptcha_ready_method_name}(function() {
189 #{recaptcha_execute_method_name}('#{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 #{recaptcha_ready_method_name}(async function() {
203 resolve(await #{recaptcha_execute_method_name}('#{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 #{recaptcha_ready_method_name}(function() {
221 #{recaptcha_execute_method_name}('#{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
255 # reCAPTCHA code (calls `grecaptcha.execute` or
256 # `grecaptcha.enterprise.execute`). You can call it again later to reset it.
257 def self.recaptcha_v3_execute_function_name(action)
258 "executeRecaptchaFor#{sanitize_action_for_js(action)}"
259 end
260
261 # Returns the name of an async JavaScript function that executes the reCAPTCHA code.
262 def self.recaptcha_v3_async_execute_function_name(action)
263 "#{recaptcha_v3_execute_function_name(action)}Async"
264 end
265
266 def self.recaptcha_v3_default_callback_name(action)
267 "setInputWithRecaptchaResponseTokenFor#{sanitize_action_for_js(action)}"
268 end
269
270 # v2
271
272 private_class_method def self.default_callback(options = {})
273 nonce = options[:nonce]
274 nonce_attr = " nonce='#{nonce}'" if nonce
275 selector_attr = options[:id] ? "##{options[:id]}" : ".g-recaptcha"
276
277 <<-HTML
278 <script#{nonce_attr}>
279 var invisibleRecaptchaSubmit = function () {
280 var closestForm = function (ele) {
281 var curEle = ele.parentNode;
282 while (curEle.nodeName !== 'FORM' && curEle.nodeName !== 'BODY'){
283 curEle = curEle.parentNode;
284 }
285 return curEle.nodeName === 'FORM' ? curEle : null
286 };
287
288 var el = document.querySelector("#{selector_attr}")
289 if (!!el) {
290 var form = closestForm(el);
291 if (form) {
292 form.submit();
293 }
294 }
295 };
296 </script>
297 HTML
298 end
299
300 def self.recaptcha_execute_method_name
301 Recaptcha.configuration.enterprise ? "grecaptcha.enterprise.execute" : "grecaptcha.execute"
302 end
303
304 def self.recaptcha_ready_method_name
305 Recaptcha.configuration.enterprise ? "grecaptcha.enterprise.ready" : "grecaptcha.ready"
306 end
307
308 private_class_method def self.default_callback_required?(options)
309 options[:callback] == 'invisibleRecaptchaSubmit' &&
310 !Recaptcha.skip_env?(options[:env]) &&
311 options[:script] != false &&
312 options[:inline_script] != false
313 end
314
315 # Returns a camelized string that is safe for use in a JavaScript variable/function name.
316 # sanitize_action_for_js('my/action') => 'MyAction'
317 private_class_method def self.sanitize_action_for_js(action)
318 action.to_s.gsub(/\W/, '_').split(/\/|_/).map(&:capitalize).join
319 end
320
321 # Returns a dasherized string that is safe for use as an HTML ID
322 # dasherize_action('my/action') => 'my-action'
323 private_class_method def self.dasherize_action(action)
324 action.to_s.gsub(/\W/, '-').tr('_', '-')
325 end
326
327 private_class_method def self.hash_to_query(hash)
328 hash.delete_if { |_, val| val.nil? || val.empty? }.to_a.map { |pair| pair.join('=') }.join('&')
329 end
330 end
331 end
00 # frozen_string_literal: true
1
12 # deprecated, but let's not blow everyone up
23 require 'recaptcha'
22 module Recaptcha
33 class Railtie < Rails::Railtie
44 ActiveSupport.on_load(:action_view) do
5 require 'recaptcha/client_helper'
6 include Recaptcha::ClientHelper
5 include Recaptcha::Adapters::ViewMethods
76 end
87
98 ActiveSupport.on_load(:action_controller) do
10 require 'recaptcha/verify'
11 include Recaptcha::Verify
9 include Recaptcha::Adapters::ControllerMethods
10 end
11
12 initializer 'recaptcha' do |app|
13 Recaptcha::Railtie.instance_eval do
14 pattern = pattern_from app.config.i18n.available_locales
15
16 add("rails/locales/#{pattern}.yml")
17 end
18 end
19
20 class << self
21 protected
22
23 def add(pattern)
24 files = Dir[File.join(File.dirname(__FILE__), '../..', pattern)]
25 I18n.load_path.concat(files)
26 end
27
28 def pattern_from(args)
29 array = Array(args || [])
30 array.blank? ? '*' : "{#{array.join ','}}"
31 end
1232 end
1333 end
1434 end
+0
-107
lib/recaptcha/verify.rb less more
0 # frozen_string_literal: true
1
2 require 'json'
3
4 module Recaptcha
5 module Verify
6 # Your private API can be specified in the +options+ hash or preferably
7 # using the Configuration.
8 def verify_recaptcha(options = {})
9 options = {model: options} unless options.is_a? Hash
10 return true if Recaptcha::Verify.skip?(options[:env])
11
12 model = options[:model]
13 attribute = options[:attribute] || :base
14 recaptcha_response = options[:response] || params['g-recaptcha-response'].to_s
15
16 begin
17 verified = if recaptcha_response.empty?
18 false
19 else
20 recaptcha_verify_via_api_call(request, recaptcha_response, options)
21 end
22
23 if verified
24 flash.delete(:recaptcha_error) if recaptcha_flash_supported? && !model
25 true
26 else
27 recaptcha_error(
28 model,
29 attribute,
30 options[:message],
31 "recaptcha.errors.verification_failed",
32 "reCAPTCHA verification failed, please try again."
33 )
34 false
35 end
36 rescue Timeout::Error
37 if Recaptcha.configuration.handle_timeouts_gracefully
38 recaptcha_error(
39 model,
40 attribute,
41 options[:message],
42 "recaptcha.errors.recaptcha_unreachable",
43 "Oops, we failed to validate your reCAPTCHA response. Please try again."
44 )
45 false
46 else
47 raise RecaptchaError, "Recaptcha unreachable."
48 end
49 rescue StandardError => e
50 raise RecaptchaError, e.message, e.backtrace
51 end
52 end
53
54 def verify_recaptcha!(options = {})
55 verify_recaptcha(options) || raise(VerifyError)
56 end
57
58 def self.skip?(env)
59 env ||= ENV['RAILS_ENV'] || ENV['RACK_ENV'] || (Rails.env if defined? Rails.env)
60 Recaptcha.configuration.skip_verify_env.include? env
61 end
62
63 private
64
65 def recaptcha_verify_via_api_call(request, recaptcha_response, options)
66 secret_key = options[:secret_key] || Recaptcha.configuration.secret_key!
67
68 verify_hash = {
69 "secret" => secret_key,
70 "response" => recaptcha_response
71 }
72
73 unless options[:skip_remote_ip]
74 remoteip = (request.respond_to?(:remote_ip) && request.remote_ip) || (env && env['REMOTE_ADDR'])
75 verify_hash["remoteip"] = remoteip.to_s
76 end
77
78 reply = JSON.parse(Recaptcha.get(verify_hash, options))
79 reply['success'].to_s == "true" &&
80 recaptcha_hostname_valid?(reply['hostname'], options[:hostname])
81 end
82
83 def recaptcha_hostname_valid?(hostname, validation)
84 validation ||= Recaptcha.configuration.hostname
85
86 case validation
87 when nil, FalseClass then true
88 when String then validation == hostname
89 else validation.call(hostname)
90 end
91 end
92
93 def recaptcha_error(model, attribute, message, key, default)
94 message ||= Recaptcha.i18n(key, default)
95 if model
96 model.errors.add attribute, message
97 else
98 flash[:recaptcha_error] = message if recaptcha_flash_supported?
99 end
100 end
101
102 def recaptcha_flash_supported?
103 request.respond_to?(:format) && request.format == :html && respond_to?(:flash)
104 end
105 end
106 end
00 # frozen_string_literal: true
11
22 module Recaptcha
3 VERSION = '4.11.1'
3 VERSION = '5.8.1'
44 end
00 # frozen_string_literal: true
11
2 require 'json'
3 require 'net/http'
4 require 'uri'
5
26 require 'recaptcha/configuration'
3 require 'uri'
4 require 'net/http'
5
7 require 'recaptcha/helpers'
8 require 'recaptcha/adapters/controller_methods'
9 require 'recaptcha/adapters/view_methods'
610 if defined?(Rails)
711 require 'recaptcha/railtie'
8 else
9 require 'recaptcha/client_helper'
10 require 'recaptcha/verify'
1112 end
1213
1314 module Recaptcha
14 CONFIG = {
15 'server_url' => 'https://www.google.com/recaptcha/api.js',
16 'verify_url' => 'https://www.google.com/recaptcha/api/siteverify'
17 }.freeze
15 DEFAULT_TIMEOUT = 3
16 RESPONSE_LIMIT = 4000
1817
19 USE_SSL_BY_DEFAULT = false
20 HANDLE_TIMEOUTS_GRACEFULLY = true
21 DEFAULT_TIMEOUT = 3
18 class RecaptchaError < StandardError
19 end
20
21 class VerifyError < RecaptchaError
22 end
2223
2324 # Gives access to the current Configuration.
2425 def self.configuration
4950 original_config.each { |key, value| configuration.send("#{key}=", value) }
5051 end
5152
52 def self.get(verify_hash, options)
53 http = if Recaptcha.configuration.proxy
54 proxy_server = URI.parse(Recaptcha.configuration.proxy)
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 > 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)
64 else
65 verify_via_api_call_free(response, options)
66 end
67 end
68
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 token_properties = reply['tokenProperties']
81 success = !token_properties.nil? &&
82 token_properties['valid'].to_s == 'true' &&
83 hostname_valid?(token_properties['hostname'], options[:hostname]) &&
84 action_valid?(token_properties['action'], options[:action]) &&
85 score_above_threshold?(reply['score'], options[:minimum_score])
86
87 if options[:with_reply] == true
88 return success, reply
89 else
90 return success
91 end
92 end
93
94 def self.verify_via_api_call_free(response, options)
95 secret_key = options.fetch(:secret_key) { configuration.secret_key! }
96 verify_hash = { 'secret' => secret_key, 'response' => response }
97 verify_hash['remoteip'] = options[:remote_ip] if options.key?(:remote_ip)
98
99 reply = api_verification_free(verify_hash, timeout: options[:timeout])
100 success = reply['success'].to_s == 'true' &&
101 hostname_valid?(reply['hostname'], options[:hostname]) &&
102 action_valid?(reply['action'], options[:action]) &&
103 score_above_threshold?(reply['score'], options[:minimum_score])
104
105 if options[:with_reply] == true
106 return success, reply
107 else
108 return success
109 end
110 end
111
112 def self.hostname_valid?(hostname, validation)
113 validation ||= configuration.hostname
114
115 case validation
116 when nil, FalseClass then true
117 when String then validation == hostname
118 else validation.call(hostname)
119 end
120 end
121
122 def self.action_valid?(action, expected_action)
123 case expected_action
124 when nil, FalseClass then true
125 else action == expected_action
126 end
127 end
128
129 # Returns true iff score is greater or equal to (>=) minimum_score, or if no minimum_score was specified
130 def self.score_above_threshold?(score, minimum_score)
131 return true if minimum_score.nil?
132 return false if score.nil?
133
134 case minimum_score
135 when nil, FalseClass then true
136 else score >= minimum_score
137 end
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)
55144 Net::HTTP::Proxy(proxy_server.host, proxy_server.port, proxy_server.user, proxy_server.password)
56145 else
57146 Net::HTTP
58147 end
59 query = URI.encode_www_form(verify_hash)
60 uri = URI.parse(Recaptcha.configuration.verify_url + '?' + query)
61 http_instance = http.new(uri.host, uri.port)
62 http_instance.read_timeout = http_instance.open_timeout = options[:timeout] || DEFAULT_TIMEOUT
63 http_instance.use_ssl = true if uri.port == 443
64 request = Net::HTTP::Get.new(uri.request_uri)
65 http_instance.request(request).body
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
66153 end
67154
68 def self.i18n(key, default)
69 if defined?(I18n)
70 I18n.translate(key, default: default)
71 else
72 default
73 end
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)
74161 end
75162
76 class RecaptchaError < StandardError
77 end
78
79 class VerifyError < RecaptchaError
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)
80171 end
81172 end
0 en:
1 recaptcha:
2 errors:
3 verification_failed: reCAPTCHA verification failed, please try again.
4 recaptcha_unreachable: Oops, we failed to validate your reCAPTCHA response. Please try again.
0 fr:
1 recaptcha:
2 errors:
3 verification_failed: La vérification reCAPTCHA a échoué, veuillez essayer à nouveau.
4 recaptcha_unreachable: Oops, nous n'avons pas pu valider votre réponse reCAPTCHA. Veuillez essayer à nouveau.
99 s.homepage = "http://github.com/ambethia/recaptcha"
1010 s.summary = s.description = "Helpers for the reCAPTCHA API"
1111 s.license = "MIT"
12 s.required_ruby_version = '>= 2.3.0'
12 s.required_ruby_version = '>= 2.4.0'
1313
14 s.files = `git ls-files lib README.md CHANGELOG.md LICENSE`.split("\n")
14 s.files = `git ls-files lib rails README.md CHANGELOG.md LICENSE`.split("\n")
1515
1616 s.add_runtime_dependency "json"
1717 s.add_development_dependency "mocha"
1818 s.add_development_dependency "rake"
19 s.add_development_dependency "activesupport"
2019 s.add_development_dependency "i18n"
2120 s.add_development_dependency "maxitest"
2221 s.add_development_dependency "pry-byebug"
2322 s.add_development_dependency "bump"
2423 s.add_development_dependency "webmock"
2524 s.add_development_dependency "rubocop"
25
26 s.metadata = { "source_code_uri" => "https://github.com/ambethia/recaptcha" }
2627 end
00 require_relative 'helper'
1 require 'active_support/core_ext/object/blank'
2 require 'active_support/core_ext/hash'
3
4 describe Recaptcha::ClientHelper do
5 include Recaptcha::ClientHelper
1
2 describe 'View helpers' do
3 include Recaptcha::Adapters::ViewMethods
64
75 it "uses ssl" do
86 recaptcha_tags.must_include "\"#{Recaptcha.configuration.api_server_url}\""
5755
5856 it "adds :hl option to the url" do
5957 html = recaptcha_tags(hl: 'en')
60 html.must_include("?hl=en")
58 html.must_include("hl=en")
6159
6260 html = recaptcha_tags(hl: 'ru')
63 html.wont_include("?hl=en")
64 html.must_include("?hl=ru")
61 html.wont_include("hl=en")
62 html.must_include("hl=ru")
6563
6664 html = recaptcha_tags
67 html.wont_include("?hl=")
65 html.wont_include("hl=")
66 end
67
68 it "adds :onload option to the url" do
69 html = recaptcha_tags(onload: 'foobar')
70 html.must_include("onload=foobar")
71
72 html = recaptcha_tags(onload: 'anotherFoobar')
73 html.wont_include("onload=foobar")
74 html.must_include("onload=anotherFoobar")
75
76 html = recaptcha_tags
77 html.wont_include("onload=")
78 end
79
80 describe "turbolinks" do
81 it "adds onload to defined function" do
82 html = recaptcha_v3(action: 'request', turbolinks: true)
83 html.must_include("onload=executeRecaptchaForRequest")
84 end
85
86 it "overrides specified onload" do
87 html = recaptcha_v3(action: 'request', onload: "foobar", turbolinks: true)
88 html.wont_include("onload=foobar")
89 html.must_include("onload=executeRecaptchaForRequest")
90 end
91 end
92
93 it "adds :render option to the url" do
94 html = recaptcha_tags(render: 'onload')
95 html.must_include("render=onload")
96
97 html = recaptcha_tags(render: 'explicit')
98 html.wont_include("render=onload")
99 html.must_include("render=explicit")
100
101 html = recaptcha_tags
102 html.wont_include("render=")
103 end
104
105 it "adds query params to the url" do
106 html = recaptcha_tags(hl: 'en', onload: 'foobar')
107 html.must_include("?")
108 html.must_include("hl=en")
109 html.must_include("&")
110 html.must_include("onload=foobar")
68111 end
69112
70113 it "includes the site key in the button attributes" do
71114 html = invisible_recaptcha_tags
72115 html.must_include(" data-sitekey=\"#{Recaptcha.configuration.site_key}\"")
116 end
117
118 it "lets you override the site_key from configuration via options hash" do
119 html = invisible_recaptcha_tags(site_key: 'different_key')
120 html.must_include(" data-sitekey=\"different_key\"")
73121 end
74122
75123 it "dasherizes the expired_callback attribute name" do
153201 html.wont_include("<button")
154202 end
155203
204 it "renders an input element with supplied text if UI is input" do
205 html = invisible_recaptcha_tags(ui: :input, text: 'Send')
206 html.must_include("<input type=\"submit\"")
207 html.must_include("value=\"Send\"/>")
208 html.wont_include("<button")
209 end
210
211 it "includes a custom selector if provided" do
212 html = invisible_recaptcha_tags(id: 'custom-selector')
213 html.must_include("id=\"custom-selector\"")
214 html.must_include("document.querySelector(\"#custom-selector\")")
215 end
216
217 it "uses default selector if no custom selector has been provided" do
218 html = invisible_recaptcha_tags
219 html.must_include("document.querySelector(\".g-recaptcha\")")
220 end
221
156222 it "raises an error on an invalid ui option" do
157223 assert_raises Recaptcha::RecaptchaError do
158224 invisible_recaptcha_tags(ui: :foo)
159225 end
160226 end
161227 end
228
229 describe "v3 recaptcha" do
230 it "renders input" do
231 html = recaptcha_v3 action: :foo
232 html.must_include('<input type="hidden" name="g-recaptcha-response-data[foo]" id="g-recaptcha-response-data-foo" data-sitekey="0000000000000000000000000000000000000000" class="g-recaptcha g-recaptcha-response "/>')
233 end
234
235 it "does not have obsole closing script tag" do
236 html = recaptcha_v3 action: :foo
237 assert html.scan(/script/).length.even?
238 end
239 end
162240 end
11
22 describe Recaptcha::Configuration do
33 describe "#api_server_url" do
4 it "serves the default" do
5 Recaptcha.configuration.api_server_url.must_equal "https://www.google.com/recaptcha/api.js"
4 it "serves the default (free API)" do
5 Recaptcha.configuration.api_server_url.must_equal "https://www.recaptcha.net/recaptcha/api.js"
6 end
7
8 describe "when enterprise is set to true" do
9 it "serves the enterprise API URL" do
10 Recaptcha.with_configuration(enterprise: true) do
11 Recaptcha.configuration.api_server_url.must_equal "https://www.recaptcha.net/recaptcha/enterprise.js"
12 end
13 end
614 end
715
816 describe "when api_server_url is overwritten" do
917 it "serves the overwritten url" do
1018 proxied_api_server_url = 'https://127.0.0.1:8080/recaptcha/api.js'
11 Recaptcha.with_configuration(api_server_url: proxied_api_server_url) do
19 Recaptcha.configuration.api_server_url = proxied_api_server_url
20 begin
1221 Recaptcha.configuration.api_server_url.must_equal proxied_api_server_url
22 ensure
23 Recaptcha.configuration.api_server_url = nil
1324 end
1425 end
1526 end
1728
1829 describe "#verify_url" do
1930 it "serves the default" do
20 Recaptcha.configuration.verify_url.must_equal "https://www.google.com/recaptcha/api/siteverify"
31 Recaptcha.configuration.verify_url.must_equal "https://www.recaptcha.net/recaptcha/api/siteverify"
2132 end
2233
2334 describe "when api_server_url is overwritten" do
2435 it "serves the overwritten url" do
2536 proxied_verify_url = 'https://127.0.0.1:8080/recaptcha/api/siteverify'
26 Recaptcha.with_configuration(verify_url: proxied_verify_url) do
37 Recaptcha.configuration.verify_url = proxied_verify_url
38 begin
2739 Recaptcha.configuration.verify_url.must_equal proxied_verify_url
40 ensure
41 Recaptcha.configuration.verify_url = nil
2842 end
2943 end
3044 end
0 # set default_env to nil
1 ENV.delete('RAILS_ENV')
2 ENV.delete('RACK_ENV')
3
04 require 'bundler/setup'
15 require 'maxitest/autorun'
26 require 'mocha/setup'
37 require 'webmock/minitest'
8 require 'byebug'
49 require 'cgi'
10 require 'i18n'
511 require 'recaptcha'
6 require 'i18n'
7
8 ENV.delete('RAILS_ENV')
9 ENV.delete('RACK_ENV')
1012
1113 I18n.enforce_available_locales = false
1214
1416 def setup
1517 super
1618 Recaptcha.configure do |config|
17 config.site_key = '0000000000000000000000000000000000000000'
18 config.secret_key = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
19 config.site_key = '0000000000000000000000000000000000000000'
20 config.secret_key = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
21 config.enterprise_api_key = 'ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ'
22 config.enterprise_project_id = 'test-project'
1923 end
2024 end
2125 end)
0 require_relative 'helper'
1
2 describe 'controller helpers (enterprise)' do
3 before do
4 Recaptcha.configuration.enterprise = true
5
6 @controller = TestController.new
7 @controller.request = stub(remote_ip: "1.1.1.1", format: :html)
8 @controller.params = {:recaptcha_response_field => "response", 'g-recaptcha-response-data' => 'string'}
9 end
10
11 after do
12 Recaptcha.configuration.enterprise = false
13 end
14
15 describe "#verify_recaptcha!" do
16 it "raises when it fails" do
17 @controller.expects(:verify_recaptcha).returns(false)
18
19 assert_raises Recaptcha::VerifyError do
20 @controller.verify_recaptcha!
21 end
22 end
23
24 it "returns a value when it passes" do
25 @controller.expects(:verify_recaptcha).returns(:foo)
26
27 assert_equal :foo, @controller.verify_recaptcha!
28 end
29 end
30
31 describe "#verify_recaptcha" do
32 it "returns true on success" do
33 @controller.flash[:recaptcha_error] = "previous error that should be cleared"
34 expect_http_post.to_return(body: '{"tokenProperties":{"valid":true}}')
35
36 assert @controller.verify_recaptcha
37 assert_nil @controller.flash[:recaptcha_error]
38 end
39
40 it "raises without api key" do
41 Recaptcha.configuration.enterprise_api_key = nil
42 assert_raises Recaptcha::RecaptchaError do
43 @controller.verify_recaptcha
44 end
45 end
46
47 it "returns false when secret key is invalid" do
48 expect_http_post.to_return(body: %({"foo":"false", "bar":"invalid-site-secret-key"}))
49
50 refute @controller.verify_recaptcha
51 assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error]
52 end
53
54 it "adds an error to the model" do
55 expect_http_post.to_return(body: %({"foo":"false", "bar":"bad-news"}))
56
57 errors = mock
58 errors.expects(:add).with(:base, "reCAPTCHA verification failed, please try again.")
59 model = mock(errors: errors)
60
61 refute @controller.verify_recaptcha(model: model)
62 assert_nil @controller.flash[:recaptcha_error]
63 end
64
65 it "returns true on success with optional key" do
66 key = 'ADIFFERENTPRIVATEKEYXXXXXXXXXXXXXX'
67 @controller.flash[:recaptcha_error] = "previous error that should be cleared"
68 expect_http_post(enterprise_api_key: key).to_return(body: '{"tokenProperties":{"valid":true}}')
69
70 assert @controller.verify_recaptcha(enterprise_api_key: key)
71 assert_nil @controller.flash[:recaptcha_error]
72 end
73
74 it "returns true on success without remote_ip" do
75 @controller.flash[:recaptcha_error] = "previous error that should be cleared"
76 expect_http_post.to_return(body: '{"tokenProperties":{"valid":true}}')
77
78 assert @controller.verify_recaptcha(skip_remote_ip: true)
79 assert_nil @controller.flash[:recaptcha_error]
80 end
81
82 it "fails silently when timing out" do
83 expect_http_post.to_timeout
84 refute @controller.verify_recaptcha
85 @controller.flash[:recaptcha_error].must_equal(
86 "Oops, we failed to validate your reCAPTCHA response. Please try again."
87 )
88 end
89
90 it "blows up on timeout when graceful is disabled" do
91 Recaptcha.with_configuration(handle_timeouts_gracefully: false) do
92 expect_http_post.to_timeout
93 assert_raises Recaptcha::RecaptchaError, "Recaptcha unreachable." do
94 assert @controller.verify_recaptcha
95 end
96 assert_nil @controller.flash[:recaptcha_error]
97 end
98 end
99
100 it "uses I18n for the failed message" do
101 I18n.locale = :de
102 verification_failed_translated = "Sicherheitscode konnte nicht verifiziert werden."
103 verification_failed_default = "reCAPTCHA verification failed, please try again."
104
105 I18n.expects(:translate).
106 with('recaptcha.errors.verification_failed', default: verification_failed_default).
107 returns(verification_failed_translated)
108
109 errors = mock
110 errors.expects(:add).with(:base, verification_failed_translated)
111 model = mock
112 model.stubs(errors: errors)
113
114 expect_http_post.to_return(body: %({"foo":"false", "bar":"bad-news"}))
115 @controller.verify_recaptcha(model: model)
116 end
117
118 it "uses I18n for the timeout message" do
119 I18n.locale = :de
120 recaptcha_unreachable_translated = "Netzwerkfehler, bitte versuchen Sie es später erneut."
121 recaptcha_unreachable_default = "Oops, we failed to validate your reCAPTCHA response. Please try again."
122
123 I18n.expects(:translate).
124 with('recaptcha.errors.recaptcha_unreachable', default: recaptcha_unreachable_default).
125 returns(recaptcha_unreachable_translated)
126
127 errors = mock
128 errors.expects(:add).with(:base, recaptcha_unreachable_translated)
129 model = mock
130 model.stubs(errors: errors)
131
132 expect_http_post.to_timeout
133 @controller.verify_recaptcha(model: model)
134 end
135
136 it "translates api response with I18n" do
137 api_error_translated = "Bad news, body :("
138 expect_http_post.to_return(body: %({"foo":"false", "bar":"bad-news"}))
139 I18n.expects(:translate).
140 with('recaptcha.errors.verification_failed', default: 'reCAPTCHA verification failed, please try again.').
141 returns(api_error_translated)
142
143 refute @controller.verify_recaptcha
144 assert_equal api_error_translated, @controller.flash[:recaptcha_error]
145 end
146
147 it "falls back to api response if i18n translation is missing" do
148 expect_http_post.to_return(body: %({"foo":"false", "bar":"bad-news"}))
149
150 refute @controller.verify_recaptcha
151 assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error]
152 end
153
154 it "does not flash error when request was not html" do
155 @controller.request = stub(remote_ip: "1.1.1.1", format: :json)
156 expect_http_post.to_return(body: %({"foo":"false", "bar":"bad-news"}))
157 refute @controller.verify_recaptcha
158 assert_nil @controller.flash[:recaptcha_error]
159 end
160
161 it "does not verify via http call when user did not click anything" do
162 @controller.params = { 'g-recaptcha-response' => ""}
163 assert_not_requested :get, %r{\.google\.com}
164 assert_equal false, @controller.verify_recaptcha
165 assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error]
166 end
167
168 it "does not verify via http call when response length exceeds G_RESPONSE_LIMIT" do
169 # this returns a 400 or 413 instead of a 200 response with error code
170 # typical response length is less than 400 characters
171 str = "a" * 4001
172 @controller.params = { 'g-recaptcha-response' => "#{str}"}
173 assert_not_requested :get, %r{\.google\.com}
174 assert_equal false, @controller.verify_recaptcha
175 assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error]
176 end
177
178 describe ':hostname' do
179 let(:hostname) { 'fake.hostname.com' }
180
181 before do
182 expect_http_post.to_return(body: %({"tokenProperties":{"valid":true,"hostname":"#{hostname}"}}))
183 end
184
185 it "passes with nil" do
186 assert @controller.verify_recaptcha(hostname: nil)
187 assert_nil @controller.flash[:recaptcha_error]
188 end
189
190 it "passes with false" do
191 assert @controller.verify_recaptcha(hostname: false)
192 assert_nil @controller.flash[:recaptcha_error]
193 end
194
195 it "check for equality when string custom hostname validation is passed" do
196 assert @controller.verify_recaptcha(hostname: hostname)
197 assert_nil @controller.flash[:recaptcha_error]
198 end
199
200 it "fails when custom hostname validation does not match" do
201 expect_http_post.to_return(body: %({"success":true, "hostname": "not_#{hostname}"}))
202
203 refute @controller.verify_recaptcha(hostname: hostname)
204 assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error]
205 end
206
207 it "check with call when callable custom hostname validation is passed" do
208 assert @controller.verify_recaptcha(hostname: -> (d) { d == hostname })
209 assert_nil @controller.flash[:recaptcha_error]
210 end
211
212 it "raises when invalid custom hostname validation is passed" do
213 assert_raises Recaptcha::RecaptchaError do
214 @controller.verify_recaptcha(hostname: 0)
215 end
216 end
217
218 describe "when default hostname validation matches" do
219 around { |test| Recaptcha.with_configuration(hostname: hostname, &test) }
220
221 it "passes" do
222 assert @controller.verify_recaptcha
223 assert_nil @controller.flash[:recaptcha_error]
224 end
225
226 it "fails when custom validation does not match" do
227 refute @controller.verify_recaptcha(hostname: "not_#{hostname}")
228 assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error]
229 end
230 end
231
232 describe "when default hostname validation does not match" do
233 around { |test| Recaptcha.with_configuration(hostname: "not_#{hostname}", &test) }
234
235 it "fails" do
236 refute @controller.verify_recaptcha
237 assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error]
238 end
239
240 it "passes when custom validation matches" do
241 assert @controller.verify_recaptcha(hostname: hostname)
242 assert_nil @controller.flash[:recaptcha_error]
243 end
244 end
245 end
246
247 describe 'action_valid?' do
248 let(:default_response_hash) {
249 {
250 tokenProperties: {
251 valid: true,
252 action: 'homepage'
253 }
254 }
255 }
256
257 before do
258 expect_http_post.to_return(body: success_body)
259 end
260
261 it "fails when action from response does not match expected action" do
262 expect_http_post.to_return(body: success_body(action: "not_homepage"))
263
264 refute verify_recaptcha(action: 'homepage')
265 assert_flash_error
266 end
267
268 it "passes with string that matches" do
269 assert verify_recaptcha(action: 'homepage')
270 assert_nil @controller.flash[:recaptcha_error]
271 end
272
273 it "passes with nil" do
274 assert verify_recaptcha(action: nil)
275 assert_nil @controller.flash[:recaptcha_error]
276 end
277
278 it "passes with false" do
279 assert verify_recaptcha(action: false)
280 assert_nil @controller.flash[:recaptcha_error]
281 end
282 end
283
284 describe 'score_above_threshold?' do
285 let(:default_response_hash) {
286 {
287 tokenProperties: {
288 valid: true,
289 action: 'homepage'
290 }
291 }
292 }
293
294 it "fails when score is below minimum_score" do
295 expect_http_post.to_return(body: success_body(score: 0.4))
296
297 refute verify_recaptcha(minimum_score: 0.5)
298 assert_flash_error
299 end
300
301 it "fails when response doesn't include a score" do
302 expect_http_post.to_return(body: success_body)
303
304 refute verify_recaptcha(minimum_score: 0.4)
305 assert_flash_error
306 end
307
308 it "passes with score exactly at minimum_score" do
309 expect_http_post.to_return(body: success_body(score: 0.4))
310
311 assert verify_recaptcha(minimum_score: 0.4)
312 assert_nil @controller.flash[:recaptcha_error]
313 end
314
315 it "passes when minimum_score not specified or nil" do
316 expect_http_post.to_return(body: success_body(score: 0.4))
317
318 assert verify_recaptcha()
319 assert_nil @controller.flash[:recaptcha_error]
320 end
321
322 it "passes with false" do
323 expect_http_post.to_return(body: success_body(score: 0.4))
324
325 assert verify_recaptcha(minimum_score: false)
326 assert_nil @controller.flash[:recaptcha_error]
327 end
328 end
329 end
330
331 describe "#recatcha_reply" do
332 let(:default_response_hash) {
333 {
334 tokenProperties: {
335 valid: true,
336 action: 'homepage'
337 },
338 score: 0.97
339 }
340 }
341
342 before do
343 expect_http_post.to_return(body: success_body)
344 end
345
346 it "is initially nil" do
347 assert_nil @controller.recaptcha_reply
348 end
349
350 it "contains the recaptcha reply once verify_recaptcha has been called" do
351 assert verify_recaptcha()
352 assert_equal default_response_hash.to_json, @controller.recaptcha_reply.to_json
353 end
354 end
355
356 private
357
358 class TestController
359 include Recaptcha::Adapters::ControllerMethods
360
361 attr_accessor :request, :params, :flash
362
363 def initialize
364 @flash = {}
365 end
366
367 public :verify_recaptcha
368 public :verify_recaptcha!
369 public :recaptcha_reply
370 end
371
372 def expect_http_post(enterprise_api_key: Recaptcha.configuration.enterprise_api_key,
373 enterprise_project_id: Recaptcha.configuration.enterprise_project_id)
374 stub_request(
375 :post,
376 "https://recaptchaenterprise.googleapis.com/v1beta1/projects/#{enterprise_project_id}/assessments?key=#{enterprise_api_key}"
377 )
378 end
379
380 def success_body(action: nil, score: nil)
381 result = default_response_hash
382 result[:tokenProperties][:action] = action if action
383 result[:score] = score if score
384 result.to_json
385 end
386
387 def verify_recaptcha(options = {})
388 options[:action] = 'homepage' unless options.key?(:action)
389 @controller.verify_recaptcha(options)
390 end
391
392 def assert_flash_error
393 assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error]
394 end
395 end
00 require_relative 'helper'
11
2 describe Recaptcha::Verify do
2 describe 'controller helpers' do
33 before do
44 @controller = TestController.new
55 @controller.request = stub(remote_ip: "1.1.1.1", format: :html)
6
7 @expected_post_data = {}
8 @expected_post_data["remoteip"] = @controller.request.remote_ip
9 @expected_post_data["response"] = "response"
10
11 @controller.params = {:recaptcha_response_field => "response", 'g-recaptcha-response' => 'string'}
12 @expected_post_data["secret"] = Recaptcha.configuration.secret_key
13
14 @expected_uri = URI.parse(Recaptcha.configuration.verify_url)
6 @controller.params = {:recaptcha_response_field => "response", 'g-recaptcha-response-data' => 'string'}
157 end
168
179 describe "#verify_recaptcha!" do
7870 secret_key = Recaptcha.configuration.secret_key
7971 stub_request(
8072 :get,
81 "https://www.google.com/recaptcha/api/siteverify?response=string&secret=#{secret_key}"
73 "https://www.recaptcha.net/recaptcha/api/siteverify?response=string&secret=#{secret_key}"
8274 ).to_return(body: '{"success":true}')
8375
8476 assert @controller.verify_recaptcha(skip_remote_ip: true)
171163 assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error]
172164 end
173165
166 it "does not verify via http call when response length exceeds G_RESPONSE_LIMIT" do
167 # this returns a 400 or 413 instead of a 200 response with error code
168 # typical response length is less than 400 characters
169 str = "a" * 4001
170 @controller.params = { 'g-recaptcha-response' => "#{str}"}
171 assert_not_requested :get, %r{\.google\.com}
172 assert_equal false, @controller.verify_recaptcha
173 assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error]
174 end
175
174176 describe ':hostname' do
175177 let(:hostname) { 'fake.hostname.com' }
176178
239241 end
240242 end
241243 end
244
245 describe 'action_valid?' do
246 let(:default_response_hash) { {
247 success: true,
248 action: 'homepage',
249 } }
250
251 before do
252 expect_http_post.to_return(body: success_body)
253 end
254
255 it "fails when action from response does not match expected action" do
256 expect_http_post.to_return(body: success_body(action: "not_homepage"))
257
258 refute verify_recaptcha(action: 'homepage')
259 assert_flash_error
260 end
261
262 it "passes with string that matches" do
263 assert verify_recaptcha(action: 'homepage')
264 assert_nil @controller.flash[:recaptcha_error]
265 end
266
267 it "passes with nil" do
268 assert verify_recaptcha(action: nil)
269 assert_nil @controller.flash[:recaptcha_error]
270 end
271
272 it "passes with false" do
273 assert verify_recaptcha(action: false)
274 assert_nil @controller.flash[:recaptcha_error]
275 end
276 end
277
278 describe 'score_above_threshold?' do
279 let(:default_response_hash) { {
280 success: true,
281 action: 'homepage',
282 } }
283
284 before do
285 expect_http_post.to_return(body: success_body(score: 0.4))
286 end
287
288 it "fails when score is below minimum_score" do
289 refute verify_recaptcha(minimum_score: 0.5)
290 assert_flash_error
291 end
292
293 it "fails when response doesn't include a score" do
294 expect_http_post.to_return(body: success_body())
295 refute verify_recaptcha(minimum_score: 0.4)
296 assert_flash_error
297 end
298
299 it "passes with score exactly at minimum_score" do
300 assert verify_recaptcha(minimum_score: 0.4)
301 assert_nil @controller.flash[:recaptcha_error]
302 end
303
304 it "passes when minimum_score not specified or nil" do
305 assert verify_recaptcha()
306 assert_nil @controller.flash[:recaptcha_error]
307 end
308
309 it "passes with false" do
310 assert verify_recaptcha(minimum_score: false)
311 assert_nil @controller.flash[:recaptcha_error]
312 end
313 end
314 end
315
316 describe "#recatcha_reply" do
317 let(:default_response_hash) { {
318 success: true,
319 score: 0.97,
320 action: 'homepage',
321 } }
322
323 before do
324 expect_http_post.to_return(body: success_body)
325 end
326
327 it "is initially nil" do
328 assert_nil @controller.recaptcha_reply
329 end
330
331 it "contains the recaptcha reply once verify_recaptcha has been called" do
332 assert verify_recaptcha()
333 assert_equal default_response_hash.to_json, @controller.recaptcha_reply.to_json
334 end
335 end
336
337 describe "#recaptcha_response_token" do
338 it "returns an empty string when params are empty and no action is provided" do
339 @controller.params = {}
340 assert_equal @controller.recaptcha_response_token, ""
341 end
342
343 it "returns an empty string when g-recaptcha-response-data is invalid and no action is provided" do
344 @controller.params = { "g-recaptcha-response-data" => {} }
345 assert_equal @controller.recaptcha_response_token, ""
346 end
347
348 it "returns an empty string when g-recaptcha-response is invalid and no action is provided" do
349 @controller.params = { "g-recaptcha-response" => {} }
350 assert_equal @controller.recaptcha_response_token, ""
351 end
352
353 it "returns the g-recaptcha-response-data when response is valid and no action is provided" do
354 @controller.params = { "g-recaptcha-response-data" => "recaptcha-response-data" }
355 assert_equal @controller.recaptcha_response_token, "recaptcha-response-data"
356 end
357
358 it "returns the g-recaptcha-response when response is valid and no action is provided" do
359 @controller.params = { "g-recaptcha-response" => "recaptcha-response" }
360 assert_equal @controller.recaptcha_response_token, "recaptcha-response"
361 end
362
363 it "returns an empty string when params are empty and an action is provided" do
364 @controller.params = {}
365 assert_equal @controller.recaptcha_response_token("test"), ""
366 end
367
368 it "returns an empty string when g-recaptcha-response-data params are invalid and an action is provided" do
369 @controller.params = { "g-recaptcha-response-data" => ["\n"] }
370 assert_equal @controller.recaptcha_response_token("test"), ""
371 end
372
373 it "returns an empty string when g-recaptcha-response-data params are nil and an action is provided" do
374 @controller.params = { "g-recaptcha-response-data" => nil }
375 assert_equal @controller.recaptcha_response_token("test"), ""
376 end
377
378 it "returns an empty string when g-recaptcha-response-data params are empty and an action is provided" do
379 @controller.params = { "g-recaptcha-response-data" => {} }
380 assert_equal @controller.recaptcha_response_token("test"), ""
381 end
382
383 it "returns an empty string when g-recaptcha-response-data params are valid but an invalid action is provided" do
384 @controller.params = { "g-recaptcha-response-data" => { "test2" => "recaptcha-response-data" } }
385 assert_equal @controller.recaptcha_response_token("test"), ""
386 end
387
388 it "returns an empty string when g-recaptcha-response params are valid but an invalid action is provided" do
389 @controller.params = { "g-recaptcha-response" => { "test2" => "recaptcha-response-data" } }
390 assert_equal @controller.recaptcha_response_token("test"), ""
391 end
392
393 it "returns the g-recaptcha-response-data action when params are valid and an action is provided" do
394 @controller.params = { "g-recaptcha-response-data" => { "test" => "recaptcha-response-data" } }
395 assert_equal @controller.recaptcha_response_token("test"), "recaptcha-response-data"
396 end
397
398 it "returns the g-recaptcha-response action when params are valid and an action is provided" do
399 @controller.params = { "g-recaptcha-response" => { "test" => "recaptcha-response" } }
400 assert_equal @controller.recaptcha_response_token("test"), "recaptcha-response"
401 end
242402 end
243403
244404 private
245405
246406 class TestController
247 include Recaptcha::Verify
407 include Recaptcha::Adapters::ControllerMethods
408
248409 attr_accessor :request, :params, :flash
249410
250411 def initialize
251412 @flash = {}
252413 end
414
415 public :verify_recaptcha
416 public :verify_recaptcha!
417 public :recaptcha_reply
418 public :recaptcha_response_token
253419 end
254420
255421 def expect_http_post(secret_key: Recaptcha.configuration.secret_key)
256422 stub_request(
257423 :get,
258 "https://www.google.com/recaptcha/api/siteverify?remoteip=1.1.1.1&response=string&secret=#{secret_key}"
424 "https://www.recaptcha.net/recaptcha/api/siteverify?remoteip=1.1.1.1&response=string&secret=#{secret_key}"
259425 )
260426 end
427
428 def success_body(other = {})
429 default_response_hash.
430 merge(other).
431 to_json
432 end
433
434 def verify_recaptcha(options = {})
435 options[:action] = 'homepage' unless options.key?(:action)
436 @controller.verify_recaptcha(options)
437 end
438
439 def assert_flash_error
440 assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error]
441 end
261442 end