Import upstream version 5.2.1, md5 038ca8b5e3dc75fda2d80aaee0596f9a
Debian Janitor
4 years ago
0 | 0 | AllCops: |
1 | 1 | TargetRubyVersion: 2.3 |
2 | 2 | Include: |
3 | - 'lib/**/*' | |
3 | 4 | - 'Rakefile' |
4 | 5 | - 'Gemfile' |
5 | - 'Rakefile' | |
6 | 6 | Exclude: |
7 | 7 | - 'vendor/**/*' |
8 | 8 | - 'demo/**/*' |
49 | 49 | Style/NumericLiterals: |
50 | 50 | Enabled: false |
51 | 51 | |
52 | Layout/FirstParameterIndentation: | |
52 | Layout/IndentFirstArgument: | |
53 | 53 | Enabled: false |
54 | 54 | |
55 | Layout/IndentHash: | |
55 | Layout/IndentFirstHashElement: | |
56 | 56 | Enabled: false |
57 | 57 | |
58 | 58 | Layout/AlignParameters: |
65 | 65 | Enabled: false |
66 | 66 | |
67 | 67 | Metrics/PerceivedComplexity: |
68 | Enabled: false | |
69 | ||
70 | Metrics/CyclomaticComplexity: | |
71 | 68 | Enabled: false |
72 | 69 | |
73 | 70 | Style/DoubleNegation: |
4 | 4 | - 2.3 |
5 | 5 | - 2.4 |
6 | 6 | - 2.5 |
7 | - 2.6 | |
7 | 8 | env: |
8 | 9 | - TASK=test |
9 | 10 | matrix: |
13 | 14 | script: bundle exec rake $TASK |
14 | 15 | branches: |
15 | 16 | only: master |
16 | matrix: | |
17 | fast_finish: true | |
18 | ||
17 | before_install: ruby -e "File.write('Gemfile.lock', File.read('Gemfile.lock').split('BUNDLED WITH').first)" |
0 | ## Next | |
1 | ||
2 | ## 5.1.0 | |
3 | * Added default translations for rails/i18n | |
4 | * use recaptcha.net for the script tag | |
5 | ||
6 | ## 5.0.0 | |
7 | * Changed host to Recaptcha.net | |
8 | * Add v3 API support | |
9 | * Renamed `Recaptcha::ClientHelper` to `Recaptcha::Adapters::ViewMethods` | |
10 | * Renamed `Recaptcha::Verify` to `Recaptcha::Adapters::ControllerMethods` | |
11 | ||
12 | ## 4.12.0 - 2018-08-30 | |
13 | * add `input` option to `invisible_recaptcha_tags`'s `ui` setting | |
14 | ||
15 | ## 4.11.1 - 2018-08-08 | |
16 | * leave `tabindex` attribute alone for `invisible_recaptcha_tags` | |
17 | ||
0 | 18 | ## 4.11.0 - 2018-08-06 |
1 | 19 | * prefer RAILS_ENV over RACK_ENV #286 |
2 | 20 | |
59 | 77 | * support disabling stoken |
60 | 78 | * support Rails.env |
61 | 79 | |
80 | ## 0.4.0 / 2015-03-22 | |
81 | ||
82 | * Add support for ReCaptcha v2 API | |
83 | * V2 API requires `g-recaptcha-response` parameters; https://github.com/ambethia/recaptcha/pull/114 | |
84 | ||
62 | 85 | ## 0.3.6 / 2012-01-07 |
63 | 86 | |
64 | 87 | * Many documentation changes |
0 | 0 | PATH |
1 | 1 | remote: . |
2 | 2 | specs: |
3 | recaptcha (4.11.1) | |
3 | recaptcha (5.2.1) | |
4 | 4 | json |
5 | 5 | |
6 | 6 | GEM |
7 | 7 | remote: https://rubygems.org/ |
8 | 8 | 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) | |
15 | 10 | public_suffix (>= 2.0.2, < 4.0) |
16 | 11 | ast (2.4.0) |
17 | bump (0.6.1) | |
18 | byebug (10.0.2) | |
12 | bump (0.8.0) | |
13 | byebug (11.0.1) | |
19 | 14 | coderay (1.1.2) |
20 | concurrent-ruby (1.0.5) | |
15 | concurrent-ruby (1.1.5) | |
21 | 16 | crack (0.4.3) |
22 | 17 | 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) | |
25 | 20 | concurrent-ruby (~> 1.0) |
26 | jaro_winkler (1.5.1) | |
27 | json (2.1.0) | |
21 | jaro_winkler (1.5.2) | |
22 | json (2.2.0) | |
28 | 23 | maxitest (3.1.0) |
29 | 24 | minitest (>= 5.0.0, < 5.12.0) |
30 | 25 | metaclass (0.0.4) |
31 | method_source (0.9.0) | |
26 | method_source (0.9.2) | |
32 | 27 | minitest (5.11.3) |
33 | mocha (1.5.0) | |
28 | mocha (1.8.0) | |
34 | 29 | 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) | |
37 | 32 | ast (~> 2.4.0) |
38 | powerpack (0.1.2) | |
39 | pry (0.11.3) | |
33 | pry (0.12.2) | |
40 | 34 | coderay (~> 1.1.0) |
41 | 35 | 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) | |
44 | 38 | pry (~> 0.10) |
45 | public_suffix (3.0.2) | |
39 | public_suffix (3.0.3) | |
46 | 40 | rainbow (3.0.0) |
47 | rake (12.3.1) | |
48 | rubocop (0.57.2) | |
41 | rake (12.3.2) | |
42 | rubocop (0.68.1) | |
49 | 43 | jaro_winkler (~> 1.5.1) |
50 | 44 | parallel (~> 1.10) |
51 | parser (>= 2.5) | |
52 | powerpack (~> 0.1) | |
45 | parser (>= 2.5, != 2.5.1.1) | |
53 | 46 | rainbow (>= 2.2.2, < 4.0) |
54 | 47 | 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) | |
63 | 53 | addressable (>= 2.3.6) |
64 | 54 | crack (>= 0.3.2) |
65 | 55 | hashdiff |
68 | 58 | ruby |
69 | 59 | |
70 | 60 | DEPENDENCIES |
71 | activesupport | |
72 | 61 | bump |
73 | 62 | i18n |
74 | 63 | maxitest |
80 | 69 | webmock |
81 | 70 | |
82 | 71 | BUNDLED WITH |
83 | 1.16.1 | |
72 | 2.0.1 |
0 | 0 | # reCAPTCHA |
1 | [![Gem Version](https://badge.fury.io/rb/recaptcha.svg)](https://badge.fury.io/rb/recaptcha) | |
1 | 2 | |
2 | 3 | Author: Jason L Perry (http://ambethia.com)<br/> |
3 | 4 | Copyright: Copyright (c) 2007-2013 Jason L Perry<br/> |
5 | 6 | Info: https://github.com/ambethia/recaptcha<br/> |
6 | 7 | Bugs: https://github.com/ambethia/recaptcha/issues<br/> |
7 | 8 | |
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. | |
9 | This gem provides helper methods for the [reCAPTCHA API](https://www.google.com/recaptcha). In your | |
10 | views you can use the `recaptcha_tags` method to embed the needed javascript, and you can validate | |
11 | in your controllers with `verify_recaptcha` or `verify_recaptcha!`, which raises an error on | |
12 | failure. | |
13 | ||
14 | ## Obtaining a key | |
15 | ||
16 | Go to the [reCAPTCHA admin console](https://www.google.com/recaptcha/admin) to obtain a reCAPTCHA API key. | |
17 | ||
18 | The reCAPTCHA type(s) that you choose for your key will determine which methods to use below. | |
19 | ||
20 | | reCAPTCHA type | Methods to use | Description | | |
21 | |----------------------------------------------|----------------|-------------| | |
22 | | v3 | [`recaptcha_v3`](#recaptcha_v3) | Verify requests with a [score](https://developers.google.com/recaptcha/docs/v3#score) | |
23 | | v2 Checkbox<br/>("I'm not a robot" Checkbox) | [`recaptcha_tags`](#recaptcha_tags) | Validate requests with the "I'm not a robot" checkbox | | |
24 | | v2 Invisible<br/>(Invisible reCAPTCHA badge) | [`invisible_recaptcha_tags`](#invisible_recaptcha_tags) | Validate requests in the background | | |
25 | ||
26 | Note: You can _only_ use methods that match your key's type. You cannot use v2 methods with a v3 | |
27 | key or use `recaptcha_tags` with a v2 Invisible key, for example. Otherwise you will get an | |
28 | error like "Invalid key type" or "This site key is not enabled for the invisible captcha." | |
29 | ||
30 | Note: Enter `localhost` or `127.0.0.1` as the domain if using in development with `localhost:3000`. | |
12 | 31 | |
13 | 32 | ## Rails Installation |
14 | 33 | |
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 | |
34 | ```ruby | |
18 | 35 | gem "recaptcha" |
19 | 36 | ``` |
20 | 37 | |
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' | |
38 | 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/> | |
39 | ||
40 | In development, you can use the [dotenv](https://github.com/bkeepers/dotenv) gem. (Make sure to add it above `gem 'recaptcha'`.) | |
41 | ||
42 | See [Alternative API key setup](#alternative-api-key-setup) for more ways to configure or override | |
43 | keys. See also the | |
44 | [Configuration](https://www.rubydoc.info/github/ambethia/recaptcha/master/Recaptcha/Configuration) | |
45 | documentation. | |
46 | ||
47 | ```shell | |
48 | export RECAPTCHA_SITE_KEY = '6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy' | |
28 | 49 | export RECAPTCHA_SECRET_KEY = '6Lc6BAAAAAAAAKN3DRm6VA_xxxxxxxxxxxxxxxxx' |
29 | 50 | ``` |
30 | 51 | |
31 | Add `recaptcha_tags` to the forms you want to protect. | |
32 | ||
33 | ```Erb | |
52 | Add `recaptcha_tags` to the forms you want to protect: | |
53 | ||
54 | ```erb | |
34 | 55 | <%= form_for @foo do |f| %> |
35 | # ... other tags | |
56 | # … | |
36 | 57 | <%= recaptcha_tags %> |
37 | # ... other tags | |
58 | # … | |
38 | 59 | <% end %> |
39 | 60 | ``` |
40 | 61 | |
41 | And, add `verify_recaptcha` logic to each form action that you've protected. | |
42 | ||
43 | ```Ruby | |
62 | Then, add `verify_recaptcha` logic to each form action that you've protected: | |
63 | ||
64 | ```ruby | |
44 | 65 | # app/controllers/users_controller.rb |
45 | 66 | @user = User.new(params[:user].permit(:name)) |
46 | 67 | if verify_recaptcha(model: @user) && @user.save |
56 | 77 | |
57 | 78 | - add `gem 'recaptcha'` to `Gemfile` |
58 | 79 | - 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 | |
80 | - `include Recaptcha::Adapters::ViewMethods` where you need `recaptcha_tags` | |
81 | - `include Recaptcha::Adapters::ControllerMethods` where you need `verify_recaptcha` | |
82 | ||
83 | ||
84 | ## reCAPTCHA v2 API and Usage | |
85 | ||
86 | ### `recaptcha_tags` | |
87 | ||
88 | Use this when your key's reCAPTCHA type is "v2 Checkbox". | |
89 | ||
90 | The following options are available: | |
91 | ||
92 | | Option | Description | | |
93 | |---------------------|-------------| | |
94 | | `:theme` | Specify the theme to be used per the API. Available options: `dark` and `light`. (default: `light`) | | |
95 | | `:ajax` | Render the dynamic AJAX captcha per the API. (default: `false`) | | |
96 | | `:site_key` | Override site API key from configuration | | |
97 | | `:error` | Override the error code returned from the reCAPTCHA API (default: `nil`) | | |
98 | | `:size` | Specify a size (default: `nil`) | | |
99 | | `:nonce` | Optional. Sets nonce attribute for script. Can be generated via `SecureRandom.base64(32)`. (default: `nil`) | | |
100 | | `:id` | Specify an html id attribute (default: `nil`) | | |
101 | | `:callback` | Optional. Name of success callback function, executed when the user submits a successful response | | |
102 | | `:expired_callback` | Optional. Name of expiration callback function, executed when the reCAPTCHA response expires and the user needs to re-verify. | | |
103 | | `:error_callback` | Optional. Name of error callback function, executed when reCAPTCHA encounters an error (e.g. network connectivity) | | |
104 | | `:noscript` | Include `<noscript>` content (default: `true`)| | |
105 | ||
106 | [JavaScript resource (api.js) parameters](https://developers.google.com/recaptcha/docs/invisible#js_param): | |
107 | ||
108 | | Option | Description | | |
109 | |---------------------|-------------| | |
110 | | `: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)) | | |
111 | | `: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)) | | |
112 | | `: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)) | | |
113 | | `: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`). | | |
114 | | `: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. | | |
115 | | `:script_async` | Set to `false` to load the external `api.js` resource synchronously. (default: `true`) | | |
116 | | `:script_defer` | Set to `true` to defer loading of external `api.js` until HTML documen has been parsed. (default: `true`) | | |
117 | ||
118 | Any unrecognized options will be added as attributes on the generated tag. | |
119 | ||
120 | You can also override the html attributes for the sizes of the generated `textarea` and `iframe` | |
121 | elements, if CSS isn't your thing. Inspect the [source of `recaptcha_tags`](https://github.com/ambethia/recaptcha/blob/master/lib/recaptcha/client_helper.rb) | |
122 | to see these options. | |
123 | ||
124 | Note that you cannot submit/verify the same response token more than once or you will get a | |
125 | `timeout-or-duplicate` error code. If you need reset the captcha and generate a new response token, | |
126 | then you need to call `grecaptcha.reset()`. | |
127 | ||
128 | ### `verify_recaptcha` | |
129 | ||
130 | This method returns `true` or `false` after processing the response token from the reCAPTCHA widget. | |
131 | This is usually called from your controller, as seen [above](#rails-installation). | |
132 | ||
133 | Passing in the ActiveRecord object via `model: object` is optional. If you pass a `model`—and the | |
134 | captcha fails to verify—an error will be added to the object for you to use (available as | |
135 | `object.errors`). | |
136 | ||
137 | Why isn't this a model validation? Because that violates MVC. You can use it like this, or how ever | |
138 | you like. | |
63 | 139 | |
64 | 140 | Some of the options available: |
65 | 141 | |
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). | |
142 | | Option | Description | | |
143 | |----------------|-------------| | |
144 | | `:model` | Model to set errors. | |
145 | | `:attribute` | Model attribute to receive errors. (default: `:base`) | |
146 | | `:message` | Custom error message. | |
147 | | `:secret_key` | Override the secret API key from the configuration. | |
148 | | `:timeout` | The number of seconds to wait for reCAPTCHA servers before give up. (default: `3`) | |
149 | | `:response` | Custom response parameter. (default: `params['g-recaptcha-response']`) | |
150 | | `: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`) | |
151 | | `:env` | Current environment. The request to verify will be skipped if the environment is specified in configuration under `skip_verify_env` | |
152 | ||
153 | ||
154 | ### `invisible_recaptcha_tags` | |
155 | ||
156 | Use this when your key's reCAPTCHA type is "v2 Invisible". | |
157 | ||
158 | For more information, refer to: [Invisible reCAPTCHA](https://developers.google.com/recaptcha/docs/invisible). | |
159 | ||
160 | This is similar to `recaptcha_tags`, with the following additional options that are only available | |
161 | on `invisible_recaptcha_tags`: | |
162 | ||
163 | | Option | Description | | |
164 | |---------------------|-------------| | |
165 | | `: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. | | |
166 | | `:text` | The text to show for the button. (default: `"Submit"`) | |
167 | | `:inline_script` | If you do not need this helper to add an inline script tag, you can set the option to `false` (default: `true`). | |
168 | ||
169 | It also accepts most of the options that `recaptcha_tags` accepts, including the following: | |
170 | ||
171 | | Option | Description | | |
172 | |---------------------|-------------| | |
173 | | `:site_key` | Override site API key from configuration | | |
174 | | `:nonce` | Optional. Sets nonce attribute for script tag. Can be generated via `SecureRandom.base64(32)`. (default: `nil`) | | |
175 | | `:id` | Specify an html id attribute (default: `nil`) | | |
176 | | `: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. | | |
177 | | `:callback` | Optional. Name of success callback function, executed when the user submits a successful response | | |
178 | | `:expired_callback` | Optional. Name of expiration callback function, executed when the reCAPTCHA response expires and the user needs to re-verify. | | |
179 | | `:error_callback` | Optional. Name of error callback function, executed when reCAPTCHA encounters an error (e.g. network connectivity) | | |
180 | ||
181 | [JavaScript resource (api.js) parameters](https://developers.google.com/recaptcha/docs/invisible#js_param): | |
182 | ||
183 | | Option | Description | | |
184 | |---------------------|-------------| | |
185 | | `: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)) | | |
186 | | `: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)) | | |
187 | | `: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)) | | |
188 | | `: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. | | |
189 | | `:script_async` | Set to `false` to load the external `api.js` resource synchronously. (default: `true`) | | |
190 | | `:script_defer` | Set to `false` to defer loading of external `api.js` until HTML documen has been parsed. (default: `true`) | | |
108 | 191 | |
109 | 192 | ### With a single form on a page |
110 | 193 | |
111 | 194 | 1. The `invisible_recaptcha_tags` generates a submit button for you. |
112 | 195 | |
113 | ```Erb | |
196 | ```erb | |
114 | 197 | <%= form_for @foo do |f| %> |
115 | 198 | # ... other tags |
116 | 199 | <%= invisible_recaptcha_tags text: 'Submit form' %> |
124 | 207 | 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. |
125 | 208 | 2. The `invisible_recaptcha_tags` generates a submit button for you. |
126 | 209 | |
127 | ```Erb | |
210 | ```erb | |
128 | 211 | <%= form_for @foo, html: {id: 'invisible-recaptcha-form'} do |f| %> |
129 | 212 | # ... other tags |
130 | 213 | <%= invisible_recaptcha_tags callback: 'submitInvisibleRecaptchaForm', text: 'Submit form' %> |
131 | 214 | <% end %> |
132 | 215 | ``` |
133 | 216 | |
134 | ```Javascript | |
217 | ```javascript | |
135 | 218 | // app/assets/javascripts/application.js |
136 | 219 | var submitInvisibleRecaptchaForm = function () { |
137 | 220 | document.getElementById("invisible-recaptcha-form").submit(); |
144 | 227 | |
145 | 228 | 1. Specify `ui` option |
146 | 229 | |
147 | ```Erb | |
230 | ```erb | |
148 | 231 | <%= form_for @foo, html: {id: 'invisible-recaptcha-form'} do |f| %> |
149 | 232 | # ... other tags |
150 | 233 | <button type="button" id="submit-btn"> |
154 | 237 | <% end %> |
155 | 238 | ``` |
156 | 239 | |
157 | ```Javascript | |
240 | ```javascript | |
158 | 241 | // app/assets/javascripts/application.js |
159 | 242 | document.getElementById('submit-btn').addEventListener('click', function (e) { |
160 | 243 | // do some validation |
169 | 252 | }; |
170 | 253 | ``` |
171 | 254 | |
255 | ||
256 | ## reCAPTCHA v3 API and Usage | |
257 | ||
258 | The main differences from v2 are: | |
259 | 1. you must specify an [action](https://developers.google.com/recaptcha/docs/v3#actions) in both frontend and backend | |
260 | 1. you can choose the minimum score required for you to consider the verification a success | |
261 | (consider the user a human and not a robot) | |
262 | 1. reCAPTCHA v3 is invisible (except for the reCAPTCHA badge) and will never interrupt your users; | |
263 | you have to choose which scores are considered an acceptable risk, and choose what to do (require | |
264 | two-factor authentication, show a v3 challenge, etc.) if the score falls below the threshold you | |
265 | choose | |
266 | ||
267 | For more information, refer to the [v3 documentation](https://developers.google.com/recaptcha/docs/v3). | |
268 | ||
269 | ### Examples | |
270 | ||
271 | With v3, you can let all users log in without any intervention at all if their score is above some | |
272 | threshold, and only show a v2 checkbox recaptcha challenge (fall back to v2) if it is below the | |
273 | threshold: | |
274 | ||
275 | ```erb | |
276 | … | |
277 | <% if @show_checkbox_recaptcha %> | |
278 | <%= recaptcha_tags %> | |
279 | <% else %> | |
280 | <%= recaptcha_v3(action: 'login') %> | |
281 | <% end %> | |
282 | … | |
283 | ``` | |
284 | ||
285 | ```ruby | |
286 | # app/controllers/sessions_controller.rb | |
287 | def create | |
288 | success = verify_recaptcha(action: 'login', minimum_score: 0.5) | |
289 | checkbox_success = verify_recaptcha unless success | |
290 | if success || checkbox_success | |
291 | # Perform action | |
292 | else | |
293 | if !success | |
294 | @show_checkbox_recaptcha = true | |
295 | end | |
296 | render 'new' | |
297 | end | |
298 | end | |
299 | ``` | |
300 | ||
301 | (You can also find this [example](demo/rails/app/controllers/v3_captchas_controller.rb) in the demo app.) | |
302 | ||
303 | Another example: | |
304 | ||
305 | ```erb | |
306 | <%= form_for @user do |f| %> | |
307 | … | |
308 | <%= recaptcha_v3(action: 'registration') %> | |
309 | … | |
310 | <% end %> | |
311 | ``` | |
312 | ||
313 | ```ruby | |
314 | # app/controllers/users_controller.rb | |
315 | def create | |
316 | @user = User.new(params[:user].permit(:name)) | |
317 | recaptcha_valid = verify_recaptcha(model: @user, action: 'registration') | |
318 | if recaptcha_valid | |
319 | if @user.save | |
320 | redirect_to @user | |
321 | else | |
322 | render 'new' | |
323 | end | |
324 | else | |
325 | # Score is below threshold, so user may be a bot. Show a challenge, require multi-factor | |
326 | # authentication, or do something else. | |
327 | render 'new' | |
328 | end | |
329 | end | |
330 | ``` | |
331 | ||
332 | ||
333 | ### `recaptcha_v3` | |
334 | ||
335 | Adds an inline script tag that calls `grecaptcha.execute` for the given `site_key` and `action` and | |
336 | calls the `callback` with the resulting response token. You need to verify this token with | |
337 | [`verify_recaptcha`](#verify_recaptcha-use-with-v3) in your controller in order to get the | |
338 | [score](https://developers.google.com/recaptcha/docs/v3#score). | |
339 | ||
340 | By default, this inserts a hidden `<input type="hidden" class="g-recaptcha-response">` tag. The | |
341 | value of this input will automatically be set to the response token (by the default callback | |
342 | function). This lets you include `recaptcha_v3` within a `<form>` tag and have it automatically | |
343 | submit the token as part of the form submission. | |
344 | ||
345 | Note: reCAPTCHA actually already adds its own hidden tag, like `<textarea | |
346 | id="g-recaptcha-response-100000" name="g-recaptcha-response" class="g-recaptcha-response">`, | |
347 | immediately ater the reCAPTCHA badge in the bottom right of the page — but since it is not inside of | |
348 | any `<form>` element, and since it already passes the token to the callback, this hidden `textarea` | |
349 | isn't helpful to us. | |
350 | ||
351 | If you need to submit the response token to the server in a different way than via a regular form | |
352 | 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), | |
353 | then you can either: | |
354 | 1. just extract the token out of the hidden `<input>` or `<textarea>` (both of which will have a | |
355 | predictable name/id), like `document.getElementById('g-recaptcha-response-my-action').value`, or | |
356 | 2. write and specify a custom `callback` function. You may also want to pass `element: false` if you | |
357 | don't have a use for the hidden input element. | |
358 | ||
359 | Note that you cannot submit/verify the same response token more than once or you will get a | |
360 | `timeout-or-duplicate` error code. If you need reset the captcha and generate a new response token, | |
361 | then you need to call `grecaptcha.execute(…)` again. This helper provides a JavaScript method (for | |
362 | each action) named `executeRecaptchaFor{action}` to make this easier. That is the same method that | |
363 | is invoked immediately. It simply calls `grecaptcha.execute` again and then calls the `callback` | |
364 | function with the response token. | |
365 | ||
366 | You will also get a `timeout-or-duplicate` error if too much time has passed between getting the | |
367 | response token and verifying it. This can easily happen with large forms that take the user a couple | |
368 | minutes to complete. Unlike v2, where you can use the `expired-callback` to be notified when the | |
369 | response expries, v3 appears to provide no such callback. See also | |
370 | [1](https://github.com/google/recaptcha/issues/281) and | |
371 | [2](https://stackoverflow.com/questions/54437745/recaptcha-v3-how-to-deal-with-expired-token-after-idle). | |
372 | ||
373 | To deal with this, it is recommended to call the "execute" in your form's submit handler (or | |
374 | immediately before sending to the server to verify if not using a form) rather than using the | |
375 | response token that gets generated when the page first loads. The `executeRecaptchaFor{action}` | |
376 | function mentioned above can be used if you want it to invoke a callback, or the | |
377 | `executeRecaptchaFor{action}Async` variant if you want a `Promise` that you can `await`. See | |
378 | [demo/rails/app/views/v3_captchas/index.html.erb](demo/rails/app/views/v3_captchas/index.html.erb) | |
379 | for an example of this. | |
380 | ||
381 | This helper is similar to the [`recaptcha_tags`](#recaptcha_tags)/[`invisible_recaptcha_tags`](#invisible_recaptcha_tags) helpers | |
382 | but only accepts the following options: | |
383 | ||
384 | | Option | Description | | |
385 | |---------------------|-------------| | |
386 | | `:site_key` | Override site API key | | |
387 | | `: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. | | |
388 | | `:nonce` | Optional. Sets nonce attribute for script. Can be generated via `SecureRandom.base64(32)`. (default: `nil`) | | |
389 | | `: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. | | |
390 | | `:id` | Specify a unique `id` attribute for the `<input>` element if using `element: :input`. (default: `"g-recaptcha-response-"` + `action`) | | |
391 | | `:name` | Specify a unique `name` attribute for the `<input>` element if using `element: :input`. (default: `g-recaptcha-response[action]`) | | |
392 | | `:script` | Same as setting both `:inline_script` and `:external_script`. (default: `true`). | | |
393 | | `: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`) | | |
394 | | `: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. | | |
395 | ||
396 | [JavaScript resource (api.js) parameters](https://developers.google.com/recaptcha/docs/invisible#js_param): | |
397 | ||
398 | | Option | Description | | |
399 | |---------------------|-------------| | |
400 | | `: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))| | |
401 | | `: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. | |
402 | | `:script_async` | Set to `true` to load the external `api.js` resource asynchronously. (default: `false`) | | |
403 | | `:script_defer` | Set to `true` to defer loading of external `api.js` until HTML documen has been parsed. (default: `false`) | | |
404 | ||
405 | If using `element: :input`, any unrecognized options will be added as attributes on the generated | |
406 | `<input>` element. | |
407 | ||
408 | ### `verify_recaptcha` (use with v3) | |
409 | ||
410 | This works the same as for v2, except that you may pass an `action` and `minimum_score` if you wish | |
411 | to validate that the action matches or that the score is above the given threshold, respectively. | |
412 | ||
413 | ```ruby | |
414 | result = verify_recaptcha(action: 'action/name') | |
415 | ``` | |
416 | ||
417 | | Option | Description | | |
418 | |------------------|-------------| | |
419 | | `: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. | |
420 | | `: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`) | | |
421 | ||
422 | ### Multiple actions on the same page | |
423 | ||
424 | According to https://developers.google.com/recaptcha/docs/v3#placement, | |
425 | ||
426 | > Note: You can execute reCAPTCHA as many times as you'd like with different actions on the same page. | |
427 | ||
428 | You will need to verify each action individually with separate call to `verify_recaptcha`. | |
429 | ||
430 | ```ruby | |
431 | result_a = verify_recaptcha(action: 'a') | |
432 | result_b = verify_recaptcha(action: 'b') | |
433 | ``` | |
434 | ||
435 | Because the response tokens for multiple actions may be submitted together in the same request, they | |
436 | are passed as a hash under `params['g-recaptcha-response']` with the action as the key. | |
437 | ||
438 | It is recommended to pass `external_script: false` on all but one of the calls to | |
439 | `recaptcha` since you only need to include the script tag once for a given `site_key`. | |
440 | ||
172 | 441 | ## 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 | |
442 | ||
443 | reCAPTCHA supports the I18n gem (it comes with English translations) | |
444 | To override or add new languages, add to `config/locales/*.yml` | |
445 | ||
446 | ```yaml | |
447 | # config/locales/en.yml | |
183 | 448 | en: |
184 | 449 | recaptcha: |
185 | 450 | errors: |
186 | verification_failed: 'Fail' | |
451 | verification_failed: 'reCAPTCHA was incorrect, please try again.' | |
452 | recaptcha_unreachable: 'reCAPTCHA verification server error, please try again.' | |
187 | 453 | ``` |
188 | 454 | |
189 | 455 | ## Testing |
190 | 456 | |
191 | 457 | By default, reCAPTCHA is skipped in "test" and "cucumber" env. To enable it during test: |
192 | 458 | |
193 | ```Ruby | |
459 | ```ruby | |
194 | 460 | Recaptcha.configuration.skip_verify_env.delete("test") |
195 | 461 | ``` |
196 | 462 | |
198 | 464 | |
199 | 465 | ### Recaptcha.configure |
200 | 466 | |
201 | ```Ruby | |
467 | ```ruby | |
202 | 468 | # config/initializers/recaptcha.rb |
203 | 469 | Recaptcha.configure do |config| |
204 | 470 | config.site_key = '6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy' |
212 | 478 | |
213 | 479 | For temporary overwrites (not thread safe). |
214 | 480 | |
215 | ```Ruby | |
481 | ```ruby | |
216 | 482 | Recaptcha.with_configuration(site_key: '12345') do |
217 | 483 | # Do stuff with the overwritten site_key. |
218 | 484 | end |
222 | 488 | |
223 | 489 | Pass in keys as options at runtime, for code base with multiple reCAPTCHA setups: |
224 | 490 | |
225 | ```Ruby | |
491 | ```ruby | |
226 | 492 | recaptcha_tags site_key: '6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy' |
227 | 493 | |
228 | 494 | # and |
234 | 500 | - Check out the [wiki](https://github.com/ambethia/recaptcha/wiki) and leave whatever you found valuable there. |
235 | 501 | - [Add multiple widgets to the same page](https://github.com/ambethia/recaptcha/wiki/Add-multiple-widgets-to-the-same-page) |
236 | 502 | - [Use Recaptcha with Devise](https://github.com/plataformatec/devise/wiki/How-To:-Use-Recaptcha-with-Devise) |
503 |
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 | |
2 | 3 | RECAPTCHA_SITE_KEY=6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI |
3 | 4 | RECAPTCHA_SECRET_KEY=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe |
0 | 0 | source 'https://rubygems.org' |
1 | 1 | |
2 | 2 | gem 'dotenv-rails', groups: [:development, :test] |
3 | gem 'rails', '4.2.5' | |
3 | gem 'rails', '~> 5.2' | |
4 | 4 | gem 'sqlite3' |
5 | 5 | gem 'recaptcha', require: 'recaptcha/rails', path: '../..' |
6 | ||
7 | group :development do | |
8 | gem 'listen' | |
9 | gem 'byebug' | |
10 | end |
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 | 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 | <% 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 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-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] %> |
0 | 0 | #!/usr/bin/env ruby |
1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) | |
1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) | |
2 | 2 | load Gem.bin_path('bundler', 'bundle') |
0 | 0 | #!/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__) | |
7 | 2 | require_relative '../config/boot' |
8 | 3 | require 'rails/commands' |
0 | 0 | #!/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 | 1 | require_relative '../config/boot' |
7 | 2 | require 'rake' |
8 | 3 | Rake.application.run |
0 | 0 | #!/usr/bin/env ruby |
1 | require 'pathname' | |
1 | require 'fileutils' | |
2 | include FileUtils | |
2 | 3 | |
3 | 4 | # path to your application root. |
4 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) | |
5 | APP_ROOT = File.expand_path('..', __dir__) | |
5 | 6 | |
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 | |
7 | 12 | # 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. | |
9 | 14 | |
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') | |
13 | 21 | |
14 | 22 | # 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' | |
17 | 25 | # end |
18 | 26 | |
19 | 27 | puts "\n== Preparing database ==" |
20 | system "bin/rake db:setup" | |
28 | system! 'bin/rails db:setup' | |
21 | 29 | |
22 | 30 | 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' | |
25 | 32 | |
26 | 33 | puts "\n== Restarting application server ==" |
27 | system "touch tmp/restart.txt" | |
34 | system! 'bin/rails restart' | |
28 | 35 | 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' | |
1 | 1 | |
2 | 2 | require 'rails/all' |
3 | 3 | |
7 | 7 | |
8 | 8 | module Foo |
9 | 9 | class Application < Rails::Application |
10 | # Initialize configuration defaults for originally generated Rails version. | |
11 | config.load_defaults 5.0 | |
12 | ||
10 | 13 | # 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. | |
24 | 17 | end |
25 | 18 | end |
0 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) | |
0 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) | |
1 | 1 | |
2 | 2 | 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 |
0 | 0 | # Load the Rails application. |
1 | require File.expand_path('../application', __FILE__) | |
1 | require_relative 'application' | |
2 | 2 | |
3 | 3 | # Initialize the Rails application. |
4 | 4 | Rails.application.initialize! |
8 | 8 | # Do not eager load code on boot. |
9 | 9 | config.eager_load = false |
10 | 10 | |
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 | |
14 | 31 | |
15 | 32 | # Don't care if the mailer can't send. |
16 | 33 | config.action_mailer.raise_delivery_errors = false |
34 | ||
35 | config.action_mailer.perform_caching = false | |
17 | 36 | |
18 | 37 | # Print deprecation notices to the Rails logger. |
19 | 38 | config.active_support.deprecation = :log |
21 | 40 | # Raise an error on page load if there are pending migrations. |
22 | 41 | config.active_record.migration_error = :page_load |
23 | 42 | |
43 | # Highlight code that triggered database queries in logs. | |
44 | config.active_record.verbose_query_logs = true | |
45 | ||
24 | 46 | # Debug mode disables concatenation and preprocessing of assets. |
25 | 47 | # This option may cause significant delays in view rendering with a large |
26 | 48 | # number of complex assets. |
27 | 49 | config.assets.debug = true |
28 | 50 | |
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 | |
37 | 53 | |
38 | 54 | # Raises error for missing translations |
39 | 55 | # 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 | |
40 | 60 | 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 |
2 | 2 | # Version of your assets, change this if you want to expire all your assets. |
3 | 3 | Rails.application.config.assets.version = '1.0' |
4 | 4 | |
5 | # Add additional assets to the asset load path | |
5 | # Add additional assets to the asset load path. | |
6 | 6 | # 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') | |
7 | 9 | |
8 | 10 | # 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 |
0 | 0 | # Be sure to restart your server when you modify this file. |
1 | 1 | |
2 | # Specify a serializer for the signed and encrypted cookie jars. | |
3 | # Valid options are :json, :marshal, and :hybrid. | |
2 | 4 | 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 |
4 | 4 | |
5 | 5 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. |
6 | 6 | ActiveSupport.on_load(:action_controller) do |
7 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) | |
7 | wrap_parameters format: [:json] | |
8 | 8 | end |
9 | 9 | |
10 | 10 | # To enable root element in JSON for ActiveRecord objects. |
11 | 11 | # ActiveSupport.on_load(:active_record) do |
12 | # self.include_root_in_json = true | |
12 | # self.include_root_in_json = true | |
13 | 13 | # end |
15 | 15 | # |
16 | 16 | # This would use the information in config/locales/es.yml. |
17 | 17 | # |
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 | # | |
18 | 28 | # To learn more, please read the Rails Internationalization guide |
19 | 29 | # available at http://guides.rubyonrails.org/i18n.html. |
20 | 30 |
0 | 0 | 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 | |
3 | 9 | resources :users |
4 | 10 | 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] | |
1 | 1 | def change |
2 | 2 | create_table :users do |t| |
3 | 3 | t.string :name |
0 | # encoding: UTF-8 | |
1 | 0 | # This file is auto-generated from the current state of the database. Instead |
2 | 1 | # of editing this file, please use the migrations feature of Active Record to |
3 | 2 | # incrementally modify your database, and then regenerate this schema definition. |
10 | 9 | # |
11 | 10 | # It's strongly recommended that you check this file into your version control system. |
12 | 11 | |
13 | ActiveRecord::Schema.define(version: 20151226015155) do | |
12 | ActiveRecord::Schema.define(version: 2015_12_26_015155) do | |
14 | 13 | |
15 | 14 | create_table "users", force: :cascade do |t| |
16 | 15 | t.string "name" |
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 |
7 | 7 | config.secret_key = '6Le7oRETAAAAAL5a8yOmEdmDi3b2pH7mq5iH1bYK' |
8 | 8 | end |
9 | 9 | |
10 | include Recaptcha::ClientHelper | |
11 | include Recaptcha::Verify | |
10 | include Recaptcha::Adapters::ControllerMethods | |
11 | include Recaptcha::Adapters::ViewMethods | |
12 | 12 | |
13 | 13 | get '/' do |
14 | 14 | <<-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 | Recaptcha.verify_via_api_call(recaptcha_response, options) | |
27 | end | |
28 | ||
29 | if verified | |
30 | flash.delete(:recaptcha_error) if recaptcha_flash_supported? && !model | |
31 | true | |
32 | else | |
33 | recaptcha_error( | |
34 | model, | |
35 | attribute, | |
36 | options.fetch(:message) { Recaptcha::Helpers.to_error_message(:verification_failed) } | |
37 | ) | |
38 | false | |
39 | end | |
40 | rescue Timeout::Error | |
41 | if Recaptcha.configuration.handle_timeouts_gracefully | |
42 | recaptcha_error( | |
43 | model, | |
44 | attribute, | |
45 | options.fetch(:message) { Recaptcha::Helpers.to_error_message(:recaptcha_unreachable) } | |
46 | ) | |
47 | false | |
48 | else | |
49 | raise RecaptchaError, 'Recaptcha unreachable.' | |
50 | end | |
51 | rescue StandardError => e | |
52 | raise RecaptchaError, e.message, e.backtrace | |
53 | end | |
54 | end | |
55 | ||
56 | def verify_recaptcha!(options = {}) | |
57 | verify_recaptcha(options) || raise(VerifyError) | |
58 | end | |
59 | ||
60 | def recaptcha_error(model, attribute, message) | |
61 | if model | |
62 | model.errors.add(attribute, message) | |
63 | elsif recaptcha_flash_supported? | |
64 | flash[:recaptcha_error] = message | |
65 | end | |
66 | end | |
67 | ||
68 | def recaptcha_flash_supported? | |
69 | request.respond_to?(:format) && request.format == :html && respond_to?(:flash) | |
70 | end | |
71 | ||
72 | # Extracts response token from params. params['g-recaptcha-response'] should either be a | |
73 | # string or a hash with the action name(s) as keys. If it is a hash, then `action` is used as | |
74 | # the key. | |
75 | # @return [String] A response token if one was passed in the params; otherwise, `''` | |
76 | def recaptcha_response_token(action = nil) | |
77 | response_param = params['g-recaptcha-response'] | |
78 | if response_param&.respond_to?(:to_h) # Includes ActionController::Parameters | |
79 | response_param[action].to_s | |
80 | else | |
81 | response_param.to_s | |
82 | end | |
83 | end | |
84 | end | |
85 | end | |
86 | 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 | # 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 |
29 | 29 | # end |
30 | 30 | # |
31 | 31 | class Configuration |
32 | attr_accessor :skip_verify_env, :secret_key, :site_key, :proxy, :handle_timeouts_gracefully, :hostname | |
32 | DEFAULTS = { | |
33 | 'server_url' => 'https://www.recaptcha.net/recaptcha/api.js', | |
34 | 'verify_url' => 'https://www.recaptcha.net/recaptcha/api/siteverify' | |
35 | }.freeze | |
36 | ||
37 | attr_accessor :default_env, :skip_verify_env, :secret_key, :site_key, :proxy, :handle_timeouts_gracefully, :hostname | |
33 | 38 | attr_writer :api_server_url, :verify_url |
34 | 39 | |
35 | 40 | def initialize #:nodoc: |
41 | @default_env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || (Rails.env if defined? Rails.env) | |
36 | 42 | @skip_verify_env = %w[test cucumber] |
37 | @handle_timeouts_gracefully = HANDLE_TIMEOUTS_GRACEFULLY | |
43 | @handle_timeouts_gracefully = true | |
38 | 44 | |
39 | 45 | @secret_key = ENV['RECAPTCHA_SECRET_KEY'] |
40 | 46 | @site_key = ENV['RECAPTCHA_SITE_KEY'] |
51 | 57 | end |
52 | 58 | |
53 | 59 | def api_server_url |
54 | @api_server_url || CONFIG.fetch('server_url') | |
60 | @api_server_url || DEFAULTS.fetch('server_url') | |
55 | 61 | end |
56 | 62 | |
57 | 63 | def verify_url |
58 | @verify_url || CONFIG.fetch('verify_url') | |
64 | @verify_url || DEFAULTS.fetch('verify_url') | |
59 | 65 | end |
60 | 66 | end |
61 | 67 | 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-" + dasherize_action(action) | |
13 | name = options.delete(:name) || "g-recaptcha-response[#{action}]" | |
14 | options[:render] = site_key | |
15 | options[:script_async] ||= false | |
16 | options[:script_defer] ||= false | |
17 | element = options.delete(:element) | |
18 | element = element == false ? false : :input | |
19 | if element == :input | |
20 | callback = options.delete(:callback) || recaptcha_v3_default_callback_name(action) | |
21 | end | |
22 | options[:class] = "g-recaptcha-response #{options[:class]}" | |
23 | ||
24 | html, tag_attributes = components(options) | |
25 | if recaptcha_v3_inline_script?(options) | |
26 | html << recaptcha_v3_inline_script(site_key, action, callback, id, options) | |
27 | end | |
28 | case element | |
29 | when :input | |
30 | html << %(<input type="hidden" name="#{name}" id="#{id}" #{tag_attributes}/>\n) | |
31 | when false | |
32 | # No tag | |
33 | nil | |
34 | else | |
35 | raise(RecaptchaError, "ReCAPTCHA element `#{options[:element]}` is not valid.") | |
36 | end | |
37 | html.respond_to?(:html_safe) ? html.html_safe : html | |
38 | end | |
39 | ||
40 | def self.recaptcha_tags(options) | |
41 | if options.key?(:stoken) | |
42 | raise(RecaptchaError, "Secure Token is deprecated. Please remove 'stoken' from your calls to recaptcha_tags.") | |
43 | end | |
44 | if options.key?(:ssl) | |
45 | raise(RecaptchaError, "SSL is now always true. Please remove 'ssl' from your calls to recaptcha_tags.") | |
46 | end | |
47 | ||
48 | noscript = options.delete(:noscript) | |
49 | ||
50 | html, tag_attributes, fallback_uri = components(options.dup) | |
51 | html << %(<div #{tag_attributes}></div>\n) | |
52 | ||
53 | if noscript != false | |
54 | html << <<-HTML | |
55 | <noscript> | |
56 | <div> | |
57 | <div style="width: 302px; height: 422px; position: relative;"> | |
58 | <div style="width: 302px; height: 422px; position: absolute;"> | |
59 | <iframe | |
60 | src="#{fallback_uri}" | |
61 | name="ReCAPTCHA" | |
62 | style="width: 302px; height: 422px; border-style: none; border: 0; overflow: hidden;"> | |
63 | </iframe> | |
64 | </div> | |
65 | </div> | |
66 | <div style="width: 300px; height: 60px; border-style: none; | |
67 | bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px; | |
68 | background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;"> | |
69 | <textarea id="g-recaptcha-response" name="g-recaptcha-response" | |
70 | class="g-recaptcha-response" | |
71 | style="width: 250px; height: 40px; border: 1px solid #c1c1c1; | |
72 | margin: 10px 25px; padding: 0px; resize: none;"> | |
73 | </textarea> | |
74 | </div> | |
75 | </div> | |
76 | </noscript> | |
77 | HTML | |
78 | end | |
79 | ||
80 | html.respond_to?(:html_safe) ? html.html_safe : html | |
81 | end | |
82 | ||
83 | def self.invisible_recaptcha_tags(custom) | |
84 | options = {callback: 'invisibleRecaptchaSubmit', ui: :button}.merge(custom) | |
85 | text = options.delete(:text) | |
86 | html, tag_attributes = components(options.dup) | |
87 | html << default_callback(options) if default_callback_required?(options) | |
88 | ||
89 | case options[:ui] | |
90 | when :button | |
91 | html << %(<button type="submit" #{tag_attributes}>#{text}</button>\n) | |
92 | when :invisible | |
93 | html << %(<div data-size="invisible" #{tag_attributes}></div>\n) | |
94 | when :input | |
95 | html << %(<input type="submit" #{tag_attributes} value="#{text}"/>\n) | |
96 | else | |
97 | raise(RecaptchaError, "ReCAPTCHA ui `#{options[:ui]}` is not valid.") | |
98 | end | |
99 | html.respond_to?(:html_safe) ? html.html_safe : html | |
100 | end | |
101 | ||
102 | def self.to_error_message(key) | |
103 | default = DEFAULT_ERRORS.fetch(key) { raise ArgumentError "Unknown reCAPTCHA error - #{key}" } | |
104 | to_message("recaptcha.errors.#{key}", default) | |
105 | end | |
106 | ||
107 | if defined?(I18n) | |
108 | def self.to_message(key, default) | |
109 | I18n.translate(key, default: default) | |
110 | end | |
111 | else | |
112 | def self.to_message(_key, default) | |
113 | default | |
114 | end | |
115 | end | |
116 | ||
117 | private_class_method def self.components(options) | |
118 | html = +'' | |
119 | attributes = {} | |
120 | fallback_uri = +'' | |
121 | ||
122 | options = options.dup | |
123 | env = options.delete(:env) | |
124 | class_attribute = options.delete(:class) | |
125 | site_key = options.delete(:site_key) | |
126 | hl = options.delete(:hl) | |
127 | onload = options.delete(:onload) | |
128 | render = options.delete(:render) | |
129 | script_async = options.delete(:script_async) | |
130 | script_defer = options.delete(:script_defer) | |
131 | nonce = options.delete(:nonce) | |
132 | skip_script = (options.delete(:script) == false) || (options.delete(:external_script) == false) | |
133 | ui = options.delete(:ui) | |
134 | ||
135 | data_attribute_keys = [:badge, :theme, :type, :callback, :expired_callback, :error_callback, :size] | |
136 | data_attribute_keys << :tabindex unless ui == :button | |
137 | data_attributes = {} | |
138 | data_attribute_keys.each do |data_attribute| | |
139 | value = options.delete(data_attribute) | |
140 | data_attributes["data-#{data_attribute.to_s.tr('_', '-')}"] = value if value | |
141 | end | |
142 | ||
143 | unless Recaptcha.skip_env?(env) | |
144 | site_key ||= Recaptcha.configuration.site_key! | |
145 | script_url = Recaptcha.configuration.api_server_url | |
146 | query_params = hash_to_query( | |
147 | hl: hl, | |
148 | onload: onload, | |
149 | render: render | |
150 | ) | |
151 | script_url += "?#{query_params}" unless query_params.empty? | |
152 | async_attr = "async" if script_async != false | |
153 | defer_attr = "defer" if script_defer != false | |
154 | nonce_attr = " nonce='#{nonce}'" if nonce | |
155 | html << %(<script src="#{script_url}" #{async_attr} #{defer_attr} #{nonce_attr}></script>\n) unless skip_script | |
156 | fallback_uri = %(#{script_url.chomp(".js")}/fallback?k=#{site_key}) | |
157 | attributes["data-sitekey"] = site_key | |
158 | attributes.merge! data_attributes | |
159 | end | |
160 | ||
161 | # The remaining options will be added as attributes on the tag. | |
162 | attributes["class"] = "g-recaptcha #{class_attribute}" | |
163 | tag_attributes = attributes.merge(options).map { |k, v| %(#{k}="#{v}") }.join(" ") | |
164 | ||
165 | [html, tag_attributes, fallback_uri] | |
166 | end | |
167 | ||
168 | # v3 | |
169 | ||
170 | # Renders a script that calls `grecaptcha.execute` for the given `site_key` and `action` and | |
171 | # calls the `callback` with the resulting response token. | |
172 | private_class_method def self.recaptcha_v3_inline_script(site_key, action, callback, id, options = {}) | |
173 | nonce = options[:nonce] | |
174 | nonce_attr = " nonce='#{nonce}'" if nonce | |
175 | ||
176 | <<-HTML | |
177 | <script#{nonce_attr}> | |
178 | // Define function so that we can call it again later if we need to reset it | |
179 | // This executes reCAPTCHA and then calls our callback. | |
180 | function #{recaptcha_v3_execute_function_name(action)}() { | |
181 | grecaptcha.ready(function() { | |
182 | grecaptcha.execute('#{site_key}', {action: '#{action}'}).then(function(token) { | |
183 | //console.log('#{id}', token) | |
184 | #{callback}('#{id}', token) | |
185 | }); | |
186 | }); | |
187 | }; | |
188 | // Invoke immediately | |
189 | #{recaptcha_v3_execute_function_name(action)}() | |
190 | ||
191 | // Async variant so you can await this function from another async function (no need for | |
192 | // an explicit callback function then!) | |
193 | // Returns a Promise that resolves with the response token. | |
194 | async function #{recaptcha_v3_async_execute_function_name(action)}() { | |
195 | return new Promise((resolve, reject) => { | |
196 | grecaptcha.ready(async function() { | |
197 | resolve(await grecaptcha.execute('#{site_key}', {action: '#{action}'})) | |
198 | }); | |
199 | }) | |
200 | }; | |
201 | ||
202 | #{recaptcha_v3_define_default_callback(callback) if recaptcha_v3_define_default_callback?(callback, action, options)} | |
203 | </script> | |
204 | HTML | |
205 | end | |
206 | ||
207 | private_class_method def self.recaptcha_v3_inline_script?(options) | |
208 | !Recaptcha.skip_env?(options[:env]) && | |
209 | options[:script] != false && | |
210 | options[:inline_script] != false | |
211 | end | |
212 | ||
213 | private_class_method def self.recaptcha_v3_define_default_callback(callback) | |
214 | <<-HTML | |
215 | var #{callback} = function(id, token) { | |
216 | var element = document.getElementById(id); | |
217 | element.value = token; | |
218 | } | |
219 | HTML | |
220 | end | |
221 | ||
222 | # Returns true if we should be adding the default callback. | |
223 | # That is, if the given callback name is the default callback name (for the given action) and we | |
224 | # are not skipping inline scripts for any reason. | |
225 | private_class_method def self.recaptcha_v3_define_default_callback?(callback, action, options) | |
226 | callback == recaptcha_v3_default_callback_name(action) && | |
227 | recaptcha_v3_inline_script?(options) | |
228 | end | |
229 | ||
230 | # Returns the name of the JavaScript function that actually executes the reCAPTCHA code (calls | |
231 | # grecaptcha.execute). You can call it again later to reset it. | |
232 | def self.recaptcha_v3_execute_function_name(action) | |
233 | "executeRecaptchaFor#{sanitize_action_for_js(action)}" | |
234 | end | |
235 | ||
236 | # Returns the name of an async JavaScript function that executes the reCAPTCHA code. | |
237 | def self.recaptcha_v3_async_execute_function_name(action) | |
238 | "#{recaptcha_v3_execute_function_name(action)}Async" | |
239 | end | |
240 | ||
241 | def self.recaptcha_v3_default_callback_name(action) | |
242 | "setInputWithRecaptchaResponseTokenFor#{sanitize_action_for_js(action)}" | |
243 | end | |
244 | ||
245 | # v2 | |
246 | ||
247 | private_class_method def self.default_callback(options = {}) | |
248 | nonce = options[:nonce] | |
249 | nonce_attr = " nonce='#{nonce}'" if nonce | |
250 | ||
251 | <<-HTML | |
252 | <script#{nonce_attr}> | |
253 | var invisibleRecaptchaSubmit = function () { | |
254 | var closestForm = function (ele) { | |
255 | var curEle = ele.parentNode; | |
256 | while (curEle.nodeName !== 'FORM' && curEle.nodeName !== 'BODY'){ | |
257 | curEle = curEle.parentNode; | |
258 | } | |
259 | return curEle.nodeName === 'FORM' ? curEle : null | |
260 | }; | |
261 | ||
262 | var eles = document.getElementsByClassName('g-recaptcha'); | |
263 | if (eles.length > 0) { | |
264 | var form = closestForm(eles[0]); | |
265 | if (form) { | |
266 | form.submit(); | |
267 | } | |
268 | } | |
269 | }; | |
270 | </script> | |
271 | HTML | |
272 | end | |
273 | ||
274 | private_class_method def self.default_callback_required?(options) | |
275 | options[:callback] == 'invisibleRecaptchaSubmit' && | |
276 | !Recaptcha.skip_env?(options[:env]) && | |
277 | options[:script] != false && | |
278 | options[:inline_script] != false | |
279 | end | |
280 | ||
281 | # Returns a camelized string that is safe for use in a JavaScript variable/function name. | |
282 | # sanitize_action_for_js('my/action') => 'MyAction' | |
283 | private_class_method def self.sanitize_action_for_js(action) | |
284 | action.to_s.gsub(/\W/, '_').split(/\/|_/).map(&:capitalize).join | |
285 | end | |
286 | ||
287 | # Returns a dasherized string that is safe for use as an HTML ID | |
288 | # dasherize_action('my/action') => 'my-action' | |
289 | private_class_method def self.dasherize_action(action) | |
290 | action.to_s.gsub(/\W/, '-').tr('_', '-') | |
291 | end | |
292 | ||
293 | private_class_method def self.hash_to_query(hash) | |
294 | hash.delete_if { |_, val| val.nil? || val.empty? }.to_a.map { |pair| pair.join('=') }.join('&') | |
295 | end | |
296 | end | |
297 | end |
0 | 0 | # frozen_string_literal: true |
1 | ||
1 | 2 | # deprecated, but let's not blow everyone up |
2 | 3 | require 'recaptcha' |
2 | 2 | module Recaptcha |
3 | 3 | class Railtie < Rails::Railtie |
4 | 4 | ActiveSupport.on_load(:action_view) do |
5 | require 'recaptcha/client_helper' | |
6 | include Recaptcha::ClientHelper | |
5 | include Recaptcha::Adapters::ViewMethods | |
7 | 6 | end |
8 | 7 | |
9 | 8 | 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 | |
12 | 32 | end |
13 | 33 | end |
14 | 34 | end |
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 |
0 | 0 | # frozen_string_literal: true |
1 | 1 | |
2 | 2 | module Recaptcha |
3 | VERSION = '4.11.1' | |
3 | VERSION = '5.2.1' | |
4 | 4 | end |
0 | 0 | # frozen_string_literal: true |
1 | 1 | |
2 | require 'json' | |
3 | require 'net/http' | |
4 | require 'uri' | |
5 | ||
2 | 6 | 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' | |
6 | 10 | if defined?(Rails) |
7 | 11 | require 'recaptcha/railtie' |
8 | else | |
9 | require 'recaptcha/client_helper' | |
10 | require 'recaptcha/verify' | |
11 | 12 | end |
12 | 13 | |
13 | 14 | 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 | |
18 | 17 | |
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 | |
22 | 23 | |
23 | 24 | # Gives access to the current Configuration. |
24 | 25 | def self.configuration |
49 | 50 | original_config.each { |key, value| configuration.send("#{key}=", value) } |
50 | 51 | end |
51 | 52 | |
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 | secret_key = options.fetch(:secret_key) { configuration.secret_key! } | |
63 | verify_hash = { 'secret' => secret_key, 'response' => response } | |
64 | verify_hash['remoteip'] = options[:remote_ip] if options.key?(:remote_ip) | |
65 | ||
66 | reply = api_verification(verify_hash, timeout: options[:timeout]) | |
67 | reply['success'].to_s == 'true' && | |
68 | hostname_valid?(reply['hostname'], options[:hostname]) && | |
69 | action_valid?(reply['action'], options[:action]) && | |
70 | score_above_threshold?(reply['score'], options[:minimum_score]) | |
71 | end | |
72 | ||
73 | def self.hostname_valid?(hostname, validation) | |
74 | validation ||= configuration.hostname | |
75 | ||
76 | case validation | |
77 | when nil, FalseClass then true | |
78 | when String then validation == hostname | |
79 | else validation.call(hostname) | |
80 | end | |
81 | end | |
82 | ||
83 | def self.action_valid?(action, expected_action) | |
84 | case expected_action | |
85 | when nil, FalseClass then true | |
86 | else action == expected_action | |
87 | end | |
88 | end | |
89 | ||
90 | # Returns true iff score is greater or equal to (>=) minimum_score, or if no minimum_score was specified | |
91 | def self.score_above_threshold?(score, minimum_score) | |
92 | return true if minimum_score.nil? | |
93 | return false if score.nil? | |
94 | ||
95 | case minimum_score | |
96 | when nil, FalseClass then true | |
97 | else score >= minimum_score | |
98 | end | |
99 | end | |
100 | ||
101 | def self.api_verification(verify_hash, timeout: nil) | |
102 | timeout ||= DEFAULT_TIMEOUT | |
103 | http = if configuration.proxy | |
104 | proxy_server = URI.parse(configuration.proxy) | |
55 | 105 | Net::HTTP::Proxy(proxy_server.host, proxy_server.port, proxy_server.user, proxy_server.password) |
56 | 106 | else |
57 | 107 | Net::HTTP |
58 | 108 | end |
59 | 109 | query = URI.encode_www_form(verify_hash) |
60 | uri = URI.parse(Recaptcha.configuration.verify_url + '?' + query) | |
110 | uri = URI.parse(configuration.verify_url + '?' + query) | |
61 | 111 | http_instance = http.new(uri.host, uri.port) |
62 | http_instance.read_timeout = http_instance.open_timeout = options[:timeout] || DEFAULT_TIMEOUT | |
112 | http_instance.read_timeout = http_instance.open_timeout = timeout | |
63 | 113 | http_instance.use_ssl = true if uri.port == 443 |
64 | 114 | request = Net::HTTP::Get.new(uri.request_uri) |
65 | http_instance.request(request).body | |
66 | end | |
67 | ||
68 | def self.i18n(key, default) | |
69 | if defined?(I18n) | |
70 | I18n.translate(key, default: default) | |
71 | else | |
72 | default | |
73 | end | |
74 | end | |
75 | ||
76 | class RecaptchaError < StandardError | |
77 | end | |
78 | ||
79 | class VerifyError < RecaptchaError | |
115 | JSON.parse(http_instance.request(request).body) | |
80 | 116 | end |
81 | 117 | 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. |
11 | 11 | s.license = "MIT" |
12 | 12 | s.required_ruby_version = '>= 2.3.0' |
13 | 13 | |
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") | |
15 | 15 | |
16 | 16 | s.add_runtime_dependency "json" |
17 | 17 | s.add_development_dependency "mocha" |
18 | 18 | s.add_development_dependency "rake" |
19 | s.add_development_dependency "activesupport" | |
20 | 19 | s.add_development_dependency "i18n" |
21 | 20 | s.add_development_dependency "maxitest" |
22 | 21 | s.add_development_dependency "pry-byebug" |
23 | 22 | s.add_development_dependency "bump" |
24 | 23 | s.add_development_dependency "webmock" |
25 | 24 | s.add_development_dependency "rubocop" |
25 | ||
26 | s.metadata = { "source_code_uri" => "https://github.com/ambethia/recaptcha" } | |
26 | 27 | end |
0 | 0 | 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 | |
6 | 4 | |
7 | 5 | it "uses ssl" do |
8 | 6 | recaptcha_tags.must_include "\"#{Recaptcha.configuration.api_server_url}\"" |
57 | 55 | |
58 | 56 | it "adds :hl option to the url" do |
59 | 57 | html = recaptcha_tags(hl: 'en') |
60 | html.must_include("?hl=en") | |
58 | html.must_include("hl=en") | |
61 | 59 | |
62 | 60 | 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") | |
65 | 63 | |
66 | 64 | 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 | it "adds :render option to the url" do | |
81 | html = recaptcha_tags(render: 'onload') | |
82 | html.must_include("render=onload") | |
83 | ||
84 | html = recaptcha_tags(render: 'explicit') | |
85 | html.wont_include("render=onload") | |
86 | html.must_include("render=explicit") | |
87 | ||
88 | html = recaptcha_tags | |
89 | html.wont_include("render=") | |
90 | end | |
91 | ||
92 | it "adds query params to the url" do | |
93 | html = recaptcha_tags(hl: 'en', onload: 'foobar') | |
94 | html.must_include("?") | |
95 | html.must_include("hl=en") | |
96 | html.must_include("&") | |
97 | html.must_include("onload=foobar") | |
68 | 98 | end |
69 | 99 | |
70 | 100 | it "includes the site key in the button attributes" do |
71 | 101 | html = invisible_recaptcha_tags |
72 | 102 | html.must_include(" data-sitekey=\"#{Recaptcha.configuration.site_key}\"") |
103 | end | |
104 | ||
105 | it "lets you override the site_key from configuration via options hash" do | |
106 | html = invisible_recaptcha_tags(site_key: 'different_key') | |
107 | html.must_include(" data-sitekey=\"different_key\"") | |
73 | 108 | end |
74 | 109 | |
75 | 110 | it "dasherizes the expired_callback attribute name" do |
153 | 188 | html.wont_include("<button") |
154 | 189 | end |
155 | 190 | |
191 | it "renders an input element with supplied text if UI is input" do | |
192 | html = invisible_recaptcha_tags(ui: :input, text: 'Send') | |
193 | html.must_include("<input type=\"submit\"") | |
194 | html.must_include("value=\"Send\"/>") | |
195 | html.wont_include("<button") | |
196 | end | |
197 | ||
156 | 198 | it "raises an error on an invalid ui option" do |
157 | 199 | assert_raises Recaptcha::RecaptchaError do |
158 | 200 | invisible_recaptcha_tags(ui: :foo) |
159 | 201 | end |
160 | 202 | end |
161 | 203 | end |
204 | ||
205 | describe "v3 recaptcha" do | |
206 | it "renders input" do | |
207 | html = recaptcha_v3 action: :foo | |
208 | html.must_include('<input type="hidden" name="g-recaptcha-response[foo]" id="g-recaptcha-response-foo" data-sitekey="0000000000000000000000000000000000000000" class="g-recaptcha g-recaptcha-response "/>') | |
209 | end | |
210 | ||
211 | it "does not have obsole closing script tag" do | |
212 | html = recaptcha_v3 action: :foo | |
213 | assert html.scan(/script/).length.even? | |
214 | end | |
215 | end | |
162 | 216 | end |
2 | 2 | describe Recaptcha::Configuration do |
3 | 3 | describe "#api_server_url" do |
4 | 4 | it "serves the default" do |
5 | Recaptcha.configuration.api_server_url.must_equal "https://www.google.com/recaptcha/api.js" | |
5 | Recaptcha.configuration.api_server_url.must_equal "https://www.recaptcha.net/recaptcha/api.js" | |
6 | 6 | end |
7 | 7 | |
8 | 8 | describe "when api_server_url is overwritten" do |
17 | 17 | |
18 | 18 | describe "#verify_url" do |
19 | 19 | it "serves the default" do |
20 | Recaptcha.configuration.verify_url.must_equal "https://www.google.com/recaptcha/api/siteverify" | |
20 | Recaptcha.configuration.verify_url.must_equal "https://www.recaptcha.net/recaptcha/api/siteverify" | |
21 | 21 | end |
22 | 22 | |
23 | 23 | describe "when api_server_url is overwritten" do |
0 | # set default_env to nil | |
1 | ENV.delete('RAILS_ENV') | |
2 | ENV.delete('RACK_ENV') | |
3 | ||
0 | 4 | require 'bundler/setup' |
1 | 5 | require 'maxitest/autorun' |
2 | 6 | require 'mocha/setup' |
3 | 7 | require 'webmock/minitest' |
8 | require 'byebug' | |
4 | 9 | require 'cgi' |
10 | require 'i18n' | |
5 | 11 | require 'recaptcha' |
6 | require 'i18n' | |
7 | ||
8 | ENV.delete('RAILS_ENV') | |
9 | ENV.delete('RACK_ENV') | |
10 | 12 | |
11 | 13 | I18n.enforce_available_locales = false |
12 | 14 |
0 | 0 | require_relative 'helper' |
1 | 1 | |
2 | describe Recaptcha::Verify do | |
2 | describe 'controller helpers' do | |
3 | 3 | before do |
4 | 4 | @controller = TestController.new |
5 | 5 | @controller.request = stub(remote_ip: "1.1.1.1", format: :html) |
78 | 78 | secret_key = Recaptcha.configuration.secret_key |
79 | 79 | stub_request( |
80 | 80 | :get, |
81 | "https://www.google.com/recaptcha/api/siteverify?response=string&secret=#{secret_key}" | |
81 | "https://www.recaptcha.net/recaptcha/api/siteverify?response=string&secret=#{secret_key}" | |
82 | 82 | ).to_return(body: '{"success":true}') |
83 | 83 | |
84 | 84 | assert @controller.verify_recaptcha(skip_remote_ip: true) |
171 | 171 | assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error] |
172 | 172 | end |
173 | 173 | |
174 | it "does not verify via http call when response length exceeds G_RESPONSE_LIMIT" do | |
175 | # this returns a 400 or 413 instead of a 200 response with error code | |
176 | # typical response length is less than 400 characters | |
177 | str = "a" * 4001 | |
178 | @controller.params = { 'g-recaptcha-response' => "#{str}"} | |
179 | assert_not_requested :get, %r{\.google\.com} | |
180 | assert_equal false, @controller.verify_recaptcha | |
181 | assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error] | |
182 | end | |
183 | ||
174 | 184 | describe ':hostname' do |
175 | 185 | let(:hostname) { 'fake.hostname.com' } |
176 | 186 | |
239 | 249 | end |
240 | 250 | end |
241 | 251 | end |
252 | ||
253 | describe 'action_valid?' do | |
254 | let(:default_response_hash) { { | |
255 | success: true, | |
256 | action: 'homepage', | |
257 | } } | |
258 | ||
259 | before do | |
260 | expect_http_post.to_return(body: success_body) | |
261 | end | |
262 | ||
263 | it "fails when action from response does not match expected action" do | |
264 | expect_http_post.to_return(body: success_body(action: "not_homepage")) | |
265 | ||
266 | refute verify_recaptcha(action: 'homepage') | |
267 | assert_flash_error | |
268 | end | |
269 | ||
270 | it "passes with string that matches" do | |
271 | assert verify_recaptcha(action: 'homepage') | |
272 | assert_nil @controller.flash[:recaptcha_error] | |
273 | end | |
274 | ||
275 | it "passes with nil" do | |
276 | assert verify_recaptcha(action: nil) | |
277 | assert_nil @controller.flash[:recaptcha_error] | |
278 | end | |
279 | ||
280 | it "passes with false" do | |
281 | assert verify_recaptcha(action: false) | |
282 | assert_nil @controller.flash[:recaptcha_error] | |
283 | end | |
284 | end | |
285 | ||
286 | describe 'score_above_threshold?' do | |
287 | let(:default_response_hash) { { | |
288 | success: true, | |
289 | action: 'homepage', | |
290 | } } | |
291 | ||
292 | before do | |
293 | expect_http_post.to_return(body: success_body(score: 0.4)) | |
294 | end | |
295 | ||
296 | it "fails when score is below minimum_score" do | |
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 | refute verify_recaptcha(minimum_score: 0.4) | |
304 | assert_flash_error | |
305 | end | |
306 | ||
307 | it "passes with score exactly at minimum_score" do | |
308 | assert verify_recaptcha(minimum_score: 0.4) | |
309 | assert_nil @controller.flash[:recaptcha_error] | |
310 | end | |
311 | ||
312 | it "passes when minimum_score not specified or nil" do | |
313 | assert verify_recaptcha() | |
314 | assert_nil @controller.flash[:recaptcha_error] | |
315 | end | |
316 | ||
317 | it "passes with false" do | |
318 | assert verify_recaptcha(minimum_score: false) | |
319 | assert_nil @controller.flash[:recaptcha_error] | |
320 | end | |
321 | end | |
242 | 322 | end |
243 | 323 | |
244 | 324 | private |
245 | 325 | |
246 | 326 | class TestController |
247 | include Recaptcha::Verify | |
327 | include Recaptcha::Adapters::ControllerMethods | |
328 | ||
248 | 329 | attr_accessor :request, :params, :flash |
249 | 330 | |
250 | 331 | def initialize |
251 | 332 | @flash = {} |
252 | 333 | end |
334 | ||
335 | public :verify_recaptcha | |
336 | public :verify_recaptcha! | |
253 | 337 | end |
254 | 338 | |
255 | 339 | def expect_http_post(secret_key: Recaptcha.configuration.secret_key) |
256 | 340 | stub_request( |
257 | 341 | :get, |
258 | "https://www.google.com/recaptcha/api/siteverify?remoteip=1.1.1.1&response=string&secret=#{secret_key}" | |
342 | "https://www.recaptcha.net/recaptcha/api/siteverify?remoteip=1.1.1.1&response=string&secret=#{secret_key}" | |
259 | 343 | ) |
260 | 344 | end |
345 | ||
346 | def success_body(other = {}) | |
347 | default_response_hash. | |
348 | merge(other). | |
349 | to_json | |
350 | end | |
351 | ||
352 | def error_body(error_code = "bad-news") | |
353 | { "error-codes" => [error_code] }. | |
354 | to_json | |
355 | end | |
356 | ||
357 | def verify_recaptcha(options = {}) | |
358 | options[:action] = 'homepage' unless options.key?(:action) | |
359 | @controller.verify_recaptcha(options) | |
360 | end | |
361 | ||
362 | def assert_flash_error | |
363 | assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error] | |
364 | end | |
261 | 365 | end |