Import upstream version 5.8.1
Debian Janitor
2 years ago
0 | Why and what is being done. | |
1 | ||
2 | ## Pre-Merge Checklist | |
3 | - [ ] CHANGELOG.md updated with short summary |
0 | name: CI | |
1 | ||
2 | on: | |
3 | pull_request: | |
4 | push: | |
5 | branches: | |
6 | - master | |
7 | ||
8 | jobs: | |
9 | test: | |
10 | name: Test ruby version matrix | |
11 | runs-on: ubuntu-latest | |
12 | strategy: | |
13 | matrix: | |
14 | ruby-version: ['2.4', '2.5', '2.6', '2.7', 'truffleruby-head'] | |
15 | steps: | |
16 | - uses: actions/checkout@v2 | |
17 | - uses: ruby/setup-ruby@v1 | |
18 | with: | |
19 | ruby-version: ${{ matrix.ruby-version }} | |
20 | bundler-cache: true | |
21 | - run: bundle exec rake test | |
22 | ||
23 | rubocop: | |
24 | name: Run rubocop | |
25 | runs-on: ubuntu-latest | |
26 | steps: | |
27 | - uses: actions/checkout@v2 | |
28 | - uses: ruby/setup-ruby@v1 | |
29 | with: | |
30 | ruby-version: '2.4' | |
31 | bundler-cache: true | |
32 | - run: bundle exec rake rubocop |
0 | 0 | AllCops: |
1 | TargetRubyVersion: 2.3 | |
1 | TargetRubyVersion: 2.4 | |
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: |
0 | language: ruby | |
1 | cache: bundler | |
2 | sudo: false | |
3 | rvm: | |
4 | - 2.3 | |
5 | - 2.4 | |
6 | - 2.5 | |
7 | env: | |
8 | - TASK=test | |
9 | matrix: | |
10 | include: | |
11 | - rvm: 2.3 # keep same as lowest ruby version | |
12 | env: TASK=rubocop | |
13 | script: bundle exec rake $TASK | |
14 | branches: | |
15 | only: master | |
16 | matrix: | |
17 | fast_finish: true | |
18 |
0 | ## Next | |
1 | * Gracefully handle invalid params | |
2 | ||
3 | ## 5.8.0 | |
4 | * Add support for the enterprise API | |
5 | ||
6 | ## 5.7.0 | |
7 | * french locale | |
8 | * drop ruby 2.3 | |
9 | ||
10 | ## 5.6.0 | |
11 | * Allow multiple invisible recaptchas on a single page by setting custom selector | |
12 | ||
13 | ## 5.5.0 | |
14 | * add `recaptcha_reply` controller method for better debugging/inspection | |
15 | ||
16 | ## 5.4.1 | |
17 | * fix v2 vs 'data' postfix | |
18 | ||
19 | ## 5.4.0 | |
20 | * added 'data' postfix to g-recaptcha-response attribute name to avoid collisions | |
21 | ||
22 | ## 5.3.0 | |
23 | * turbolinks support | |
24 | ||
25 | ## 5.2.0 | |
26 | * remove dependency on rails methods | |
27 | ||
28 | ## 5.1.0 | |
29 | * Added default translations for rails/i18n | |
30 | * use recaptcha.net for the script tag | |
31 | ||
32 | ## 5.0.0 | |
33 | * Changed host to Recaptcha.net | |
34 | * Add v3 API support | |
35 | * Renamed `Recaptcha::ClientHelper` to `Recaptcha::Adapters::ViewMethods` | |
36 | * Renamed `Recaptcha::Verify` to `Recaptcha::Adapters::ControllerMethods` | |
37 | ||
38 | ## 4.12.0 - 2018-08-30 | |
39 | * add `input` option to `invisible_recaptcha_tags`'s `ui` setting | |
40 | ||
41 | ## 4.11.1 - 2018-08-08 | |
42 | * leave `tabindex` attribute alone for `invisible_recaptcha_tags` | |
43 | ||
0 | 44 | ## 4.11.0 - 2018-08-06 |
1 | 45 | * prefer RAILS_ENV over RACK_ENV #286 |
2 | 46 | |
59 | 103 | * support disabling stoken |
60 | 104 | * support Rails.env |
61 | 105 | |
106 | ## 0.4.0 / 2015-03-22 | |
107 | ||
108 | * Add support for ReCaptcha v2 API | |
109 | * V2 API requires `g-recaptcha-response` parameters; https://github.com/ambethia/recaptcha/pull/114 | |
110 | ||
62 | 111 | ## 0.3.6 / 2012-01-07 |
63 | 112 | |
64 | 113 | * Many documentation changes |
0 | 0 | PATH |
1 | 1 | remote: . |
2 | 2 | specs: |
3 | recaptcha (4.11.1) | |
3 | recaptcha (5.8.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.5.1) | |
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.1.4 |
0 | ||
0 | 1 | # reCAPTCHA |
2 | [![Gem Version](https://badge.fury.io/rb/recaptcha.svg)](https://badge.fury.io/rb/recaptcha) | |
1 | 3 | |
2 | 4 | Author: Jason L Perry (http://ambethia.com)<br/> |
3 | 5 | Copyright: Copyright (c) 2007-2013 Jason L Perry<br/> |
5 | 7 | Info: https://github.com/ambethia/recaptcha<br/> |
6 | 8 | Bugs: https://github.com/ambethia/recaptcha/issues<br/> |
7 | 9 | |
8 | This plugin adds helpers for the [reCAPTCHA API](https://www.google.com/recaptcha). In your | |
9 | views you can use the `recaptcha_tags` method to embed the needed javascript, | |
10 | and you can validate in your controllers with `verify_recaptcha` or `verify_recaptcha!`, | |
11 | which throws an error on failiure. | |
10 | This gem provides helper methods for the [reCAPTCHA API](https://www.google.com/recaptcha). In your | |
11 | views you can use the `recaptcha_tags` method to embed the needed javascript, and you can validate | |
12 | in your controllers with `verify_recaptcha` or `verify_recaptcha!`, which raises an error on | |
13 | failure. | |
14 | ||
15 | ||
16 | # Table of Contents | |
17 | 1. [Obtaining a key](#obtaining-a-key) | |
18 | 2. [Rails Installation](#rails-installation) | |
19 | 3. [Sinatra / Rack / Ruby Installation](#sinatra--rack--ruby-installation) | |
20 | 4. [reCAPTCHA V2 API & Usage](#recaptcha-v2-api-and-usage) | |
21 | - [`recaptcha_tags`](#recaptcha_tags) | |
22 | - [`verify_recaptcha`](#verify_recaptcha) | |
23 | - [`invisible_recaptcha_tags`](#invisible_recaptcha_tags) | |
24 | 5. [reCAPTCHA V3 API & Usage](#recaptcha-v3-api-and-usage) | |
25 | - [`recaptcha_v3`](#recaptcha_v3) | |
26 | - [`verify_recaptcha` (use with v3)](#verify_recaptcha-use-with-v3) | |
27 | - [`recaptcha_reply`](#recaptcha_reply) | |
28 | 6. [I18n Support](#i18n-support) | |
29 | 7. [Testing](#testing) | |
30 | 8. [Alternative API Key Setup](#alternative-api-key-setup) | |
31 | ||
32 | ## Obtaining a key | |
33 | ||
34 | Go to the [reCAPTCHA admin console](https://www.google.com/recaptcha/admin) to obtain a reCAPTCHA API key. | |
35 | ||
36 | The reCAPTCHA type(s) that you choose for your key will determine which methods to use below. | |
37 | ||
38 | | reCAPTCHA type | Methods to use | Description | | |
39 | |----------------------------------------------|----------------|-------------| | |
40 | | v3 | [`recaptcha_v3`](#recaptcha_v3) | Verify requests with a [score](https://developers.google.com/recaptcha/docs/v3#score) | |
41 | | v2 Checkbox<br/>("I'm not a robot" Checkbox) | [`recaptcha_tags`](#recaptcha_tags) | Validate requests with the "I'm not a robot" checkbox | | |
42 | | v2 Invisible<br/>(Invisible reCAPTCHA badge) | [`invisible_recaptcha_tags`](#invisible_recaptcha_tags) | Validate requests in the background | | |
43 | ||
44 | Note: You can _only_ use methods that match your key's type. You cannot use v2 methods with a v3 | |
45 | key or use `recaptcha_tags` with a v2 Invisible key, for example. Otherwise you will get an | |
46 | error like "Invalid key type" or "This site key is not enabled for the invisible captcha." | |
47 | ||
48 | Note: Enter `localhost` or `127.0.0.1` as the domain if using in development with `localhost:3000`. | |
12 | 49 | |
13 | 50 | ## Rails Installation |
14 | 51 | |
15 | [obtain a reCAPTCHA API key](https://www.google.com/recaptcha/admin). Note: Use localhost or 127.0.0.1 in domain if using localhost:3000. | |
16 | ||
17 | ```Ruby | |
52 | ```ruby | |
18 | 53 | gem "recaptcha" |
19 | 54 | ``` |
20 | 55 | |
21 | Keep keys out of the code base with environment variables.<br/> | |
22 | Set in production and locally use [dotenv](https://github.com/bkeepers/dotenv), make sure to add it above recaptcha. | |
23 | ||
24 | Otherwise see [Alternative API key setup](#alternative-api-key-setup). | |
25 | ||
26 | ``` | |
27 | export RECAPTCHA_SITE_KEY = '6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy' | |
56 | You can keep keys out of the code base with environment variables or with Rails [secrets](https://api.rubyonrails.org/classes/Rails/Application.html#method-i-secrets).<br/> | |
57 | ||
58 | In development, you can use the [dotenv](https://github.com/bkeepers/dotenv) gem. (Make sure to add it above `gem 'recaptcha'`.) | |
59 | ||
60 | See [Alternative API key setup](#alternative-api-key-setup) for more ways to configure or override | |
61 | keys. See also the | |
62 | [Configuration](https://www.rubydoc.info/github/ambethia/recaptcha/master/Recaptcha/Configuration) | |
63 | documentation. | |
64 | ||
65 | ```shell | |
66 | export RECAPTCHA_SITE_KEY = '6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy' | |
28 | 67 | export RECAPTCHA_SECRET_KEY = '6Lc6BAAAAAAAAKN3DRm6VA_xxxxxxxxxxxxxxxxx' |
29 | 68 | ``` |
30 | 69 | |
31 | Add `recaptcha_tags` to the forms you want to protect. | |
32 | ||
33 | ```Erb | |
70 | If you have an Enterprise API key: | |
71 | ||
72 | ```shell | |
73 | export RECAPTCHA_ENTERPRISE = 'true' | |
74 | export RECAPTCHA_ENTERPRISE_API_KEY = 'AIzvFyE3TU-g4K_Kozr9F1smEzZSGBVOfLKyupA' | |
75 | export RECAPTCHA_ENTERPRISE_PROJECT_ID = 'my-project' | |
76 | ``` | |
77 | ||
78 | Add `recaptcha_tags` to the forms you want to protect: | |
79 | ||
80 | ```erb | |
34 | 81 | <%= form_for @foo do |f| %> |
35 | # ... other tags | |
82 | # … | |
36 | 83 | <%= recaptcha_tags %> |
37 | # ... other tags | |
84 | # … | |
38 | 85 | <% end %> |
39 | 86 | ``` |
40 | 87 | |
41 | And, add `verify_recaptcha` logic to each form action that you've protected. | |
42 | ||
43 | ```Ruby | |
88 | Then, add `verify_recaptcha` logic to each form action that you've protected: | |
89 | ||
90 | ```ruby | |
44 | 91 | # app/controllers/users_controller.rb |
45 | 92 | @user = User.new(params[:user].permit(:name)) |
46 | 93 | if verify_recaptcha(model: @user) && @user.save |
49 | 96 | render 'new' |
50 | 97 | end |
51 | 98 | ``` |
99 | Please note that this setup uses [`reCAPTCHA_v2`](#recaptcha-v2-api-and-usage). For a `recaptcha_v3` use, please refer to [`reCAPTCHA_v3 setup`](#examples). | |
52 | 100 | |
53 | 101 | ## Sinatra / Rack / Ruby installation |
54 | 102 | |
56 | 104 | |
57 | 105 | - add `gem 'recaptcha'` to `Gemfile` |
58 | 106 | - set env variables |
59 | - `include Recaptcha::ClientHelper` where you need `recaptcha_tags` | |
60 | - `include Recaptcha::Verify` where you need `verify_recaptcha` | |
61 | ||
62 | ## recaptcha_tags | |
107 | - `include Recaptcha::Adapters::ViewMethods` where you need `recaptcha_tags` | |
108 | - `include Recaptcha::Adapters::ControllerMethods` where you need `verify_recaptcha` | |
109 | ||
110 | ||
111 | ## reCAPTCHA v2 API and Usage | |
112 | ||
113 | ### `recaptcha_tags` | |
114 | ||
115 | Use this when your key's reCAPTCHA type is "v2 Checkbox". | |
116 | ||
117 | The following options are available: | |
118 | ||
119 | | Option | Description | | |
120 | |---------------------|-------------| | |
121 | | `:theme` | Specify the theme to be used per the API. Available options: `dark` and `light`. (default: `light`) | | |
122 | | `:ajax` | Render the dynamic AJAX captcha per the API. (default: `false`) | | |
123 | | `:site_key` | Override site API key from configuration | | |
124 | | `:error` | Override the error code returned from the reCAPTCHA API (default: `nil`) | | |
125 | | `:size` | Specify a size (default: `nil`) | | |
126 | | `:nonce` | Optional. Sets nonce attribute for script. Can be generated via `SecureRandom.base64(32)`. (default: `nil`) | | |
127 | | `:id` | Specify an html id attribute (default: `nil`) | | |
128 | | `:callback` | Optional. Name of success callback function, executed when the user submits a successful response | | |
129 | | `:expired_callback` | Optional. Name of expiration callback function, executed when the reCAPTCHA response expires and the user needs to re-verify. | | |
130 | | `:error_callback` | Optional. Name of error callback function, executed when reCAPTCHA encounters an error (e.g. network connectivity) | | |
131 | | `:noscript` | Include `<noscript>` content (default: `true`)| | |
132 | ||
133 | [JavaScript resource (api.js) parameters](https://developers.google.com/recaptcha/docs/invisible#js_param): | |
134 | ||
135 | | Option | Description | | |
136 | |---------------------|-------------| | |
137 | | `:onload` | Optional. The name of your callback function to be executed once all the dependencies have loaded. (See [explicit rendering](https://developers.google.com/recaptcha/docs/display#explicit_render)) | | |
138 | | `:render` | Optional. Whether to render the widget explicitly. Defaults to `onload`, which will render the widget in the first g-recaptcha tag it finds. (See [explicit rendering](https://developers.google.com/recaptcha/docs/display#explicit_render)) | | |
139 | | `:hl` | Optional. Forces the widget to render in a specific language. Auto-detects the user's language if unspecified. (See [language codes](https://developers.google.com/recaptcha/docs/language)) | | |
140 | | `:script` | Alias for `:external_script`. If you do not need to add a script tag by helper you can set the option to `false`. It's necessary when you add a script tag manualy (default: `true`). | | |
141 | | `:external_script` | Set to `false` to avoid including a script tag for the external `api.js` resource. Useful when including multiple `recaptcha_tags` on the same page. | | |
142 | | `:script_async` | Set to `false` to load the external `api.js` resource synchronously. (default: `true`) | | |
143 | | `:script_defer` | Set to `true` to defer loading of external `api.js` until HTML documen has been parsed. (default: `true`) | | |
144 | ||
145 | Any unrecognized options will be added as attributes on the generated tag. | |
146 | ||
147 | You can also override the html attributes for the sizes of the generated `textarea` and `iframe` | |
148 | elements, if CSS isn't your thing. Inspect the [source of `recaptcha_tags`](https://github.com/ambethia/recaptcha/blob/master/lib/recaptcha/helpers.rb) | |
149 | to see these options. | |
150 | ||
151 | Note that you cannot submit/verify the same response token more than once or you will get a | |
152 | `timeout-or-duplicate` error code. If you need reset the captcha and generate a new response token, | |
153 | then you need to call `grecaptcha.reset()`. | |
154 | ||
155 | ### `verify_recaptcha` | |
156 | ||
157 | This method returns `true` or `false` after processing the response token from the reCAPTCHA widget. | |
158 | This is usually called from your controller, as seen [above](#rails-installation). | |
159 | ||
160 | Passing in the ActiveRecord object via `model: object` is optional. If you pass a `model`—and the | |
161 | captcha fails to verify—an error will be added to the object for you to use (available as | |
162 | `object.errors`). | |
163 | ||
164 | Why isn't this a model validation? Because that violates MVC. You can use it like this, or how ever | |
165 | you like. | |
63 | 166 | |
64 | 167 | Some of the options available: |
65 | 168 | |
66 | | Option | Description | | |
67 | |-------------------|-------------| | |
68 | | :noscript | Include <noscript> content (default `true`)| | |
69 | | :theme | Specify the theme to be used per the API. Available options: `dark` and `light`. (default `light`)| | |
70 | | :ajax | Render the dynamic AJAX captcha per the API. (default `false`)| | |
71 | | :site_key | Override site API key | | |
72 | | :error | Override the error code returned from the reCAPTCHA API (default `nil`)| | |
73 | | :size | Specify a size (default `nil`)| | |
74 | | :hl | Optional. Forces the widget to render in a specific language. Auto-detects the user's language if unspecified. (See [language codes](https://developers.google.com/recaptcha/docs/language)) | | |
75 | | :nonce | Optional. Sets nonce attribute for script. Can be generated via `SecureRandom.base64(32)`. (default `nil`)| | |
76 | | :id | Specify an html id attribute (default `nil`)| | |
77 | | :script | If you do not need to add a script tag by helper you can set the option to false. It's necessary when you add a script tag manualy (default `true`)| | |
78 | | :callback | Optional. Name of success callback function, executed when the user submits a successful response | | |
79 | | :expired_callback | Optional. Name of expiration callback function, executed when the reCAPTCHA response expires and the user needs to re-verify. | | |
80 | | :error_callback | Optional. Name of error callback function, executed when reCAPTCHA encounters an error (e.g. network connectivity) | | |
81 | ||
82 | You can also override the html attributes for the sizes of the generated `textarea` and `iframe` | |
83 | elements, if CSS isn't your thing. Inspect the source of `recaptcha_tags` to see these options. | |
84 | ||
85 | ## verify_recaptcha | |
86 | ||
87 | This method returns `true` or `false` after processing the parameters from the reCAPTCHA widget. Why | |
88 | isn't this a model validation? Because that violates MVC. You can use it like this, or how ever you | |
89 | like. Passing in the ActiveRecord object is optional, if you do--and the captcha fails to verify--an | |
90 | error will be added to the object for you to use. | |
91 | ||
92 | Some of the options available: | |
93 | ||
94 | | Option | Description | | |
95 | |--------------|-------------| | |
96 | | :model | Model to set errors. | |
97 | | :attribute | Model attribute to receive errors. (default :base) | |
98 | | :message | Custom error message. | |
99 | | :secret_key | Override secret API key. | |
100 | | :timeout | The number of seconds to wait for reCAPTCHA servers before give up. (default `3`) | |
101 | | :response | Custom response parameter. (default: params['g-recaptcha-response']) | |
102 | | :hostname | Expected hostname or a callable that validates the hostname, see [domain validation](https://developers.google.com/recaptcha/docs/domain_validation) and [hostname](https://developers.google.com/recaptcha/docs/verify#api-response) docs. (default: `nil`, but can be changed by setting `config.hostname`) | |
103 | | :env | Current environment. The request to verify will be skipped if the environment is specified in configuration under `skip_verify_env` | |
104 | ||
105 | ## invisible_recaptcha_tags | |
106 | ||
107 | Make sure to read [Invisible reCAPTCHA](https://developers.google.com/recaptcha/docs/invisible). | |
169 | | Option | Description | | |
170 | |---------------------------|-------------| | |
171 | | `:model` | Model to set errors. | |
172 | | `:attribute` | Model attribute to receive errors. (default: `:base`) | |
173 | | `:message` | Custom error message. | |
174 | | `:secret_key` | Override the secret API key from the configuration. | |
175 | | `:enterprise_api_key` | Override the Enterprise API key from the configuration. | |
176 | | `:enterprise_project_id ` | Override the Enterprise project ID from the configuration. | |
177 | | `:timeout` | The number of seconds to wait for reCAPTCHA servers before give up. (default: `3`) | |
178 | | `:response` | Custom response parameter. (default: `params['g-recaptcha-response-data']`) | |
179 | | `:hostname` | Expected hostname or a callable that validates the hostname, see [domain validation](https://developers.google.com/recaptcha/docs/domain_validation) and [hostname](https://developers.google.com/recaptcha/docs/verify#api-response) docs. (default: `nil`, but can be changed by setting `config.hostname`) | |
180 | | `:env` | Current environment. The request to verify will be skipped if the environment is specified in configuration under `skip_verify_env` | |
181 | ||
182 | ||
183 | ### `invisible_recaptcha_tags` | |
184 | ||
185 | Use this when your key's reCAPTCHA type is "v2 Invisible". | |
186 | ||
187 | For more information, refer to: [Invisible reCAPTCHA](https://developers.google.com/recaptcha/docs/invisible). | |
188 | ||
189 | This is similar to `recaptcha_tags`, with the following additional options that are only available | |
190 | on `invisible_recaptcha_tags`: | |
191 | ||
192 | | Option | Description | | |
193 | |---------------------|-------------| | |
194 | | `:ui` | The type of UI to render for this "invisible" widget. (default: `:button`)<br/>`:button`: Renders a `<button type="submit">` tag with `options[:text]` as the button text.<br/>`:invisible`: Renders a `<div>` tag.<br/>`:input`: Renders a `<input type="submit">` tag with `options[:text]` as the button text. | | |
195 | | `:text` | The text to show for the button. (default: `"Submit"`) | |
196 | | `:inline_script` | If you do not need this helper to add an inline script tag, you can set the option to `false` (default: `true`). | |
197 | ||
198 | It also accepts most of the options that `recaptcha_tags` accepts, including the following: | |
199 | ||
200 | | Option | Description | | |
201 | |---------------------|-------------| | |
202 | | `:site_key` | Override site API key from configuration | | |
203 | | `:nonce` | Optional. Sets nonce attribute for script tag. Can be generated via `SecureRandom.base64(32)`. (default: `nil`) | | |
204 | | `:id` | Specify an html id attribute (default: `nil`) | | |
205 | | `:script` | Same as setting both `:inline_script` and `:external_script`. If you only need one or the other, use `:inline_script` and `:external_script` instead. | | |
206 | | `:callback` | Optional. Name of success callback function, executed when the user submits a successful response | | |
207 | | `:expired_callback` | Optional. Name of expiration callback function, executed when the reCAPTCHA response expires and the user needs to re-verify. | | |
208 | | `:error_callback` | Optional. Name of error callback function, executed when reCAPTCHA encounters an error (e.g. network connectivity) | | |
209 | ||
210 | [JavaScript resource (api.js) parameters](https://developers.google.com/recaptcha/docs/invisible#js_param): | |
211 | ||
212 | | Option | Description | | |
213 | |---------------------|-------------| | |
214 | | `:onload` | Optional. The name of your callback function to be executed once all the dependencies have loaded. (See [explicit rendering](https://developers.google.com/recaptcha/docs/display#explicit_render)) | | |
215 | | `:render` | Optional. Whether to render the widget explicitly. Defaults to `onload`, which will render the widget in the first g-recaptcha tag it finds. (See [explicit rendering](https://developers.google.com/recaptcha/docs/display#explicit_render)) | | |
216 | | `:hl` | Optional. Forces the widget to render in a specific language. Auto-detects the user's language if unspecified. (See [language codes](https://developers.google.com/recaptcha/docs/language)) | | |
217 | | `:external_script` | Set to `false` to avoid including a script tag for the external `api.js` resource. Useful when including multiple `recaptcha_tags` on the same page. | | |
218 | | `:script_async` | Set to `false` to load the external `api.js` resource synchronously. (default: `true`) | | |
219 | | `:script_defer` | Set to `false` to defer loading of external `api.js` until HTML documen has been parsed. (default: `true`) | | |
108 | 220 | |
109 | 221 | ### With a single form on a page |
110 | 222 | |
111 | 223 | 1. The `invisible_recaptcha_tags` generates a submit button for you. |
112 | 224 | |
113 | ```Erb | |
225 | ```erb | |
114 | 226 | <%= form_for @foo do |f| %> |
115 | 227 | # ... other tags |
116 | 228 | <%= invisible_recaptcha_tags text: 'Submit form' %> |
124 | 236 | 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 | 237 | 2. The `invisible_recaptcha_tags` generates a submit button for you. |
126 | 238 | |
127 | ```Erb | |
239 | ```erb | |
128 | 240 | <%= form_for @foo, html: {id: 'invisible-recaptcha-form'} do |f| %> |
129 | 241 | # ... other tags |
130 | 242 | <%= invisible_recaptcha_tags callback: 'submitInvisibleRecaptchaForm', text: 'Submit form' %> |
131 | 243 | <% end %> |
132 | 244 | ``` |
133 | 245 | |
134 | ```Javascript | |
246 | ```javascript | |
135 | 247 | // app/assets/javascripts/application.js |
136 | 248 | var submitInvisibleRecaptchaForm = function () { |
137 | 249 | document.getElementById("invisible-recaptcha-form").submit(); |
144 | 256 | |
145 | 257 | 1. Specify `ui` option |
146 | 258 | |
147 | ```Erb | |
259 | ```erb | |
148 | 260 | <%= form_for @foo, html: {id: 'invisible-recaptcha-form'} do |f| %> |
149 | 261 | # ... other tags |
150 | 262 | <button type="button" id="submit-btn"> |
154 | 266 | <% end %> |
155 | 267 | ``` |
156 | 268 | |
157 | ```Javascript | |
269 | ```javascript | |
158 | 270 | // app/assets/javascripts/application.js |
159 | 271 | document.getElementById('submit-btn').addEventListener('click', function (e) { |
160 | 272 | // do some validation |
169 | 281 | }; |
170 | 282 | ``` |
171 | 283 | |
284 | ||
285 | ## reCAPTCHA v3 API and Usage | |
286 | ||
287 | The main differences from v2 are: | |
288 | 1. you must specify an [action](https://developers.google.com/recaptcha/docs/v3#actions) in both frontend and backend | |
289 | 1. you can choose the minimum score required for you to consider the verification a success | |
290 | (consider the user a human and not a robot) | |
291 | 1. reCAPTCHA v3 is invisible (except for the reCAPTCHA badge) and will never interrupt your users; | |
292 | you have to choose which scores are considered an acceptable risk, and choose what to do (require | |
293 | two-factor authentication, show a v3 challenge, etc.) if the score falls below the threshold you | |
294 | choose | |
295 | ||
296 | For more information, refer to the [v3 documentation](https://developers.google.com/recaptcha/docs/v3). | |
297 | ||
298 | ### Examples | |
299 | ||
300 | With v3, you can let all users log in without any intervention at all if their score is above some | |
301 | threshold, and only show a v2 checkbox recaptcha challenge (fall back to v2) if it is below the | |
302 | threshold: | |
303 | ||
304 | ```erb | |
305 | … | |
306 | <% if @show_checkbox_recaptcha %> | |
307 | <%= recaptcha_tags %> | |
308 | <% else %> | |
309 | <%= recaptcha_v3(action: 'login', site_key: ENV['RECAPTCHA_SITE_KEY_V3']) %> | |
310 | <% end %> | |
311 | … | |
312 | ``` | |
313 | ||
314 | ```ruby | |
315 | # app/controllers/sessions_controller.rb | |
316 | def create | |
317 | success = verify_recaptcha(action: 'login', minimum_score: 0.5, secret_key: ENV['RECAPTCHA_SECRET_KEY_V3']) | |
318 | checkbox_success = verify_recaptcha unless success | |
319 | if success || checkbox_success | |
320 | # Perform action | |
321 | else | |
322 | if !success | |
323 | @show_checkbox_recaptcha = true | |
324 | end | |
325 | render 'new' | |
326 | end | |
327 | end | |
328 | ``` | |
329 | ||
330 | (You can also find this [example](demo/rails/app/controllers/v3_captchas_controller.rb) in the demo app.) | |
331 | ||
332 | Another example: | |
333 | ||
334 | ```erb | |
335 | <%= form_for @user do |f| %> | |
336 | … | |
337 | <%= recaptcha_v3(action: 'registration') %> | |
338 | … | |
339 | <% end %> | |
340 | ``` | |
341 | ||
342 | ```ruby | |
343 | # app/controllers/users_controller.rb | |
344 | def create | |
345 | @user = User.new(params[:user].permit(:name)) | |
346 | recaptcha_valid = verify_recaptcha(model: @user, action: 'registration') | |
347 | if recaptcha_valid | |
348 | if @user.save | |
349 | redirect_to @user | |
350 | else | |
351 | render 'new' | |
352 | end | |
353 | else | |
354 | # Score is below threshold, so user may be a bot. Show a challenge, require multi-factor | |
355 | # authentication, or do something else. | |
356 | render 'new' | |
357 | end | |
358 | end | |
359 | ``` | |
360 | ||
361 | ||
362 | ### `recaptcha_v3` | |
363 | ||
364 | Adds an inline script tag that calls `grecaptcha.execute` for the given `site_key` and `action` and | |
365 | calls the `callback` with the resulting response token. You need to verify this token with | |
366 | [`verify_recaptcha`](#verify_recaptcha-use-with-v3) in your controller in order to get the | |
367 | [score](https://developers.google.com/recaptcha/docs/v3#score). | |
368 | ||
369 | By default, this inserts a hidden `<input type="hidden" class="g-recaptcha-response">` tag. The | |
370 | value of this input will automatically be set to the response token (by the default callback | |
371 | function). This lets you include `recaptcha_v3` within a `<form>` tag and have it automatically | |
372 | submit the token as part of the form submission. | |
373 | ||
374 | Note: reCAPTCHA actually already adds its own hidden tag, like `<textarea | |
375 | id="g-recaptcha-response-data-100000" name="g-recaptcha-response-data" class="g-recaptcha-response">`, | |
376 | immediately ater the reCAPTCHA badge in the bottom right of the page — but since it is not inside of | |
377 | any `<form>` element, and since it already passes the token to the callback, this hidden `textarea` | |
378 | isn't helpful to us. | |
379 | ||
380 | If you need to submit the response token to the server in a different way than via a regular form | |
381 | submit, such as via [Ajax](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) or [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), | |
382 | then you can either: | |
383 | 1. just extract the token out of the hidden `<input>` or `<textarea>` (both of which will have a | |
384 | predictable name/id), like `document.getElementById('g-recaptcha-response-data-my-action').value`, or | |
385 | 2. write and specify a custom `callback` function. You may also want to pass `element: false` if you | |
386 | don't have a use for the hidden input element. | |
387 | ||
388 | Note that you cannot submit/verify the same response token more than once or you | |
389 | will get a `timeout-or-duplicate` error code. If you need reset the captcha and | |
390 | generate a new response token, then you need to call `grecaptcha.execute(…)` or | |
391 | `grecaptcha.enterprise.execute(…)` again. This helper provides a JavaScript | |
392 | method (for each action) named `executeRecaptchaFor{action}` to make this | |
393 | easier. That is the same method that is invoked immediately. It simply calls | |
394 | `grecaptcha.execute` or `grecaptcha.enterprise.execute` again and then calls the | |
395 | `callback` function with the response token. | |
396 | ||
397 | You will also get a `timeout-or-duplicate` error if too much time has passed between getting the | |
398 | response token and verifying it. This can easily happen with large forms that take the user a couple | |
399 | minutes to complete. Unlike v2, where you can use the `expired-callback` to be notified when the | |
400 | response expires, v3 appears to provide no such callback. See also | |
401 | [1](https://github.com/google/recaptcha/issues/281) and | |
402 | [2](https://stackoverflow.com/questions/54437745/recaptcha-v3-how-to-deal-with-expired-token-after-idle). | |
403 | ||
404 | To deal with this, it is recommended to call the "execute" in your form's submit handler (or | |
405 | immediately before sending to the server to verify if not using a form) rather than using the | |
406 | response token that gets generated when the page first loads. The `executeRecaptchaFor{action}` | |
407 | function mentioned above can be used if you want it to invoke a callback, or the | |
408 | `executeRecaptchaFor{action}Async` variant if you want a `Promise` that you can `await`. See | |
409 | [demo/rails/app/views/v3_captchas/index.html.erb](demo/rails/app/views/v3_captchas/index.html.erb) | |
410 | for an example of this. | |
411 | ||
412 | This helper is similar to the [`recaptcha_tags`](#recaptcha_tags)/[`invisible_recaptcha_tags`](#invisible_recaptcha_tags) helpers | |
413 | but only accepts the following options: | |
414 | ||
415 | | Option | Description | | |
416 | |---------------------|-------------| | |
417 | | `:site_key` | Override site API key | | |
418 | | `:action` | The name of the [reCAPTCHA action](https://developers.google.com/recaptcha/docs/v3#actions). Actions may only contain alphanumeric characters and slashes, and must not be user-specific. | | |
419 | | `:nonce` | Optional. Sets nonce attribute for script. Can be generated via `SecureRandom.base64(32)`. (default: `nil`) | | |
420 | | `:callback` | Name of callback function to call with the token. When `element` is `:input`, this defaults to a function named `setInputWithRecaptchaResponseTokenFor#{sanitize_action(action)}` that sets the value of the hidden input to the token. | | |
421 | | `:id` | Specify a unique `id` attribute for the `<input>` element if using `element: :input`. (default: `"g-recaptcha-response-data-"` + `action`) | | |
422 | | `:name` | Specify a unique `name` attribute for the `<input>` element if using `element: :input`. (default: `g-recaptcha-response-data[action]`) | | |
423 | | `:script` | Same as setting both `:inline_script` and `:external_script`. (default: `true`). | | |
424 | | `:inline_script` | If `true`, adds an inline script tag that calls `grecaptcha.execute` for the given `site_key` and `action` and calls the `callback` with the resulting response token. Pass `false` if you want to handle calling `grecaptcha.execute` yourself. (default: `true`) | | |
425 | | `:element` | The element to render, if any (default: `:input`)<br/>`:input`: Renders a hidden `<input type="hidden">` tag. The value of this will be set to the response token by the default `setInputWithRecaptchaResponseTokenFor{action}` callback.<br/>`false`: Doesn't render any tag. You'll have to add a custom callback that does something with the token. | | |
426 | | `:turbolinks` | If `true`, calls the js function which executes reCAPTCHA after all the dependencies have been loaded. This cannot be used with the js param `:onload`. This makes reCAPTCHAv3 usable with turbolinks. | | |
427 | ||
428 | [JavaScript resource (api.js) parameters](https://developers.google.com/recaptcha/docs/invisible#js_param): | |
429 | ||
430 | | Option | Description | | |
431 | |---------------------|-------------| | |
432 | | `:onload` | Optional. The name of your callback function to be executed once all the dependencies have loaded. (See [explicit rendering](https://developers.google.com/recaptcha/docs/display#explicit_render))| | |
433 | | `:external_script` | Set to `false` to avoid including a script tag for the external `api.js` resource. Useful when including multiple `recaptcha_tags` on the same page. | |
434 | | `:script_async` | Set to `true` to load the external `api.js` resource asynchronously. (default: `false`) | | |
435 | | `:script_defer` | Set to `true` to defer loading of external `api.js` until HTML documen has been parsed. (default: `false`) | | |
436 | ||
437 | If using `element: :input`, any unrecognized options will be added as attributes on the generated | |
438 | `<input>` element. | |
439 | ||
440 | ### `verify_recaptcha` (use with v3) | |
441 | ||
442 | This works the same as for v2, except that you may pass an `action` and `minimum_score` if you wish | |
443 | to validate that the action matches or that the score is above the given threshold, respectively. | |
444 | ||
445 | ```ruby | |
446 | result = verify_recaptcha(action: 'action/name') | |
447 | ``` | |
448 | ||
449 | | Option | Description | | |
450 | |------------------|-------------| | |
451 | | `:action` | The name of the [reCAPTCHA action](https://developers.google.com/recaptcha/docs/v3#actions) that we are verifying. Set to `false` or `nil` to skip verifying that the action matches. | |
452 | | `:minimum_score` | Provide a threshold to meet or exceed. Threshold should be a float between 0 and 1 which will be tested as `score >= minimum_score`. (Default: `nil`) | | |
453 | ||
454 | ### Multiple actions on the same page | |
455 | ||
456 | According to https://developers.google.com/recaptcha/docs/v3#placement, | |
457 | ||
458 | > Note: You can execute reCAPTCHA as many times as you'd like with different actions on the same page. | |
459 | ||
460 | You will need to verify each action individually with a separate call to `verify_recaptcha`. | |
461 | ||
462 | ```ruby | |
463 | result_a = verify_recaptcha(action: 'a') | |
464 | result_b = verify_recaptcha(action: 'b') | |
465 | ``` | |
466 | ||
467 | Because the response tokens for multiple actions may be submitted together in the same request, they | |
468 | are passed as a hash under `params['g-recaptcha-response-data']` with the action as the key. | |
469 | ||
470 | It is recommended to pass `external_script: false` on all but one of the calls to | |
471 | `recaptcha` since you only need to include the script tag once for a given `site_key`. | |
472 | ||
473 | ## `recaptcha_reply` | |
474 | ||
475 | After `verify_recaptcha` has been called, you can call `recaptcha_reply` to get the raw reply from recaptcha. This can allow you to get the exact score returned by recaptcha should you need it. | |
476 | ||
477 | ```ruby | |
478 | if verify_recaptcha(action: 'login') | |
479 | redirect_to @user | |
480 | else | |
481 | score = recaptcha_reply['score'] | |
482 | Rails.logger.warn("User #{@user.id} was denied login because of a recaptcha score of #{score}") | |
483 | render 'new' | |
484 | end | |
485 | ``` | |
486 | ||
487 | `recaptcha_reply` will return `nil` if the the reply was not yet fetched. | |
488 | ||
172 | 489 | ## I18n support |
173 | reCAPTCHA passes two types of error explanation to a linked model. It will use the I18n gem | |
174 | to translate the default error message if I18n is available. To customize the messages to your locale, | |
175 | add these keys to your I18n backend: | |
176 | ||
177 | `recaptcha.errors.verification_failed` error message displayed if the captcha words didn't match | |
178 | `recaptcha.errors.recaptcha_unreachable` displayed if a timeout error occured while attempting to verify the captcha | |
179 | ||
180 | Also you can translate API response errors to human friendly by adding translations to the locale (`config/locales/en.yml`): | |
181 | ||
182 | ```Yaml | |
490 | ||
491 | reCAPTCHA supports the I18n gem (it comes with English translations) | |
492 | To override or add new languages, add to `config/locales/*.yml` | |
493 | ||
494 | ```yaml | |
495 | # config/locales/en.yml | |
183 | 496 | en: |
184 | 497 | recaptcha: |
185 | 498 | errors: |
186 | verification_failed: 'Fail' | |
499 | verification_failed: 'reCAPTCHA was incorrect, please try again.' | |
500 | recaptcha_unreachable: 'reCAPTCHA verification server error, please try again.' | |
187 | 501 | ``` |
188 | 502 | |
189 | 503 | ## Testing |
190 | 504 | |
191 | 505 | By default, reCAPTCHA is skipped in "test" and "cucumber" env. To enable it during test: |
192 | 506 | |
193 | ```Ruby | |
507 | ```ruby | |
194 | 508 | Recaptcha.configuration.skip_verify_env.delete("test") |
195 | 509 | ``` |
196 | 510 | |
198 | 512 | |
199 | 513 | ### Recaptcha.configure |
200 | 514 | |
201 | ```Ruby | |
515 | ```ruby | |
202 | 516 | # config/initializers/recaptcha.rb |
203 | 517 | Recaptcha.configure do |config| |
204 | 518 | config.site_key = '6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy' |
205 | 519 | config.secret_key = '6Lc6BAAAAAAAAKN3DRm6VA_xxxxxxxxxxxxxxxxx' |
520 | ||
206 | 521 | # Uncomment the following line if you are using a proxy server: |
207 | 522 | # config.proxy = 'http://myproxy.com.au:8080' |
523 | ||
524 | # Uncomment the following lines if you are using the Enterprise API: | |
525 | # config.enterprise = true | |
526 | # config.enterprise_api_key = 'AIzvFyE3TU-g4K_Kozr9F1smEzZSGBVOfLKyupA' | |
527 | # config.enterprise_project_id = 'my-project' | |
208 | 528 | end |
209 | 529 | ``` |
210 | 530 | |
211 | 531 | ### Recaptcha.with_configuration |
212 | 532 | |
213 | For temporary overwrites (not thread safe). | |
214 | ||
215 | ```Ruby | |
533 | For temporary overwrites (not thread-safe). | |
534 | ||
535 | ```ruby | |
216 | 536 | Recaptcha.with_configuration(site_key: '12345') do |
217 | 537 | # Do stuff with the overwritten site_key. |
218 | 538 | end |
222 | 542 | |
223 | 543 | Pass in keys as options at runtime, for code base with multiple reCAPTCHA setups: |
224 | 544 | |
225 | ```Ruby | |
545 | ```ruby | |
226 | 546 | recaptcha_tags site_key: '6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy' |
227 | 547 | |
228 | 548 | # and |
0 | # reCAPTCHA keys for testing purposes | |
1 | # Make your own at https://www.google.com/recaptcha | |
0 | # v2 reCAPTCHA keys for testing purposes | |
1 | # (https://developers.google.com/recaptcha/docs/faq#id-like-to-run-automated-tests-with-recaptcha-what-should-i-do) | |
2 | # You can make your own at https://www.google.com/recaptcha | |
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 inline_script: true, action: 'demo' %> | |
34 | <%= submit_tag %> | |
35 | <% end %> | |
36 | ||
37 | <script type="text/javascript"> | |
38 | const form = document.forms.item(0) | |
39 | const hiddenInput = document.getElementById('g-recaptcha-response-data-demo') | |
40 | form.addEventListener('submit', async function(e) { | |
41 | e.preventDefault(); | |
42 | console.log('Submitted form. Getting fresh reCAPTCHA response token…') | |
43 | const response = await window.executeRecaptchaForDemoAsync() | |
44 | console.log(`Response was: ${response}`) | |
45 | hiddenInput.value = response | |
46 | form.submit() | |
47 | }); | |
48 | </script> | |
49 | ||
50 | <% else # Single %> | |
51 | <%= form_tag "/#{controller_name}" do %> | |
52 | <%= recaptcha_v3 action: 'demo' %> | |
53 | <%= submit_tag %> | |
54 | <% end %> | |
55 | <% end %> | |
56 | ||
57 | <%= link_to 'Single', '?' if params[:multi] || params[:custom_callback] || params[:execute_on_submit] %> | |
58 | <%= link_to 'with v2 fallback', '/v3_captchas/?with_v2_fallback=1' unless params[:with_v2_fallback] %> | |
59 | <%= link_to 'Multi', '?multi=1' unless params[:multi] %> | |
60 | <%= link_to 'Custom callback', '?custom_callback=1' unless params[:custom_callback] %> | |
61 | <%= link_to 'Execute on submit', '?execute_on_submit=1' unless params[:execute_on_submit] %> |
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 | success, @_recaptcha_reply = | |
27 | Recaptcha.verify_via_api_call(recaptcha_response, options.merge(with_reply: true)) | |
28 | success | |
29 | end | |
30 | ||
31 | if verified | |
32 | flash.delete(:recaptcha_error) if recaptcha_flash_supported? && !model | |
33 | true | |
34 | else | |
35 | recaptcha_error( | |
36 | model, | |
37 | attribute, | |
38 | options.fetch(:message) { Recaptcha::Helpers.to_error_message(:verification_failed) } | |
39 | ) | |
40 | false | |
41 | end | |
42 | rescue Timeout::Error | |
43 | if Recaptcha.configuration.handle_timeouts_gracefully | |
44 | recaptcha_error( | |
45 | model, | |
46 | attribute, | |
47 | options.fetch(:message) { Recaptcha::Helpers.to_error_message(:recaptcha_unreachable) } | |
48 | ) | |
49 | false | |
50 | else | |
51 | raise RecaptchaError, 'Recaptcha unreachable.' | |
52 | end | |
53 | rescue StandardError => e | |
54 | raise RecaptchaError, e.message, e.backtrace | |
55 | end | |
56 | end | |
57 | ||
58 | def verify_recaptcha!(options = {}) | |
59 | verify_recaptcha(options) || raise(VerifyError) | |
60 | end | |
61 | ||
62 | def recaptcha_reply | |
63 | @_recaptcha_reply if defined?(@_recaptcha_reply) | |
64 | end | |
65 | ||
66 | def recaptcha_error(model, attribute, message) | |
67 | if model | |
68 | model.errors.add(attribute, message) | |
69 | elsif recaptcha_flash_supported? | |
70 | flash[:recaptcha_error] = message | |
71 | end | |
72 | end | |
73 | ||
74 | def recaptcha_flash_supported? | |
75 | request.respond_to?(:format) && request.format == :html && respond_to?(:flash) | |
76 | end | |
77 | ||
78 | # Extracts response token from params. params['g-recaptcha-response-data'] for recaptcha_v3 or | |
79 | # params['g-recaptcha-response'] for recaptcha_tags and invisible_recaptcha_tags and should | |
80 | # either be a string or a hash with the action name(s) as keys. If it is a hash, then `action` | |
81 | # is used as the key. | |
82 | # @return [String] A response token if one was passed in the params; otherwise, `''` | |
83 | def recaptcha_response_token(action = nil) | |
84 | response_param = params['g-recaptcha-response-data'] || params['g-recaptcha-response'] | |
85 | response_param = response_param[action] if action && response_param.respond_to?(:key?) | |
86 | ||
87 | if response_param.is_a?(String) | |
88 | response_param | |
89 | else | |
90 | '' | |
91 | end | |
92 | end | |
93 | end | |
94 | end | |
95 | end |
0 | # frozen_string_literal: true | |
1 | ||
2 | module Recaptcha | |
3 | module Adapters | |
4 | module ViewMethods | |
5 | # Renders a [reCAPTCHA v3](https://developers.google.com/recaptcha/docs/v3) script and (by | |
6 | # default) a hidden input to submit the response token. You can also call the functions | |
7 | # directly if you prefer. You can use | |
8 | # `Recaptcha::Helpers.recaptcha_v3_execute_function_name(action)` to get the name of the | |
9 | # function to call. | |
10 | def recaptcha_v3(options = {}) | |
11 | ::Recaptcha::Helpers.recaptcha_v3(options) | |
12 | end | |
13 | ||
14 | # Renders a reCAPTCHA [v2 Checkbox](https://developers.google.com/recaptcha/docs/display) widget | |
15 | def recaptcha_tags(options = {}) | |
16 | ::Recaptcha::Helpers.recaptcha_tags(options) | |
17 | end | |
18 | ||
19 | # Renders a reCAPTCHA v2 [Invisible reCAPTCHA](https://developers.google.com/recaptcha/docs/invisible) | |
20 | def invisible_recaptcha_tags(options = {}) | |
21 | ::Recaptcha::Helpers.invisible_recaptcha_tags(options) | |
22 | end | |
23 | end | |
24 | end | |
25 | end |
0 | # 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 | 'free_server_url' => 'https://www.recaptcha.net/recaptcha/api.js', | |
34 | 'enterprise_server_url' => 'https://www.recaptcha.net/recaptcha/enterprise.js', | |
35 | 'free_verify_url' => 'https://www.recaptcha.net/recaptcha/api/siteverify', | |
36 | 'enterprise_verify_url' => 'https://recaptchaenterprise.googleapis.com/v1beta1/projects' | |
37 | }.freeze | |
38 | ||
39 | attr_accessor :default_env, :skip_verify_env, :proxy, :secret_key, :site_key, :handle_timeouts_gracefully, :hostname | |
40 | attr_accessor :enterprise, :enterprise_api_key, :enterprise_project_id | |
33 | 41 | attr_writer :api_server_url, :verify_url |
34 | 42 | |
35 | 43 | def initialize #:nodoc: |
44 | @default_env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || (Rails.env if defined? Rails.env) | |
36 | 45 | @skip_verify_env = %w[test cucumber] |
37 | @handle_timeouts_gracefully = HANDLE_TIMEOUTS_GRACEFULLY | |
46 | @handle_timeouts_gracefully = true | |
38 | 47 | |
39 | 48 | @secret_key = ENV['RECAPTCHA_SECRET_KEY'] |
40 | 49 | @site_key = ENV['RECAPTCHA_SITE_KEY'] |
50 | ||
51 | @enterprise = ENV['RECAPTCHA_ENTERPRISE'] == 'true' | |
52 | @enterprise_api_key = ENV['RECAPTCHA_ENTERPRISE_API_KEY'] | |
53 | @enterprise_project_id = ENV['RECAPTCHA_ENTERPRISE_PROJECT_ID'] | |
54 | ||
41 | 55 | @verify_url = nil |
42 | 56 | @api_server_url = nil |
43 | 57 | end |
50 | 64 | site_key || raise(RecaptchaError, "No site key specified.") |
51 | 65 | end |
52 | 66 | |
67 | def enterprise_api_key! | |
68 | enterprise_api_key || raise(RecaptchaError, "No Enterprise API key specified.") | |
69 | end | |
70 | ||
71 | def enterprise_project_id! | |
72 | enterprise_project_id || raise(RecaptchaError, "No Enterprise project ID specified.") | |
73 | end | |
74 | ||
53 | 75 | def api_server_url |
54 | @api_server_url || CONFIG.fetch('server_url') | |
76 | @api_server_url || (enterprise ? DEFAULTS.fetch('enterprise_server_url') : DEFAULTS.fetch('free_server_url')) | |
55 | 77 | end |
56 | 78 | |
57 | 79 | def verify_url |
58 | @verify_url || CONFIG.fetch('verify_url') | |
80 | @verify_url || (enterprise ? DEFAULTS.fetch('enterprise_verify_url') : DEFAULTS.fetch('free_verify_url')) | |
59 | 81 | end |
60 | 82 | end |
61 | 83 | end |
0 | # frozen_string_literal: true | |
1 | ||
2 | module Recaptcha | |
3 | module Helpers | |
4 | DEFAULT_ERRORS = { | |
5 | recaptcha_unreachable: 'Oops, we failed to validate your reCAPTCHA response. Please try again.', | |
6 | verification_failed: 'reCAPTCHA verification failed, please try again.' | |
7 | }.freeze | |
8 | ||
9 | def self.recaptcha_v3(options = {}) | |
10 | site_key = options[:site_key] ||= Recaptcha.configuration.site_key! | |
11 | action = options.delete(:action) || raise(Recaptcha::RecaptchaError, 'action is required') | |
12 | id = options.delete(:id) || "g-recaptcha-response-data-" + dasherize_action(action) | |
13 | name = options.delete(:name) || "g-recaptcha-response-data[#{action}]" | |
14 | turbolinks = options.delete(:turbolinks) | |
15 | options[:render] = site_key | |
16 | options[:script_async] ||= false | |
17 | options[:script_defer] ||= false | |
18 | element = options.delete(:element) | |
19 | element = element == false ? false : :input | |
20 | if element == :input | |
21 | callback = options.delete(:callback) || recaptcha_v3_default_callback_name(action) | |
22 | end | |
23 | options[:class] = "g-recaptcha-response #{options[:class]}" | |
24 | ||
25 | if turbolinks | |
26 | options[:onload] = recaptcha_v3_execute_function_name(action) | |
27 | end | |
28 | html, tag_attributes = components(options) | |
29 | if turbolinks | |
30 | html << recaptcha_v3_onload_script(site_key, action, callback, id, options) | |
31 | elsif recaptcha_v3_inline_script?(options) | |
32 | html << recaptcha_v3_inline_script(site_key, action, callback, id, options) | |
33 | end | |
34 | case element | |
35 | when :input | |
36 | html << %(<input type="hidden" name="#{name}" id="#{id}" #{tag_attributes}/>\n) | |
37 | when false | |
38 | # No tag | |
39 | nil | |
40 | else | |
41 | raise(RecaptchaError, "ReCAPTCHA element `#{options[:element]}` is not valid.") | |
42 | end | |
43 | html.respond_to?(:html_safe) ? html.html_safe : html | |
44 | end | |
45 | ||
46 | def self.recaptcha_tags(options) | |
47 | if options.key?(:stoken) | |
48 | raise(RecaptchaError, "Secure Token is deprecated. Please remove 'stoken' from your calls to recaptcha_tags.") | |
49 | end | |
50 | if options.key?(:ssl) | |
51 | raise(RecaptchaError, "SSL is now always true. Please remove 'ssl' from your calls to recaptcha_tags.") | |
52 | end | |
53 | ||
54 | noscript = options.delete(:noscript) | |
55 | ||
56 | html, tag_attributes, fallback_uri = components(options.dup) | |
57 | html << %(<div #{tag_attributes}></div>\n) | |
58 | ||
59 | if noscript != false | |
60 | html << <<-HTML | |
61 | <noscript> | |
62 | <div> | |
63 | <div style="width: 302px; height: 422px; position: relative;"> | |
64 | <div style="width: 302px; height: 422px; position: absolute;"> | |
65 | <iframe | |
66 | src="#{fallback_uri}" | |
67 | name="ReCAPTCHA" | |
68 | style="width: 302px; height: 422px; border-style: none; border: 0; overflow: hidden;"> | |
69 | </iframe> | |
70 | </div> | |
71 | </div> | |
72 | <div style="width: 300px; height: 60px; border-style: none; | |
73 | bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px; | |
74 | background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;"> | |
75 | <textarea id="g-recaptcha-response" name="g-recaptcha-response" | |
76 | class="g-recaptcha-response" | |
77 | style="width: 250px; height: 40px; border: 1px solid #c1c1c1; | |
78 | margin: 10px 25px; padding: 0px; resize: none;"> | |
79 | </textarea> | |
80 | </div> | |
81 | </div> | |
82 | </noscript> | |
83 | HTML | |
84 | end | |
85 | ||
86 | html.respond_to?(:html_safe) ? html.html_safe : html | |
87 | end | |
88 | ||
89 | def self.invisible_recaptcha_tags(custom) | |
90 | options = {callback: 'invisibleRecaptchaSubmit', ui: :button}.merge(custom) | |
91 | text = options.delete(:text) | |
92 | html, tag_attributes = components(options.dup) | |
93 | html << default_callback(options) if default_callback_required?(options) | |
94 | ||
95 | case options[:ui] | |
96 | when :button | |
97 | html << %(<button type="submit" #{tag_attributes}>#{text}</button>\n) | |
98 | when :invisible | |
99 | html << %(<div data-size="invisible" #{tag_attributes}></div>\n) | |
100 | when :input | |
101 | html << %(<input type="submit" #{tag_attributes} value="#{text}"/>\n) | |
102 | else | |
103 | raise(RecaptchaError, "ReCAPTCHA ui `#{options[:ui]}` is not valid.") | |
104 | end | |
105 | html.respond_to?(:html_safe) ? html.html_safe : html | |
106 | end | |
107 | ||
108 | def self.to_error_message(key) | |
109 | default = DEFAULT_ERRORS.fetch(key) { raise ArgumentError "Unknown reCAPTCHA error - #{key}" } | |
110 | to_message("recaptcha.errors.#{key}", default) | |
111 | end | |
112 | ||
113 | if defined?(I18n) | |
114 | def self.to_message(key, default) | |
115 | I18n.translate(key, default: default) | |
116 | end | |
117 | else | |
118 | def self.to_message(_key, default) | |
119 | default | |
120 | end | |
121 | end | |
122 | ||
123 | private_class_method def self.components(options) | |
124 | html = +'' | |
125 | attributes = {} | |
126 | fallback_uri = +'' | |
127 | ||
128 | options = options.dup | |
129 | env = options.delete(:env) | |
130 | class_attribute = options.delete(:class) | |
131 | site_key = options.delete(:site_key) | |
132 | hl = options.delete(:hl) | |
133 | onload = options.delete(:onload) | |
134 | render = options.delete(:render) | |
135 | script_async = options.delete(:script_async) | |
136 | script_defer = options.delete(:script_defer) | |
137 | nonce = options.delete(:nonce) | |
138 | skip_script = (options.delete(:script) == false) || (options.delete(:external_script) == false) | |
139 | ui = options.delete(:ui) | |
140 | ||
141 | data_attribute_keys = [:badge, :theme, :type, :callback, :expired_callback, :error_callback, :size] | |
142 | data_attribute_keys << :tabindex unless ui == :button | |
143 | data_attributes = {} | |
144 | data_attribute_keys.each do |data_attribute| | |
145 | value = options.delete(data_attribute) | |
146 | data_attributes["data-#{data_attribute.to_s.tr('_', '-')}"] = value if value | |
147 | end | |
148 | ||
149 | unless Recaptcha.skip_env?(env) | |
150 | site_key ||= Recaptcha.configuration.site_key! | |
151 | script_url = Recaptcha.configuration.api_server_url | |
152 | query_params = hash_to_query( | |
153 | hl: hl, | |
154 | onload: onload, | |
155 | render: render | |
156 | ) | |
157 | script_url += "?#{query_params}" unless query_params.empty? | |
158 | async_attr = "async" if script_async != false | |
159 | defer_attr = "defer" if script_defer != false | |
160 | nonce_attr = " nonce='#{nonce}'" if nonce | |
161 | html << %(<script src="#{script_url}" #{async_attr} #{defer_attr} #{nonce_attr}></script>\n) unless skip_script | |
162 | fallback_uri = %(#{script_url.chomp(".js")}/fallback?k=#{site_key}) | |
163 | attributes["data-sitekey"] = site_key | |
164 | attributes.merge! data_attributes | |
165 | end | |
166 | ||
167 | # The remaining options will be added as attributes on the tag. | |
168 | attributes["class"] = "g-recaptcha #{class_attribute}" | |
169 | tag_attributes = attributes.merge(options).map { |k, v| %(#{k}="#{v}") }.join(" ") | |
170 | ||
171 | [html, tag_attributes, fallback_uri] | |
172 | end | |
173 | ||
174 | # v3 | |
175 | ||
176 | # Renders a script that calls `grecaptcha.execute` or | |
177 | # `grecaptcha.enterprise.execute` for the given `site_key` and `action` and | |
178 | # calls the `callback` with the resulting response token. | |
179 | private_class_method def self.recaptcha_v3_inline_script(site_key, action, callback, id, options = {}) | |
180 | nonce = options[:nonce] | |
181 | nonce_attr = " nonce='#{nonce}'" if nonce | |
182 | ||
183 | <<-HTML | |
184 | <script#{nonce_attr}> | |
185 | // Define function so that we can call it again later if we need to reset it | |
186 | // This executes reCAPTCHA and then calls our callback. | |
187 | function #{recaptcha_v3_execute_function_name(action)}() { | |
188 | #{recaptcha_ready_method_name}(function() { | |
189 | #{recaptcha_execute_method_name}('#{site_key}', {action: '#{action}'}).then(function(token) { | |
190 | #{callback}('#{id}', token) | |
191 | }); | |
192 | }); | |
193 | }; | |
194 | // Invoke immediately | |
195 | #{recaptcha_v3_execute_function_name(action)}() | |
196 | ||
197 | // Async variant so you can await this function from another async function (no need for | |
198 | // an explicit callback function then!) | |
199 | // Returns a Promise that resolves with the response token. | |
200 | async function #{recaptcha_v3_async_execute_function_name(action)}() { | |
201 | return new Promise((resolve, reject) => { | |
202 | #{recaptcha_ready_method_name}(async function() { | |
203 | resolve(await #{recaptcha_execute_method_name}('#{site_key}', {action: '#{action}'})) | |
204 | }); | |
205 | }) | |
206 | }; | |
207 | ||
208 | #{recaptcha_v3_define_default_callback(callback) if recaptcha_v3_define_default_callback?(callback, action, options)} | |
209 | </script> | |
210 | HTML | |
211 | end | |
212 | ||
213 | private_class_method def self.recaptcha_v3_onload_script(site_key, action, callback, id, options = {}) | |
214 | nonce = options[:nonce] | |
215 | nonce_attr = " nonce='#{nonce}'" if nonce | |
216 | ||
217 | <<-HTML | |
218 | <script#{nonce_attr}> | |
219 | function #{recaptcha_v3_execute_function_name(action)}() { | |
220 | #{recaptcha_ready_method_name}(function() { | |
221 | #{recaptcha_execute_method_name}('#{site_key}', {action: '#{action}'}).then(function(token) { | |
222 | #{callback}('#{id}', token) | |
223 | }); | |
224 | }); | |
225 | }; | |
226 | #{recaptcha_v3_define_default_callback(callback) if recaptcha_v3_define_default_callback?(callback, action, options)} | |
227 | </script> | |
228 | HTML | |
229 | end | |
230 | ||
231 | private_class_method def self.recaptcha_v3_inline_script?(options) | |
232 | !Recaptcha.skip_env?(options[:env]) && | |
233 | options[:script] != false && | |
234 | options[:inline_script] != false | |
235 | end | |
236 | ||
237 | private_class_method def self.recaptcha_v3_define_default_callback(callback) | |
238 | <<-HTML | |
239 | var #{callback} = function(id, token) { | |
240 | var element = document.getElementById(id); | |
241 | element.value = token; | |
242 | } | |
243 | HTML | |
244 | end | |
245 | ||
246 | # Returns true if we should be adding the default callback. | |
247 | # That is, if the given callback name is the default callback name (for the given action) and we | |
248 | # are not skipping inline scripts for any reason. | |
249 | private_class_method def self.recaptcha_v3_define_default_callback?(callback, action, options) | |
250 | callback == recaptcha_v3_default_callback_name(action) && | |
251 | recaptcha_v3_inline_script?(options) | |
252 | end | |
253 | ||
254 | # Returns the name of the JavaScript function that actually executes the | |
255 | # reCAPTCHA code (calls `grecaptcha.execute` or | |
256 | # `grecaptcha.enterprise.execute`). You can call it again later to reset it. | |
257 | def self.recaptcha_v3_execute_function_name(action) | |
258 | "executeRecaptchaFor#{sanitize_action_for_js(action)}" | |
259 | end | |
260 | ||
261 | # Returns the name of an async JavaScript function that executes the reCAPTCHA code. | |
262 | def self.recaptcha_v3_async_execute_function_name(action) | |
263 | "#{recaptcha_v3_execute_function_name(action)}Async" | |
264 | end | |
265 | ||
266 | def self.recaptcha_v3_default_callback_name(action) | |
267 | "setInputWithRecaptchaResponseTokenFor#{sanitize_action_for_js(action)}" | |
268 | end | |
269 | ||
270 | # v2 | |
271 | ||
272 | private_class_method def self.default_callback(options = {}) | |
273 | nonce = options[:nonce] | |
274 | nonce_attr = " nonce='#{nonce}'" if nonce | |
275 | selector_attr = options[:id] ? "##{options[:id]}" : ".g-recaptcha" | |
276 | ||
277 | <<-HTML | |
278 | <script#{nonce_attr}> | |
279 | var invisibleRecaptchaSubmit = function () { | |
280 | var closestForm = function (ele) { | |
281 | var curEle = ele.parentNode; | |
282 | while (curEle.nodeName !== 'FORM' && curEle.nodeName !== 'BODY'){ | |
283 | curEle = curEle.parentNode; | |
284 | } | |
285 | return curEle.nodeName === 'FORM' ? curEle : null | |
286 | }; | |
287 | ||
288 | var el = document.querySelector("#{selector_attr}") | |
289 | if (!!el) { | |
290 | var form = closestForm(el); | |
291 | if (form) { | |
292 | form.submit(); | |
293 | } | |
294 | } | |
295 | }; | |
296 | </script> | |
297 | HTML | |
298 | end | |
299 | ||
300 | def self.recaptcha_execute_method_name | |
301 | Recaptcha.configuration.enterprise ? "grecaptcha.enterprise.execute" : "grecaptcha.execute" | |
302 | end | |
303 | ||
304 | def self.recaptcha_ready_method_name | |
305 | Recaptcha.configuration.enterprise ? "grecaptcha.enterprise.ready" : "grecaptcha.ready" | |
306 | end | |
307 | ||
308 | private_class_method def self.default_callback_required?(options) | |
309 | options[:callback] == 'invisibleRecaptchaSubmit' && | |
310 | !Recaptcha.skip_env?(options[:env]) && | |
311 | options[:script] != false && | |
312 | options[:inline_script] != false | |
313 | end | |
314 | ||
315 | # Returns a camelized string that is safe for use in a JavaScript variable/function name. | |
316 | # sanitize_action_for_js('my/action') => 'MyAction' | |
317 | private_class_method def self.sanitize_action_for_js(action) | |
318 | action.to_s.gsub(/\W/, '_').split(/\/|_/).map(&:capitalize).join | |
319 | end | |
320 | ||
321 | # Returns a dasherized string that is safe for use as an HTML ID | |
322 | # dasherize_action('my/action') => 'my-action' | |
323 | private_class_method def self.dasherize_action(action) | |
324 | action.to_s.gsub(/\W/, '-').tr('_', '-') | |
325 | end | |
326 | ||
327 | private_class_method def self.hash_to_query(hash) | |
328 | hash.delete_if { |_, val| val.nil? || val.empty? }.to_a.map { |pair| pair.join('=') }.join('&') | |
329 | end | |
330 | end | |
331 | end |
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.8.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 | if Recaptcha.configuration.enterprise | |
63 | verify_via_api_call_enterprise(response, options) | |
64 | else | |
65 | verify_via_api_call_free(response, options) | |
66 | end | |
67 | end | |
68 | ||
69 | def self.verify_via_api_call_enterprise(response, options) | |
70 | site_key = options.fetch(:site_key) { configuration.site_key! } | |
71 | api_key = options.fetch(:enterprise_api_key) { configuration.enterprise_api_key! } | |
72 | project_id = options.fetch(:enterprise_project_id) { configuration.enterprise_project_id! } | |
73 | ||
74 | query_params = { 'key' => api_key } | |
75 | body = { 'event' => { 'token' => response, 'siteKey' => site_key } } | |
76 | body['event']['expectedAction'] = options[:action] if options.key?(:action) | |
77 | body['event']['userIpAddress'] = options[:remote_ip] if options.key?(:remote_ip) | |
78 | ||
79 | reply = api_verification_enterprise(query_params, body, project_id, timeout: options[:timeout]) | |
80 | token_properties = reply['tokenProperties'] | |
81 | success = !token_properties.nil? && | |
82 | token_properties['valid'].to_s == 'true' && | |
83 | hostname_valid?(token_properties['hostname'], options[:hostname]) && | |
84 | action_valid?(token_properties['action'], options[:action]) && | |
85 | score_above_threshold?(reply['score'], options[:minimum_score]) | |
86 | ||
87 | if options[:with_reply] == true | |
88 | return success, reply | |
89 | else | |
90 | return success | |
91 | end | |
92 | end | |
93 | ||
94 | def self.verify_via_api_call_free(response, options) | |
95 | secret_key = options.fetch(:secret_key) { configuration.secret_key! } | |
96 | verify_hash = { 'secret' => secret_key, 'response' => response } | |
97 | verify_hash['remoteip'] = options[:remote_ip] if options.key?(:remote_ip) | |
98 | ||
99 | reply = api_verification_free(verify_hash, timeout: options[:timeout]) | |
100 | success = reply['success'].to_s == 'true' && | |
101 | hostname_valid?(reply['hostname'], options[:hostname]) && | |
102 | action_valid?(reply['action'], options[:action]) && | |
103 | score_above_threshold?(reply['score'], options[:minimum_score]) | |
104 | ||
105 | if options[:with_reply] == true | |
106 | return success, reply | |
107 | else | |
108 | return success | |
109 | end | |
110 | end | |
111 | ||
112 | def self.hostname_valid?(hostname, validation) | |
113 | validation ||= configuration.hostname | |
114 | ||
115 | case validation | |
116 | when nil, FalseClass then true | |
117 | when String then validation == hostname | |
118 | else validation.call(hostname) | |
119 | end | |
120 | end | |
121 | ||
122 | def self.action_valid?(action, expected_action) | |
123 | case expected_action | |
124 | when nil, FalseClass then true | |
125 | else action == expected_action | |
126 | end | |
127 | end | |
128 | ||
129 | # Returns true iff score is greater or equal to (>=) minimum_score, or if no minimum_score was specified | |
130 | def self.score_above_threshold?(score, minimum_score) | |
131 | return true if minimum_score.nil? | |
132 | return false if score.nil? | |
133 | ||
134 | case minimum_score | |
135 | when nil, FalseClass then true | |
136 | else score >= minimum_score | |
137 | end | |
138 | end | |
139 | ||
140 | def self.http_client_for(uri:, timeout: nil) | |
141 | timeout ||= DEFAULT_TIMEOUT | |
142 | http = if configuration.proxy | |
143 | proxy_server = URI.parse(configuration.proxy) | |
55 | 144 | Net::HTTP::Proxy(proxy_server.host, proxy_server.port, proxy_server.user, proxy_server.password) |
56 | 145 | else |
57 | 146 | Net::HTTP |
58 | 147 | end |
59 | query = URI.encode_www_form(verify_hash) | |
60 | uri = URI.parse(Recaptcha.configuration.verify_url + '?' + query) | |
61 | http_instance = http.new(uri.host, uri.port) | |
62 | http_instance.read_timeout = http_instance.open_timeout = options[:timeout] || DEFAULT_TIMEOUT | |
63 | http_instance.use_ssl = true if uri.port == 443 | |
64 | request = Net::HTTP::Get.new(uri.request_uri) | |
65 | http_instance.request(request).body | |
148 | instance = http.new(uri.host, uri.port) | |
149 | instance.read_timeout = instance.open_timeout = timeout | |
150 | instance.use_ssl = true if uri.port == 443 | |
151 | ||
152 | instance | |
66 | 153 | end |
67 | 154 | |
68 | def self.i18n(key, default) | |
69 | if defined?(I18n) | |
70 | I18n.translate(key, default: default) | |
71 | else | |
72 | default | |
73 | end | |
155 | def self.api_verification_free(verify_hash, timeout: nil) | |
156 | query = URI.encode_www_form(verify_hash) | |
157 | uri = URI.parse(configuration.verify_url + '?' + query) | |
158 | http_instance = http_client_for(uri: uri, timeout: timeout) | |
159 | request = Net::HTTP::Get.new(uri.request_uri) | |
160 | JSON.parse(http_instance.request(request).body) | |
74 | 161 | end |
75 | 162 | |
76 | class RecaptchaError < StandardError | |
77 | end | |
78 | ||
79 | class VerifyError < RecaptchaError | |
163 | def self.api_verification_enterprise(query_params, body, project_id, timeout: nil) | |
164 | query = URI.encode_www_form(query_params) | |
165 | uri = URI.parse(configuration.verify_url + "/#{project_id}/assessments" + '?' + query) | |
166 | http_instance = http_client_for(uri: uri, timeout: timeout) | |
167 | request = Net::HTTP::Post.new(uri.request_uri) | |
168 | request['Content-Type'] = 'application/json; charset=utf-8' | |
169 | request.body = JSON.generate(body) | |
170 | JSON.parse(http_instance.request(request).body) | |
80 | 171 | end |
81 | 172 | end |
0 | en: | |
1 | recaptcha: | |
2 | errors: | |
3 | verification_failed: reCAPTCHA verification failed, please try again. | |
4 | recaptcha_unreachable: Oops, we failed to validate your reCAPTCHA response. Please try again. |
0 | fr: | |
1 | recaptcha: | |
2 | errors: | |
3 | verification_failed: La vérification reCAPTCHA a échoué, veuillez essayer à nouveau. | |
4 | recaptcha_unreachable: Oops, nous n'avons pas pu valider votre réponse reCAPTCHA. Veuillez essayer à nouveau. |
9 | 9 | s.homepage = "http://github.com/ambethia/recaptcha" |
10 | 10 | s.summary = s.description = "Helpers for the reCAPTCHA API" |
11 | 11 | s.license = "MIT" |
12 | s.required_ruby_version = '>= 2.3.0' | |
12 | s.required_ruby_version = '>= 2.4.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 | describe "turbolinks" do | |
81 | it "adds onload to defined function" do | |
82 | html = recaptcha_v3(action: 'request', turbolinks: true) | |
83 | html.must_include("onload=executeRecaptchaForRequest") | |
84 | end | |
85 | ||
86 | it "overrides specified onload" do | |
87 | html = recaptcha_v3(action: 'request', onload: "foobar", turbolinks: true) | |
88 | html.wont_include("onload=foobar") | |
89 | html.must_include("onload=executeRecaptchaForRequest") | |
90 | end | |
91 | end | |
92 | ||
93 | it "adds :render option to the url" do | |
94 | html = recaptcha_tags(render: 'onload') | |
95 | html.must_include("render=onload") | |
96 | ||
97 | html = recaptcha_tags(render: 'explicit') | |
98 | html.wont_include("render=onload") | |
99 | html.must_include("render=explicit") | |
100 | ||
101 | html = recaptcha_tags | |
102 | html.wont_include("render=") | |
103 | end | |
104 | ||
105 | it "adds query params to the url" do | |
106 | html = recaptcha_tags(hl: 'en', onload: 'foobar') | |
107 | html.must_include("?") | |
108 | html.must_include("hl=en") | |
109 | html.must_include("&") | |
110 | html.must_include("onload=foobar") | |
68 | 111 | end |
69 | 112 | |
70 | 113 | it "includes the site key in the button attributes" do |
71 | 114 | html = invisible_recaptcha_tags |
72 | 115 | html.must_include(" data-sitekey=\"#{Recaptcha.configuration.site_key}\"") |
116 | end | |
117 | ||
118 | it "lets you override the site_key from configuration via options hash" do | |
119 | html = invisible_recaptcha_tags(site_key: 'different_key') | |
120 | html.must_include(" data-sitekey=\"different_key\"") | |
73 | 121 | end |
74 | 122 | |
75 | 123 | it "dasherizes the expired_callback attribute name" do |
153 | 201 | html.wont_include("<button") |
154 | 202 | end |
155 | 203 | |
204 | it "renders an input element with supplied text if UI is input" do | |
205 | html = invisible_recaptcha_tags(ui: :input, text: 'Send') | |
206 | html.must_include("<input type=\"submit\"") | |
207 | html.must_include("value=\"Send\"/>") | |
208 | html.wont_include("<button") | |
209 | end | |
210 | ||
211 | it "includes a custom selector if provided" do | |
212 | html = invisible_recaptcha_tags(id: 'custom-selector') | |
213 | html.must_include("id=\"custom-selector\"") | |
214 | html.must_include("document.querySelector(\"#custom-selector\")") | |
215 | end | |
216 | ||
217 | it "uses default selector if no custom selector has been provided" do | |
218 | html = invisible_recaptcha_tags | |
219 | html.must_include("document.querySelector(\".g-recaptcha\")") | |
220 | end | |
221 | ||
156 | 222 | it "raises an error on an invalid ui option" do |
157 | 223 | assert_raises Recaptcha::RecaptchaError do |
158 | 224 | invisible_recaptcha_tags(ui: :foo) |
159 | 225 | end |
160 | 226 | end |
161 | 227 | end |
228 | ||
229 | describe "v3 recaptcha" do | |
230 | it "renders input" do | |
231 | html = recaptcha_v3 action: :foo | |
232 | html.must_include('<input type="hidden" name="g-recaptcha-response-data[foo]" id="g-recaptcha-response-data-foo" data-sitekey="0000000000000000000000000000000000000000" class="g-recaptcha g-recaptcha-response "/>') | |
233 | end | |
234 | ||
235 | it "does not have obsole closing script tag" do | |
236 | html = recaptcha_v3 action: :foo | |
237 | assert html.scan(/script/).length.even? | |
238 | end | |
239 | end | |
162 | 240 | end |
1 | 1 | |
2 | 2 | describe Recaptcha::Configuration do |
3 | 3 | describe "#api_server_url" do |
4 | it "serves the default" do | |
5 | Recaptcha.configuration.api_server_url.must_equal "https://www.google.com/recaptcha/api.js" | |
4 | it "serves the default (free API)" do | |
5 | Recaptcha.configuration.api_server_url.must_equal "https://www.recaptcha.net/recaptcha/api.js" | |
6 | end | |
7 | ||
8 | describe "when enterprise is set to true" do | |
9 | it "serves the enterprise API URL" do | |
10 | Recaptcha.with_configuration(enterprise: true) do | |
11 | Recaptcha.configuration.api_server_url.must_equal "https://www.recaptcha.net/recaptcha/enterprise.js" | |
12 | end | |
13 | end | |
6 | 14 | end |
7 | 15 | |
8 | 16 | describe "when api_server_url is overwritten" do |
9 | 17 | it "serves the overwritten url" do |
10 | 18 | proxied_api_server_url = 'https://127.0.0.1:8080/recaptcha/api.js' |
11 | Recaptcha.with_configuration(api_server_url: proxied_api_server_url) do | |
19 | Recaptcha.configuration.api_server_url = proxied_api_server_url | |
20 | begin | |
12 | 21 | Recaptcha.configuration.api_server_url.must_equal proxied_api_server_url |
22 | ensure | |
23 | Recaptcha.configuration.api_server_url = nil | |
13 | 24 | end |
14 | 25 | end |
15 | 26 | end |
17 | 28 | |
18 | 29 | describe "#verify_url" do |
19 | 30 | it "serves the default" do |
20 | Recaptcha.configuration.verify_url.must_equal "https://www.google.com/recaptcha/api/siteverify" | |
31 | Recaptcha.configuration.verify_url.must_equal "https://www.recaptcha.net/recaptcha/api/siteverify" | |
21 | 32 | end |
22 | 33 | |
23 | 34 | describe "when api_server_url is overwritten" do |
24 | 35 | it "serves the overwritten url" do |
25 | 36 | proxied_verify_url = 'https://127.0.0.1:8080/recaptcha/api/siteverify' |
26 | Recaptcha.with_configuration(verify_url: proxied_verify_url) do | |
37 | Recaptcha.configuration.verify_url = proxied_verify_url | |
38 | begin | |
27 | 39 | Recaptcha.configuration.verify_url.must_equal proxied_verify_url |
40 | ensure | |
41 | Recaptcha.configuration.verify_url = nil | |
28 | 42 | end |
29 | 43 | end |
30 | 44 | end |
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 | |
14 | 16 | def setup |
15 | 17 | super |
16 | 18 | Recaptcha.configure do |config| |
17 | config.site_key = '0000000000000000000000000000000000000000' | |
18 | config.secret_key = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' | |
19 | config.site_key = '0000000000000000000000000000000000000000' | |
20 | config.secret_key = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' | |
21 | config.enterprise_api_key = 'ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ' | |
22 | config.enterprise_project_id = 'test-project' | |
19 | 23 | end |
20 | 24 | end |
21 | 25 | end) |
0 | require_relative 'helper' | |
1 | ||
2 | describe 'controller helpers (enterprise)' do | |
3 | before do | |
4 | Recaptcha.configuration.enterprise = true | |
5 | ||
6 | @controller = TestController.new | |
7 | @controller.request = stub(remote_ip: "1.1.1.1", format: :html) | |
8 | @controller.params = {:recaptcha_response_field => "response", 'g-recaptcha-response-data' => 'string'} | |
9 | end | |
10 | ||
11 | after do | |
12 | Recaptcha.configuration.enterprise = false | |
13 | end | |
14 | ||
15 | describe "#verify_recaptcha!" do | |
16 | it "raises when it fails" do | |
17 | @controller.expects(:verify_recaptcha).returns(false) | |
18 | ||
19 | assert_raises Recaptcha::VerifyError do | |
20 | @controller.verify_recaptcha! | |
21 | end | |
22 | end | |
23 | ||
24 | it "returns a value when it passes" do | |
25 | @controller.expects(:verify_recaptcha).returns(:foo) | |
26 | ||
27 | assert_equal :foo, @controller.verify_recaptcha! | |
28 | end | |
29 | end | |
30 | ||
31 | describe "#verify_recaptcha" do | |
32 | it "returns true on success" do | |
33 | @controller.flash[:recaptcha_error] = "previous error that should be cleared" | |
34 | expect_http_post.to_return(body: '{"tokenProperties":{"valid":true}}') | |
35 | ||
36 | assert @controller.verify_recaptcha | |
37 | assert_nil @controller.flash[:recaptcha_error] | |
38 | end | |
39 | ||
40 | it "raises without api key" do | |
41 | Recaptcha.configuration.enterprise_api_key = nil | |
42 | assert_raises Recaptcha::RecaptchaError do | |
43 | @controller.verify_recaptcha | |
44 | end | |
45 | end | |
46 | ||
47 | it "returns false when secret key is invalid" do | |
48 | expect_http_post.to_return(body: %({"foo":"false", "bar":"invalid-site-secret-key"})) | |
49 | ||
50 | refute @controller.verify_recaptcha | |
51 | assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error] | |
52 | end | |
53 | ||
54 | it "adds an error to the model" do | |
55 | expect_http_post.to_return(body: %({"foo":"false", "bar":"bad-news"})) | |
56 | ||
57 | errors = mock | |
58 | errors.expects(:add).with(:base, "reCAPTCHA verification failed, please try again.") | |
59 | model = mock(errors: errors) | |
60 | ||
61 | refute @controller.verify_recaptcha(model: model) | |
62 | assert_nil @controller.flash[:recaptcha_error] | |
63 | end | |
64 | ||
65 | it "returns true on success with optional key" do | |
66 | key = 'ADIFFERENTPRIVATEKEYXXXXXXXXXXXXXX' | |
67 | @controller.flash[:recaptcha_error] = "previous error that should be cleared" | |
68 | expect_http_post(enterprise_api_key: key).to_return(body: '{"tokenProperties":{"valid":true}}') | |
69 | ||
70 | assert @controller.verify_recaptcha(enterprise_api_key: key) | |
71 | assert_nil @controller.flash[:recaptcha_error] | |
72 | end | |
73 | ||
74 | it "returns true on success without remote_ip" do | |
75 | @controller.flash[:recaptcha_error] = "previous error that should be cleared" | |
76 | expect_http_post.to_return(body: '{"tokenProperties":{"valid":true}}') | |
77 | ||
78 | assert @controller.verify_recaptcha(skip_remote_ip: true) | |
79 | assert_nil @controller.flash[:recaptcha_error] | |
80 | end | |
81 | ||
82 | it "fails silently when timing out" do | |
83 | expect_http_post.to_timeout | |
84 | refute @controller.verify_recaptcha | |
85 | @controller.flash[:recaptcha_error].must_equal( | |
86 | "Oops, we failed to validate your reCAPTCHA response. Please try again." | |
87 | ) | |
88 | end | |
89 | ||
90 | it "blows up on timeout when graceful is disabled" do | |
91 | Recaptcha.with_configuration(handle_timeouts_gracefully: false) do | |
92 | expect_http_post.to_timeout | |
93 | assert_raises Recaptcha::RecaptchaError, "Recaptcha unreachable." do | |
94 | assert @controller.verify_recaptcha | |
95 | end | |
96 | assert_nil @controller.flash[:recaptcha_error] | |
97 | end | |
98 | end | |
99 | ||
100 | it "uses I18n for the failed message" do | |
101 | I18n.locale = :de | |
102 | verification_failed_translated = "Sicherheitscode konnte nicht verifiziert werden." | |
103 | verification_failed_default = "reCAPTCHA verification failed, please try again." | |
104 | ||
105 | I18n.expects(:translate). | |
106 | with('recaptcha.errors.verification_failed', default: verification_failed_default). | |
107 | returns(verification_failed_translated) | |
108 | ||
109 | errors = mock | |
110 | errors.expects(:add).with(:base, verification_failed_translated) | |
111 | model = mock | |
112 | model.stubs(errors: errors) | |
113 | ||
114 | expect_http_post.to_return(body: %({"foo":"false", "bar":"bad-news"})) | |
115 | @controller.verify_recaptcha(model: model) | |
116 | end | |
117 | ||
118 | it "uses I18n for the timeout message" do | |
119 | I18n.locale = :de | |
120 | recaptcha_unreachable_translated = "Netzwerkfehler, bitte versuchen Sie es später erneut." | |
121 | recaptcha_unreachable_default = "Oops, we failed to validate your reCAPTCHA response. Please try again." | |
122 | ||
123 | I18n.expects(:translate). | |
124 | with('recaptcha.errors.recaptcha_unreachable', default: recaptcha_unreachable_default). | |
125 | returns(recaptcha_unreachable_translated) | |
126 | ||
127 | errors = mock | |
128 | errors.expects(:add).with(:base, recaptcha_unreachable_translated) | |
129 | model = mock | |
130 | model.stubs(errors: errors) | |
131 | ||
132 | expect_http_post.to_timeout | |
133 | @controller.verify_recaptcha(model: model) | |
134 | end | |
135 | ||
136 | it "translates api response with I18n" do | |
137 | api_error_translated = "Bad news, body :(" | |
138 | expect_http_post.to_return(body: %({"foo":"false", "bar":"bad-news"})) | |
139 | I18n.expects(:translate). | |
140 | with('recaptcha.errors.verification_failed', default: 'reCAPTCHA verification failed, please try again.'). | |
141 | returns(api_error_translated) | |
142 | ||
143 | refute @controller.verify_recaptcha | |
144 | assert_equal api_error_translated, @controller.flash[:recaptcha_error] | |
145 | end | |
146 | ||
147 | it "falls back to api response if i18n translation is missing" do | |
148 | expect_http_post.to_return(body: %({"foo":"false", "bar":"bad-news"})) | |
149 | ||
150 | refute @controller.verify_recaptcha | |
151 | assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error] | |
152 | end | |
153 | ||
154 | it "does not flash error when request was not html" do | |
155 | @controller.request = stub(remote_ip: "1.1.1.1", format: :json) | |
156 | expect_http_post.to_return(body: %({"foo":"false", "bar":"bad-news"})) | |
157 | refute @controller.verify_recaptcha | |
158 | assert_nil @controller.flash[:recaptcha_error] | |
159 | end | |
160 | ||
161 | it "does not verify via http call when user did not click anything" do | |
162 | @controller.params = { 'g-recaptcha-response' => ""} | |
163 | assert_not_requested :get, %r{\.google\.com} | |
164 | assert_equal false, @controller.verify_recaptcha | |
165 | assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error] | |
166 | end | |
167 | ||
168 | it "does not verify via http call when response length exceeds G_RESPONSE_LIMIT" do | |
169 | # this returns a 400 or 413 instead of a 200 response with error code | |
170 | # typical response length is less than 400 characters | |
171 | str = "a" * 4001 | |
172 | @controller.params = { 'g-recaptcha-response' => "#{str}"} | |
173 | assert_not_requested :get, %r{\.google\.com} | |
174 | assert_equal false, @controller.verify_recaptcha | |
175 | assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error] | |
176 | end | |
177 | ||
178 | describe ':hostname' do | |
179 | let(:hostname) { 'fake.hostname.com' } | |
180 | ||
181 | before do | |
182 | expect_http_post.to_return(body: %({"tokenProperties":{"valid":true,"hostname":"#{hostname}"}})) | |
183 | end | |
184 | ||
185 | it "passes with nil" do | |
186 | assert @controller.verify_recaptcha(hostname: nil) | |
187 | assert_nil @controller.flash[:recaptcha_error] | |
188 | end | |
189 | ||
190 | it "passes with false" do | |
191 | assert @controller.verify_recaptcha(hostname: false) | |
192 | assert_nil @controller.flash[:recaptcha_error] | |
193 | end | |
194 | ||
195 | it "check for equality when string custom hostname validation is passed" do | |
196 | assert @controller.verify_recaptcha(hostname: hostname) | |
197 | assert_nil @controller.flash[:recaptcha_error] | |
198 | end | |
199 | ||
200 | it "fails when custom hostname validation does not match" do | |
201 | expect_http_post.to_return(body: %({"success":true, "hostname": "not_#{hostname}"})) | |
202 | ||
203 | refute @controller.verify_recaptcha(hostname: hostname) | |
204 | assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error] | |
205 | end | |
206 | ||
207 | it "check with call when callable custom hostname validation is passed" do | |
208 | assert @controller.verify_recaptcha(hostname: -> (d) { d == hostname }) | |
209 | assert_nil @controller.flash[:recaptcha_error] | |
210 | end | |
211 | ||
212 | it "raises when invalid custom hostname validation is passed" do | |
213 | assert_raises Recaptcha::RecaptchaError do | |
214 | @controller.verify_recaptcha(hostname: 0) | |
215 | end | |
216 | end | |
217 | ||
218 | describe "when default hostname validation matches" do | |
219 | around { |test| Recaptcha.with_configuration(hostname: hostname, &test) } | |
220 | ||
221 | it "passes" do | |
222 | assert @controller.verify_recaptcha | |
223 | assert_nil @controller.flash[:recaptcha_error] | |
224 | end | |
225 | ||
226 | it "fails when custom validation does not match" do | |
227 | refute @controller.verify_recaptcha(hostname: "not_#{hostname}") | |
228 | assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error] | |
229 | end | |
230 | end | |
231 | ||
232 | describe "when default hostname validation does not match" do | |
233 | around { |test| Recaptcha.with_configuration(hostname: "not_#{hostname}", &test) } | |
234 | ||
235 | it "fails" do | |
236 | refute @controller.verify_recaptcha | |
237 | assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error] | |
238 | end | |
239 | ||
240 | it "passes when custom validation matches" do | |
241 | assert @controller.verify_recaptcha(hostname: hostname) | |
242 | assert_nil @controller.flash[:recaptcha_error] | |
243 | end | |
244 | end | |
245 | end | |
246 | ||
247 | describe 'action_valid?' do | |
248 | let(:default_response_hash) { | |
249 | { | |
250 | tokenProperties: { | |
251 | valid: true, | |
252 | action: 'homepage' | |
253 | } | |
254 | } | |
255 | } | |
256 | ||
257 | before do | |
258 | expect_http_post.to_return(body: success_body) | |
259 | end | |
260 | ||
261 | it "fails when action from response does not match expected action" do | |
262 | expect_http_post.to_return(body: success_body(action: "not_homepage")) | |
263 | ||
264 | refute verify_recaptcha(action: 'homepage') | |
265 | assert_flash_error | |
266 | end | |
267 | ||
268 | it "passes with string that matches" do | |
269 | assert verify_recaptcha(action: 'homepage') | |
270 | assert_nil @controller.flash[:recaptcha_error] | |
271 | end | |
272 | ||
273 | it "passes with nil" do | |
274 | assert verify_recaptcha(action: nil) | |
275 | assert_nil @controller.flash[:recaptcha_error] | |
276 | end | |
277 | ||
278 | it "passes with false" do | |
279 | assert verify_recaptcha(action: false) | |
280 | assert_nil @controller.flash[:recaptcha_error] | |
281 | end | |
282 | end | |
283 | ||
284 | describe 'score_above_threshold?' do | |
285 | let(:default_response_hash) { | |
286 | { | |
287 | tokenProperties: { | |
288 | valid: true, | |
289 | action: 'homepage' | |
290 | } | |
291 | } | |
292 | } | |
293 | ||
294 | it "fails when score is below minimum_score" do | |
295 | expect_http_post.to_return(body: success_body(score: 0.4)) | |
296 | ||
297 | refute verify_recaptcha(minimum_score: 0.5) | |
298 | assert_flash_error | |
299 | end | |
300 | ||
301 | it "fails when response doesn't include a score" do | |
302 | expect_http_post.to_return(body: success_body) | |
303 | ||
304 | refute verify_recaptcha(minimum_score: 0.4) | |
305 | assert_flash_error | |
306 | end | |
307 | ||
308 | it "passes with score exactly at minimum_score" do | |
309 | expect_http_post.to_return(body: success_body(score: 0.4)) | |
310 | ||
311 | assert verify_recaptcha(minimum_score: 0.4) | |
312 | assert_nil @controller.flash[:recaptcha_error] | |
313 | end | |
314 | ||
315 | it "passes when minimum_score not specified or nil" do | |
316 | expect_http_post.to_return(body: success_body(score: 0.4)) | |
317 | ||
318 | assert verify_recaptcha() | |
319 | assert_nil @controller.flash[:recaptcha_error] | |
320 | end | |
321 | ||
322 | it "passes with false" do | |
323 | expect_http_post.to_return(body: success_body(score: 0.4)) | |
324 | ||
325 | assert verify_recaptcha(minimum_score: false) | |
326 | assert_nil @controller.flash[:recaptcha_error] | |
327 | end | |
328 | end | |
329 | end | |
330 | ||
331 | describe "#recatcha_reply" do | |
332 | let(:default_response_hash) { | |
333 | { | |
334 | tokenProperties: { | |
335 | valid: true, | |
336 | action: 'homepage' | |
337 | }, | |
338 | score: 0.97 | |
339 | } | |
340 | } | |
341 | ||
342 | before do | |
343 | expect_http_post.to_return(body: success_body) | |
344 | end | |
345 | ||
346 | it "is initially nil" do | |
347 | assert_nil @controller.recaptcha_reply | |
348 | end | |
349 | ||
350 | it "contains the recaptcha reply once verify_recaptcha has been called" do | |
351 | assert verify_recaptcha() | |
352 | assert_equal default_response_hash.to_json, @controller.recaptcha_reply.to_json | |
353 | end | |
354 | end | |
355 | ||
356 | private | |
357 | ||
358 | class TestController | |
359 | include Recaptcha::Adapters::ControllerMethods | |
360 | ||
361 | attr_accessor :request, :params, :flash | |
362 | ||
363 | def initialize | |
364 | @flash = {} | |
365 | end | |
366 | ||
367 | public :verify_recaptcha | |
368 | public :verify_recaptcha! | |
369 | public :recaptcha_reply | |
370 | end | |
371 | ||
372 | def expect_http_post(enterprise_api_key: Recaptcha.configuration.enterprise_api_key, | |
373 | enterprise_project_id: Recaptcha.configuration.enterprise_project_id) | |
374 | stub_request( | |
375 | :post, | |
376 | "https://recaptchaenterprise.googleapis.com/v1beta1/projects/#{enterprise_project_id}/assessments?key=#{enterprise_api_key}" | |
377 | ) | |
378 | end | |
379 | ||
380 | def success_body(action: nil, score: nil) | |
381 | result = default_response_hash | |
382 | result[:tokenProperties][:action] = action if action | |
383 | result[:score] = score if score | |
384 | result.to_json | |
385 | end | |
386 | ||
387 | def verify_recaptcha(options = {}) | |
388 | options[:action] = 'homepage' unless options.key?(:action) | |
389 | @controller.verify_recaptcha(options) | |
390 | end | |
391 | ||
392 | def assert_flash_error | |
393 | assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error] | |
394 | end | |
395 | end |
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) |
6 | ||
7 | @expected_post_data = {} | |
8 | @expected_post_data["remoteip"] = @controller.request.remote_ip | |
9 | @expected_post_data["response"] = "response" | |
10 | ||
11 | @controller.params = {:recaptcha_response_field => "response", 'g-recaptcha-response' => 'string'} | |
12 | @expected_post_data["secret"] = Recaptcha.configuration.secret_key | |
13 | ||
14 | @expected_uri = URI.parse(Recaptcha.configuration.verify_url) | |
6 | @controller.params = {:recaptcha_response_field => "response", 'g-recaptcha-response-data' => 'string'} | |
15 | 7 | end |
16 | 8 | |
17 | 9 | describe "#verify_recaptcha!" do |
78 | 70 | secret_key = Recaptcha.configuration.secret_key |
79 | 71 | stub_request( |
80 | 72 | :get, |
81 | "https://www.google.com/recaptcha/api/siteverify?response=string&secret=#{secret_key}" | |
73 | "https://www.recaptcha.net/recaptcha/api/siteverify?response=string&secret=#{secret_key}" | |
82 | 74 | ).to_return(body: '{"success":true}') |
83 | 75 | |
84 | 76 | assert @controller.verify_recaptcha(skip_remote_ip: true) |
171 | 163 | assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error] |
172 | 164 | end |
173 | 165 | |
166 | it "does not verify via http call when response length exceeds G_RESPONSE_LIMIT" do | |
167 | # this returns a 400 or 413 instead of a 200 response with error code | |
168 | # typical response length is less than 400 characters | |
169 | str = "a" * 4001 | |
170 | @controller.params = { 'g-recaptcha-response' => "#{str}"} | |
171 | assert_not_requested :get, %r{\.google\.com} | |
172 | assert_equal false, @controller.verify_recaptcha | |
173 | assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error] | |
174 | end | |
175 | ||
174 | 176 | describe ':hostname' do |
175 | 177 | let(:hostname) { 'fake.hostname.com' } |
176 | 178 | |
239 | 241 | end |
240 | 242 | end |
241 | 243 | end |
244 | ||
245 | describe 'action_valid?' do | |
246 | let(:default_response_hash) { { | |
247 | success: true, | |
248 | action: 'homepage', | |
249 | } } | |
250 | ||
251 | before do | |
252 | expect_http_post.to_return(body: success_body) | |
253 | end | |
254 | ||
255 | it "fails when action from response does not match expected action" do | |
256 | expect_http_post.to_return(body: success_body(action: "not_homepage")) | |
257 | ||
258 | refute verify_recaptcha(action: 'homepage') | |
259 | assert_flash_error | |
260 | end | |
261 | ||
262 | it "passes with string that matches" do | |
263 | assert verify_recaptcha(action: 'homepage') | |
264 | assert_nil @controller.flash[:recaptcha_error] | |
265 | end | |
266 | ||
267 | it "passes with nil" do | |
268 | assert verify_recaptcha(action: nil) | |
269 | assert_nil @controller.flash[:recaptcha_error] | |
270 | end | |
271 | ||
272 | it "passes with false" do | |
273 | assert verify_recaptcha(action: false) | |
274 | assert_nil @controller.flash[:recaptcha_error] | |
275 | end | |
276 | end | |
277 | ||
278 | describe 'score_above_threshold?' do | |
279 | let(:default_response_hash) { { | |
280 | success: true, | |
281 | action: 'homepage', | |
282 | } } | |
283 | ||
284 | before do | |
285 | expect_http_post.to_return(body: success_body(score: 0.4)) | |
286 | end | |
287 | ||
288 | it "fails when score is below minimum_score" do | |
289 | refute verify_recaptcha(minimum_score: 0.5) | |
290 | assert_flash_error | |
291 | end | |
292 | ||
293 | it "fails when response doesn't include a score" do | |
294 | expect_http_post.to_return(body: success_body()) | |
295 | refute verify_recaptcha(minimum_score: 0.4) | |
296 | assert_flash_error | |
297 | end | |
298 | ||
299 | it "passes with score exactly at minimum_score" do | |
300 | assert verify_recaptcha(minimum_score: 0.4) | |
301 | assert_nil @controller.flash[:recaptcha_error] | |
302 | end | |
303 | ||
304 | it "passes when minimum_score not specified or nil" do | |
305 | assert verify_recaptcha() | |
306 | assert_nil @controller.flash[:recaptcha_error] | |
307 | end | |
308 | ||
309 | it "passes with false" do | |
310 | assert verify_recaptcha(minimum_score: false) | |
311 | assert_nil @controller.flash[:recaptcha_error] | |
312 | end | |
313 | end | |
314 | end | |
315 | ||
316 | describe "#recatcha_reply" do | |
317 | let(:default_response_hash) { { | |
318 | success: true, | |
319 | score: 0.97, | |
320 | action: 'homepage', | |
321 | } } | |
322 | ||
323 | before do | |
324 | expect_http_post.to_return(body: success_body) | |
325 | end | |
326 | ||
327 | it "is initially nil" do | |
328 | assert_nil @controller.recaptcha_reply | |
329 | end | |
330 | ||
331 | it "contains the recaptcha reply once verify_recaptcha has been called" do | |
332 | assert verify_recaptcha() | |
333 | assert_equal default_response_hash.to_json, @controller.recaptcha_reply.to_json | |
334 | end | |
335 | end | |
336 | ||
337 | describe "#recaptcha_response_token" do | |
338 | it "returns an empty string when params are empty and no action is provided" do | |
339 | @controller.params = {} | |
340 | assert_equal @controller.recaptcha_response_token, "" | |
341 | end | |
342 | ||
343 | it "returns an empty string when g-recaptcha-response-data is invalid and no action is provided" do | |
344 | @controller.params = { "g-recaptcha-response-data" => {} } | |
345 | assert_equal @controller.recaptcha_response_token, "" | |
346 | end | |
347 | ||
348 | it "returns an empty string when g-recaptcha-response is invalid and no action is provided" do | |
349 | @controller.params = { "g-recaptcha-response" => {} } | |
350 | assert_equal @controller.recaptcha_response_token, "" | |
351 | end | |
352 | ||
353 | it "returns the g-recaptcha-response-data when response is valid and no action is provided" do | |
354 | @controller.params = { "g-recaptcha-response-data" => "recaptcha-response-data" } | |
355 | assert_equal @controller.recaptcha_response_token, "recaptcha-response-data" | |
356 | end | |
357 | ||
358 | it "returns the g-recaptcha-response when response is valid and no action is provided" do | |
359 | @controller.params = { "g-recaptcha-response" => "recaptcha-response" } | |
360 | assert_equal @controller.recaptcha_response_token, "recaptcha-response" | |
361 | end | |
362 | ||
363 | it "returns an empty string when params are empty and an action is provided" do | |
364 | @controller.params = {} | |
365 | assert_equal @controller.recaptcha_response_token("test"), "" | |
366 | end | |
367 | ||
368 | it "returns an empty string when g-recaptcha-response-data params are invalid and an action is provided" do | |
369 | @controller.params = { "g-recaptcha-response-data" => ["\n"] } | |
370 | assert_equal @controller.recaptcha_response_token("test"), "" | |
371 | end | |
372 | ||
373 | it "returns an empty string when g-recaptcha-response-data params are nil and an action is provided" do | |
374 | @controller.params = { "g-recaptcha-response-data" => nil } | |
375 | assert_equal @controller.recaptcha_response_token("test"), "" | |
376 | end | |
377 | ||
378 | it "returns an empty string when g-recaptcha-response-data params are empty and an action is provided" do | |
379 | @controller.params = { "g-recaptcha-response-data" => {} } | |
380 | assert_equal @controller.recaptcha_response_token("test"), "" | |
381 | end | |
382 | ||
383 | it "returns an empty string when g-recaptcha-response-data params are valid but an invalid action is provided" do | |
384 | @controller.params = { "g-recaptcha-response-data" => { "test2" => "recaptcha-response-data" } } | |
385 | assert_equal @controller.recaptcha_response_token("test"), "" | |
386 | end | |
387 | ||
388 | it "returns an empty string when g-recaptcha-response params are valid but an invalid action is provided" do | |
389 | @controller.params = { "g-recaptcha-response" => { "test2" => "recaptcha-response-data" } } | |
390 | assert_equal @controller.recaptcha_response_token("test"), "" | |
391 | end | |
392 | ||
393 | it "returns the g-recaptcha-response-data action when params are valid and an action is provided" do | |
394 | @controller.params = { "g-recaptcha-response-data" => { "test" => "recaptcha-response-data" } } | |
395 | assert_equal @controller.recaptcha_response_token("test"), "recaptcha-response-data" | |
396 | end | |
397 | ||
398 | it "returns the g-recaptcha-response action when params are valid and an action is provided" do | |
399 | @controller.params = { "g-recaptcha-response" => { "test" => "recaptcha-response" } } | |
400 | assert_equal @controller.recaptcha_response_token("test"), "recaptcha-response" | |
401 | end | |
242 | 402 | end |
243 | 403 | |
244 | 404 | private |
245 | 405 | |
246 | 406 | class TestController |
247 | include Recaptcha::Verify | |
407 | include Recaptcha::Adapters::ControllerMethods | |
408 | ||
248 | 409 | attr_accessor :request, :params, :flash |
249 | 410 | |
250 | 411 | def initialize |
251 | 412 | @flash = {} |
252 | 413 | end |
414 | ||
415 | public :verify_recaptcha | |
416 | public :verify_recaptcha! | |
417 | public :recaptcha_reply | |
418 | public :recaptcha_response_token | |
253 | 419 | end |
254 | 420 | |
255 | 421 | def expect_http_post(secret_key: Recaptcha.configuration.secret_key) |
256 | 422 | stub_request( |
257 | 423 | :get, |
258 | "https://www.google.com/recaptcha/api/siteverify?remoteip=1.1.1.1&response=string&secret=#{secret_key}" | |
424 | "https://www.recaptcha.net/recaptcha/api/siteverify?remoteip=1.1.1.1&response=string&secret=#{secret_key}" | |
259 | 425 | ) |
260 | 426 | end |
427 | ||
428 | def success_body(other = {}) | |
429 | default_response_hash. | |
430 | merge(other). | |
431 | to_json | |
432 | end | |
433 | ||
434 | def verify_recaptcha(options = {}) | |
435 | options[:action] = 'homepage' unless options.key?(:action) | |
436 | @controller.verify_recaptcha(options) | |
437 | end | |
438 | ||
439 | def assert_flash_error | |
440 | assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error] | |
441 | end | |
261 | 442 | end |