invisible_captcha 0.12.2 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +8 -30
- data/Appraisals +4 -16
- data/CHANGELOG.md +29 -0
- data/Gemfile +2 -0
- data/LICENSE +1 -1
- data/README.md +127 -17
- data/Rakefile +2 -0
- data/gemfiles/{rails_3.2.gemfile → rails_6.1.gemfile} +1 -1
- data/invisible_captcha.gemspec +4 -7
- data/lib/invisible_captcha.rb +17 -3
- data/lib/invisible_captcha/controller_ext.rb +37 -19
- data/lib/invisible_captcha/form_helpers.rb +3 -1
- data/lib/invisible_captcha/railtie.rb +2 -0
- data/lib/invisible_captcha/version.rb +3 -1
- data/lib/invisible_captcha/view_helpers.rb +21 -2
- data/spec/controllers_spec.rb +79 -31
- data/spec/dummy/app/assets/config/manifest.js +2 -0
- data/spec/dummy/config/environments/test.rb +3 -8
- data/spec/invisible_captcha_spec.rb +2 -0
- data/spec/spec_helper.rb +3 -14
- data/spec/view_helpers_spec.rb +19 -1
- metadata +10 -39
- data/gemfiles/rails_4.2.gemfile +0 -7
- data/gemfiles/rails_5.0.gemfile +0 -7
- data/gemfiles/rails_5.1.gemfile +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 86acfe71e903568702c63c261bfd32a066dbc947d1c2fa2b4956178a7de591db
|
4
|
+
data.tar.gz: a9a8768c5bcfb9a7656e0638dde4bf40e169719385bf947dace7fa4d34f03656
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ec1ed1b9f7bef2e753f7b736dbce8124b5cd4c699e4075cbd15fdade99e6f332025e3088f0754315b18f8bde48b7e883c3470f840a535a3631f2e5c659c415df
|
7
|
+
data.tar.gz: 3698ed54f31c8f87730dcd92e14c2076e10aaa98a14a765e7d3be8bfd0e259385572b5d3252b8328fb4a539634bb8e086270b2e7e34117ab76a15ab4f42c095b
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -1,40 +1,18 @@
|
|
1
1
|
language: ruby
|
2
2
|
cache: bundler
|
3
|
-
sudo: false
|
4
3
|
rvm:
|
5
4
|
- ruby-head
|
6
|
-
-
|
7
|
-
- 2.
|
8
|
-
- 2.
|
9
|
-
- 2.
|
10
|
-
- 2.2.10
|
5
|
+
- 3.0.0
|
6
|
+
- 2.7.2
|
7
|
+
- 2.6.5
|
8
|
+
- 2.5.8
|
11
9
|
gemfile:
|
10
|
+
- gemfiles/rails_6.1.gemfile
|
12
11
|
- gemfiles/rails_6.0.gemfile
|
13
12
|
- gemfiles/rails_5.2.gemfile
|
14
|
-
- gemfiles/rails_5.1.gemfile
|
15
|
-
- gemfiles/rails_5.0.gemfile
|
16
|
-
- gemfiles/rails_4.2.gemfile
|
17
|
-
- gemfiles/rails_3.2.gemfile
|
18
|
-
before_install:
|
19
|
-
# Rails 4.x requires Bundler version < 2.0.
|
20
|
-
- "find /home/travis/.rvm/rubies -wholename '*default/bundler-*.gemspec' -delete"
|
21
|
-
- rvm @global do gem uninstall bundler -a -x
|
22
|
-
- rvm @global do yes | gem install bundler -v '< 2'
|
23
13
|
matrix:
|
24
14
|
exclude:
|
25
|
-
- rvm:
|
26
|
-
gemfile: gemfiles/
|
27
|
-
- rvm: 2.3.8
|
28
|
-
gemfile: gemfiles/rails_6.0.gemfile
|
29
|
-
- rvm: 2.2.10
|
30
|
-
gemfile: gemfiles/rails_6.0.gemfile
|
31
|
-
- rvm: ruby-head
|
32
|
-
gemfile: gemfiles/rails_3.2.gemfile
|
33
|
-
- rvm: 2.6.2
|
34
|
-
gemfile: gemfiles/rails_3.2.gemfile
|
35
|
-
- rvm: 2.5.5
|
36
|
-
gemfile: gemfiles/rails_3.2.gemfile
|
37
|
-
- rvm: 2.4.5
|
38
|
-
gemfile: gemfiles/rails_3.2.gemfile
|
15
|
+
- rvm: 3.0.0
|
16
|
+
gemfile: gemfiles/rails_5.2.gemfile
|
39
17
|
allow_failures:
|
40
|
-
- rvm: ruby-head
|
18
|
+
- rvm: ruby-head
|
data/Appraisals
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
appraise "rails-6.1" do
|
2
|
+
gem "rails", "~> 6.1.0"
|
3
|
+
end
|
4
|
+
|
1
5
|
appraise "rails-6.0" do
|
2
6
|
gem "rails", "~> 6.0.0"
|
3
7
|
end
|
@@ -5,19 +9,3 @@ end
|
|
5
9
|
appraise "rails-5.2" do
|
6
10
|
gem "rails", "~> 5.2.0"
|
7
11
|
end
|
8
|
-
|
9
|
-
appraise "rails-5.1" do
|
10
|
-
gem "rails", "~> 5.1.0"
|
11
|
-
end
|
12
|
-
|
13
|
-
appraise "rails-5.0" do
|
14
|
-
gem "rails", "~> 5.0.0"
|
15
|
-
end
|
16
|
-
|
17
|
-
appraise "rails-4.2" do
|
18
|
-
gem "rails", "~> 4.2.0"
|
19
|
-
end
|
20
|
-
|
21
|
-
appraise "rails-3.2" do
|
22
|
-
gem "rails", "~> 3.2.0"
|
23
|
-
end
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,30 @@
|
|
2
2
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
4
4
|
|
5
|
+
## [2.0.0]
|
6
|
+
|
7
|
+
- New spinner, IP based, validation check (#89)
|
8
|
+
- Drop official support for unmaintained Rails versions: 5.1, 5.0 and 4.2 (#86)
|
9
|
+
- Drop official support for EOL Rubies: 2.4 and 2.3 (#86)
|
10
|
+
|
11
|
+
## [1.1.0]
|
12
|
+
|
13
|
+
- New option `prepend: true` for the controller macro (#77)
|
14
|
+
|
15
|
+
## [1.0.1]
|
16
|
+
|
17
|
+
- Fix naming issue with Ruby 2.7 (#65)
|
18
|
+
|
19
|
+
## [1.0.0]
|
20
|
+
|
21
|
+
- Remove Ruby 2.2 and Rails 3.2 support
|
22
|
+
- Add Instrumentation event (#62)
|
23
|
+
|
24
|
+
## [0.13.0]
|
25
|
+
|
26
|
+
- Add support for the Content Security Policy nonce (#61)
|
27
|
+
- Freeze all strings (#60)
|
28
|
+
|
5
29
|
## [0.12.2]
|
6
30
|
|
7
31
|
- Allow new timestamp to be set during `on_timestamp_spam` callback (#53)
|
@@ -101,6 +125,11 @@ All notable changes to this project will be documented in this file.
|
|
101
125
|
|
102
126
|
- First version of controller filters
|
103
127
|
|
128
|
+
[2.0.0]: https://github.com/markets/invisible_captcha/compare/v1.1.0...v2.0.0
|
129
|
+
[1.1.0]: https://github.com/markets/invisible_captcha/compare/v1.0.1...v1.1.0
|
130
|
+
[1.0.1]: https://github.com/markets/invisible_captcha/compare/v1.0.0...v1.0.1
|
131
|
+
[1.0.0]: https://github.com/markets/invisible_captcha/compare/v0.13.0...v1.0.0
|
132
|
+
[0.13.0]: https://github.com/markets/invisible_captcha/compare/v0.12.2...v0.13.0
|
104
133
|
[0.12.2]: https://github.com/markets/invisible_captcha/compare/v0.12.1...v0.12.2
|
105
134
|
[0.12.1]: https://github.com/markets/invisible_captcha/compare/v0.12.0...v0.12.1
|
106
135
|
[0.12.0]: https://github.com/markets/invisible_captcha/compare/v0.11.0...v0.12.0
|
data/Gemfile
CHANGED
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -1,25 +1,27 @@
|
|
1
1
|
# Invisible Captcha
|
2
2
|
|
3
3
|
[![Gem](https://img.shields.io/gem/v/invisible_captcha.svg?style=flat-square)](https://rubygems.org/gems/invisible_captcha)
|
4
|
-
[![Build Status](https://travis-ci.
|
4
|
+
[![Build Status](https://travis-ci.com/markets/invisible_captcha.svg?branch=master)](https://travis-ci.com/markets/invisible_captcha)
|
5
5
|
|
6
|
-
>
|
6
|
+
> Complete and flexible spam protection solution for Rails applications.
|
7
7
|
|
8
8
|
Invisible Captcha provides different techniques to protect your application against spambots.
|
9
9
|
|
10
|
-
The main protection is a solution based on the `honeypot` principle, which provides a better user experience
|
10
|
+
The main protection is a solution based on the `honeypot` principle, which provides a better user experience since there are no extra steps for real users, only for the bots.
|
11
11
|
|
12
12
|
Essentially, the strategy consists on adding an input field :honey_pot: into the form that:
|
13
13
|
|
14
14
|
- shouldn't be visible by the real users
|
15
15
|
- should be left empty by the real users
|
16
|
-
- will most be filled by spam bots
|
16
|
+
- will most likely be filled by spam bots
|
17
17
|
|
18
|
-
It also comes with
|
18
|
+
It also comes with:
|
19
|
+
- a time-sensitive :hourglass: form submission
|
20
|
+
- an IP based :mag: spinner validation
|
19
21
|
|
20
22
|
## Installation
|
21
23
|
|
22
|
-
Invisible Captcha is tested against Rails `>=
|
24
|
+
Invisible Captcha is tested against Rails `>= 5.2` and Ruby `>= 2.5`.
|
23
25
|
|
24
26
|
Add this line to your Gemfile and then execute `bundle install`:
|
25
27
|
|
@@ -47,7 +49,7 @@ class TopicsController < ApplicationController
|
|
47
49
|
end
|
48
50
|
```
|
49
51
|
|
50
|
-
This method will act as a `before_action` that triggers when spam is detected (honeypot field has some value). By default it responds with no content (only headers: `head(200)`). This is a good default, since the bot will surely read the response code and will think that it has achieved to submit the form properly. But, anyway, you
|
52
|
+
This method will act as a `before_action` that triggers when spam is detected (honeypot field has some value). By default, it responds with no content (only headers: `head(200)`). This is a good default, since the bot will surely read the response code and will think that it has achieved to submit the form properly. But, anyway, you can define your own callback by passing a method to the `on_spam` option:
|
51
53
|
|
52
54
|
```ruby
|
53
55
|
class TopicsController < ApplicationController
|
@@ -61,7 +63,7 @@ class TopicsController < ApplicationController
|
|
61
63
|
end
|
62
64
|
```
|
63
65
|
|
64
|
-
Note that is not mandatory to specify a `honeypot` attribute (
|
66
|
+
Note that it is not mandatory to specify a `honeypot` attribute (neither in the view nor in the controller). In this case, the engine will take a random field from `InvisibleCaptcha.honeypots`. So, if you're integrating it following this path, in your form:
|
65
67
|
|
66
68
|
```erb
|
67
69
|
<%= form_tag(new_contact_path) do |f| %>
|
@@ -69,12 +71,32 @@ Note that is not mandatory to specify a `honeypot` attribute (nor in the view, n
|
|
69
71
|
<% end %>
|
70
72
|
```
|
71
73
|
|
72
|
-
In
|
74
|
+
In your controller:
|
73
75
|
|
74
76
|
```
|
75
77
|
invisible_captcha only: [:new_contact]
|
76
78
|
```
|
77
79
|
|
80
|
+
`invisible_captcha` sends all messages to `flash[:error]`. For messages to appear on your pages, add `<%= flash[:error] %>` to `app/views/layouts/application.html.erb` (somewhere near the top of your `<body>` element):
|
81
|
+
|
82
|
+
```erb
|
83
|
+
<!DOCTYPE html>
|
84
|
+
<html>
|
85
|
+
<head>
|
86
|
+
<title>Yet another Rails app</title>
|
87
|
+
<%= stylesheet_link_tag "application", media: "all" %>
|
88
|
+
<%= javascript_include_tag "application" %>
|
89
|
+
<%= csrf_meta_tags %>
|
90
|
+
</head>
|
91
|
+
<body>
|
92
|
+
<%= flash[:error] %>
|
93
|
+
<%= yield %>
|
94
|
+
</body>
|
95
|
+
</html>
|
96
|
+
```
|
97
|
+
|
98
|
+
You can place `<%= flash[:error] %>` next to `:alert` and `:notice` message types, if you have them in your `app/views/layouts/application.html.erb`.
|
99
|
+
|
78
100
|
## Options and customization
|
79
101
|
|
80
102
|
This section contains a description of all plugin options and customizations.
|
@@ -84,12 +106,14 @@ This section contains a description of all plugin options and customizations.
|
|
84
106
|
You can customize:
|
85
107
|
|
86
108
|
- `sentence_for_humans`: text for real users if input field was visible. By default, it uses I18n (see below).
|
87
|
-
- `honeypots`: collection of default honeypots. Used by the view helper, called with no args, to generate a random honeypot field name. By default, a random collection is already generated.
|
109
|
+
- `honeypots`: collection of default honeypots. Used by the view helper, called with no args, to generate a random honeypot field name. By default, a random collection is already generated. As the random collection is stored in memory, it will not work if you are running multiple Rails instances behind a load balancer. See [Multiple Rails instances](#multiple-rails-instances).
|
88
110
|
- `visual_honeypots`: make honeypots visible, also useful to test/debug your implementation.
|
89
111
|
- `timestamp_threshold`: fastest time (in seconds) to expect a human to submit the form (see [original article by Yoav Aner](https://blog.gingerlime.com/2012/simple-detection-of-comment-spam-in-rails/) outlining the idea). By default, 4 seconds. **NOTE:** It's recommended to deactivate the autocomplete feature to avoid false positives (`autocomplete="off"`).
|
90
112
|
- `timestamp_enabled`: option to disable the time threshold check at application level. Could be useful, for example, on some testing scenarios. By default, true.
|
91
113
|
- `timestamp_error_message`: flash error message thrown when form submitted quicker than the `timestamp_threshold` value. It uses I18n by default.
|
92
114
|
- `injectable_styles`: if enabled, you should call anywhere in your layout the following helper `<%= invisible_captcha_styles %>`. This allows you to inject styles, for example, in `<head>`. False by default, styles are injected inline with the honeypot.
|
115
|
+
- `spinner_enabled`: option to disable the IP spinner validation.
|
116
|
+
- `secret`: customize the secret key to encode some internal values. By default, it reads the environment variable `ENV['INVISIBLE_CAPTCHA_SECRET']` and fallbacks to random value. Be careful, if you are running multiple Rails instances behind a load balancer, use always the same value via the environment variable.
|
93
117
|
|
94
118
|
To change these defaults, add the following to an initializer (recommended `config/initializers/invisible_captcha.rb`):
|
95
119
|
|
@@ -97,9 +121,10 @@ To change these defaults, add the following to an initializer (recommended `conf
|
|
97
121
|
InvisibleCaptcha.setup do |config|
|
98
122
|
# config.honeypots << ['more', 'fake', 'attribute', 'names']
|
99
123
|
# config.visual_honeypots = false
|
100
|
-
# config.timestamp_threshold =
|
124
|
+
# config.timestamp_threshold = 2
|
101
125
|
# config.timestamp_enabled = true
|
102
126
|
# config.injectable_styles = false
|
127
|
+
# config.spinner_enabled = true
|
103
128
|
|
104
129
|
# Leave these unset if you want to use I18n (see below)
|
105
130
|
# config.sentence_for_humans = 'If you are a human, ignore this field'
|
@@ -107,6 +132,22 @@ InvisibleCaptcha.setup do |config|
|
|
107
132
|
end
|
108
133
|
```
|
109
134
|
|
135
|
+
#### Multiple Rails instances
|
136
|
+
|
137
|
+
If you have multiple Rails instances running behind a load balancer, you have to share the same honeypots collection between the instances.
|
138
|
+
|
139
|
+
Either use a fixed collection or share them between the instances using `Rails.cache`:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
InvisibleCaptcha.setup do |config|
|
143
|
+
config.honeypots = Rails.cache.fetch('invisible_captcha_honeypots') do
|
144
|
+
(1..20).map { InvisibleCaptcha.generate_random_honeypot }
|
145
|
+
end
|
146
|
+
end
|
147
|
+
```
|
148
|
+
|
149
|
+
Be careful also with the `secret` setting. Since it will be stored in-memory, if you are running this setup, the best idea is to provide the environment variable (`ENV['INVISIBLE_CAPTCHA_SECRET']`) from your infrastructure.
|
150
|
+
|
110
151
|
### Controller method options:
|
111
152
|
|
112
153
|
The `invisible_captcha` method accepts some options:
|
@@ -114,11 +155,12 @@ The `invisible_captcha` method accepts some options:
|
|
114
155
|
- `only`: apply to given controller actions.
|
115
156
|
- `except`: exclude to given controller actions.
|
116
157
|
- `honeypot`: name of custom honeypot.
|
117
|
-
- `scope`: name of scope, ie: 'topic[subtitle]' -> 'topic' is the scope.
|
158
|
+
- `scope`: name of scope, ie: 'topic[subtitle]' -> 'topic' is the scope. By default, it's inferred from the `controller_name`.
|
118
159
|
- `on_spam`: custom callback to be called on spam detection.
|
119
160
|
- `timestamp_enabled`: enable/disable this technique at action level.
|
120
161
|
- `on_timestamp_spam`: custom callback to be called when form submitted too quickly. The default action redirects to `:back` printing a warning in `flash[:error]`.
|
121
162
|
- `timestamp_threshold`: custom threshold per controller/action. Overrides the global value for `InvisibleCaptcha.timestamp_threshold`.
|
163
|
+
- `prepend`: the spam detection will run in a `prepend_before_action` if `prepend: true`, otherwise will run in a `before_action`.
|
122
164
|
|
123
165
|
### View helpers options:
|
124
166
|
|
@@ -145,6 +187,71 @@ You can also pass html options to the input:
|
|
145
187
|
<%= invisible_captcha :subtitle, :topic, id: "your_id", class: "your_class" %>
|
146
188
|
```
|
147
189
|
|
190
|
+
### Spam detection notifications
|
191
|
+
|
192
|
+
In addition to the `on_spam` controller callback, you can use the [Active Support Instrumentation API](https://guides.rubyonrails.org/active_support_instrumentation.html) to set up a global event handler that fires whenever spam is detected. This is useful for advanced logging, background processing, etc.
|
193
|
+
|
194
|
+
To set up a global event handler, [subscribe](https://guides.rubyonrails.org/active_support_instrumentation.html#subscribing-to-an-event) to the `invisible_captcha.spam_detected` event in an initializer:
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
# config/initializers/invisible_captcha.rb
|
198
|
+
|
199
|
+
ActiveSupport::Notifications.subscribe('invisible_captcha.spam_detected') do |*args, data|
|
200
|
+
AwesomeLogger.warn(data[:message], data) # Log to an external logging service.
|
201
|
+
SpamRequest.create(data) # Record the blocked request in your database.
|
202
|
+
end
|
203
|
+
```
|
204
|
+
|
205
|
+
The `data` passed to the subscriber is hash containing information about the request that was detected as spam. For example:
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
{
|
209
|
+
message: "Invisible Captcha honeypot param 'subtitle' was present.",
|
210
|
+
remote_ip: '127.0.0.1',
|
211
|
+
user_agent: 'Chrome 77',
|
212
|
+
controller: 'users',
|
213
|
+
action: 'create',
|
214
|
+
url: 'http://example.com/users',
|
215
|
+
params: {
|
216
|
+
topic: { subtitle: 'foo' },
|
217
|
+
controller: 'users',
|
218
|
+
action: 'create'
|
219
|
+
}
|
220
|
+
}
|
221
|
+
```
|
222
|
+
|
223
|
+
_**Note:** `params` will be filtered according to your `Rails.application.config.filter_parameters` configuration, making them (probably) safe for logging. But always double-check that you're not inadvertently logging sensitive form data, like passwords and credit cards._
|
224
|
+
|
225
|
+
### Content Security Policy
|
226
|
+
|
227
|
+
If you're using a Content Security Policy (CSP) in your Rails app, you will need to generate a nonce on the server, and pass `nonce: true` attribute to the view helper. Uncomment the following lines in your `config/initializers/content_security_policy.rb` file:
|
228
|
+
|
229
|
+
```ruby
|
230
|
+
# Be sure to restart your server when you modify this file.
|
231
|
+
|
232
|
+
# If you are using UJS then enable automatic nonce generation
|
233
|
+
Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
|
234
|
+
|
235
|
+
# Set the nonce only to specific directives
|
236
|
+
Rails.application.config.content_security_policy_nonce_directives = %w(style-src)
|
237
|
+
```
|
238
|
+
Note that if you are already generating nonce for scripts, you'd have to include `script-src` to `content_security_policy_nonce_directives` as well:
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
Rails.application.config.content_security_policy_nonce_directives = %w(script-src style-src)
|
242
|
+
```
|
243
|
+
|
244
|
+
And in your view helper, you need to pass `nonce: true` to the `invisible_captcha` helper:
|
245
|
+
|
246
|
+
```erb
|
247
|
+
<%= invisible_captcha nonce: true %>
|
248
|
+
```
|
249
|
+
|
250
|
+
**WARNING:** Content Security Policy can break your site! If you already run a website with third-party scripts, styles, images, and fonts, it is highly recommended to enable CSP in report-only mode and observe warnings as they appear. Learn more at MDN:
|
251
|
+
|
252
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
|
253
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
|
254
|
+
|
148
255
|
### I18n
|
149
256
|
|
150
257
|
`invisible_captcha` tries to use I18n when it's available by default. The keys it looks for are the following:
|
@@ -156,17 +263,20 @@ en:
|
|
156
263
|
timestamp_error_message: "Sorry, that was too quick! Please resubmit."
|
157
264
|
```
|
158
265
|
|
159
|
-
You can override the
|
266
|
+
You can override the English ones in your i18n config files as well as add new ones for other locales.
|
160
267
|
|
161
268
|
If you intend to use I18n with `invisible_captcha`, you _must not_ set `sentence_for_humans` or `timestamp_error_message` to strings in the setup phase.
|
162
269
|
|
163
270
|
## Testing your controllers
|
164
271
|
|
165
|
-
If you're encountering unexpected behaviour while testing controllers that use the `invisible_captcha` action filter, you may want to disable timestamp check for the test environment:
|
272
|
+
If you're encountering unexpected behaviour while testing controllers that use the `invisible_captcha` action filter, you may want to disable timestamp check for the test environment. Add the following snippet to the `config/initializers/invisible_captcha.rb` file:
|
166
273
|
|
167
274
|
```ruby
|
168
|
-
#
|
169
|
-
|
275
|
+
# Be sure to restart your server when you modify this file.
|
276
|
+
|
277
|
+
InvisibleCaptcha.setup do |config|
|
278
|
+
config.timestamp_enabled = !Rails.env.test?
|
279
|
+
end
|
170
280
|
```
|
171
281
|
|
172
282
|
Another option is to wait for the timestamp check to be valid:
|
@@ -204,7 +314,7 @@ $ bundle exec appraisal rspec
|
|
204
314
|
Run specs against specific version:
|
205
315
|
|
206
316
|
```
|
207
|
-
$ bundle exec appraisal rails-
|
317
|
+
$ bundle exec appraisal rails-6.0 rspec
|
208
318
|
```
|
209
319
|
|
210
320
|
### Demo
|
data/Rakefile
CHANGED
data/invisible_captcha.gemspec
CHANGED
@@ -5,8 +5,8 @@ Gem::Specification.new do |spec|
|
|
5
5
|
spec.version = InvisibleCaptcha::VERSION
|
6
6
|
spec.authors = ["Marc Anguera Insa"]
|
7
7
|
spec.email = ["srmarc.ai@gmail.com"]
|
8
|
-
spec.description = "Unobtrusive, flexible and
|
9
|
-
spec.summary = "
|
8
|
+
spec.description = "Unobtrusive, flexible and complete spam protection for Rails applications using honeypot strategy for better user experience."
|
9
|
+
spec.summary = "Honeypot spam protection for Rails"
|
10
10
|
spec.homepage = "https://github.com/markets/invisible_captcha"
|
11
11
|
spec.license = "MIT"
|
12
12
|
|
@@ -15,11 +15,8 @@ Gem::Specification.new do |spec|
|
|
15
15
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
16
16
|
spec.require_paths = ["lib"]
|
17
17
|
|
18
|
-
spec.add_dependency 'rails', '>=
|
18
|
+
spec.add_dependency 'rails', '>= 5.0'
|
19
19
|
|
20
|
-
spec.add_development_dependency 'rspec-rails'
|
20
|
+
spec.add_development_dependency 'rspec-rails'
|
21
21
|
spec.add_development_dependency 'appraisal'
|
22
|
-
spec.add_development_dependency 'test-unit', '~> 3.0'
|
23
|
-
spec.add_development_dependency 'byebug'
|
24
22
|
end
|
25
|
-
|
data/lib/invisible_captcha.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'invisible_captcha/version'
|
2
4
|
require 'invisible_captcha/controller_ext'
|
3
5
|
require 'invisible_captcha/view_helpers'
|
@@ -13,7 +15,9 @@ module InvisibleCaptcha
|
|
13
15
|
:timestamp_threshold,
|
14
16
|
:timestamp_enabled,
|
15
17
|
:visual_honeypots,
|
16
|
-
:injectable_styles
|
18
|
+
:injectable_styles,
|
19
|
+
:spinner_enabled,
|
20
|
+
:secret
|
17
21
|
|
18
22
|
def init!
|
19
23
|
# Default sentence for real users if text field was visible
|
@@ -31,9 +35,15 @@ module InvisibleCaptcha
|
|
31
35
|
# Make honeypots visibles
|
32
36
|
self.visual_honeypots = false
|
33
37
|
|
34
|
-
# If enabled, you should call anywhere in
|
35
|
-
#
|
38
|
+
# If enabled, you should call anywhere in your layout the following helper, to inject the honeypot styles:
|
39
|
+
# <%= invisible_captcha_styles %>
|
36
40
|
self.injectable_styles = false
|
41
|
+
|
42
|
+
# Spinner check enabled by default
|
43
|
+
self.spinner_enabled = true
|
44
|
+
|
45
|
+
# A secret key to encode some internal values
|
46
|
+
self.secret = ENV['INVISIBLE_CAPTCHA_SECRET'] || SecureRandom.hex(64)
|
37
47
|
end
|
38
48
|
|
39
49
|
def sentence_for_humans
|
@@ -68,6 +78,10 @@ module InvisibleCaptcha
|
|
68
78
|
].sample
|
69
79
|
end
|
70
80
|
|
81
|
+
def encode(value)
|
82
|
+
Digest::MD5.hexdigest("#{self.secret}-#{value}")
|
83
|
+
end
|
84
|
+
|
71
85
|
private
|
72
86
|
|
73
87
|
def call_lambda_or_return(obj)
|
@@ -1,13 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module InvisibleCaptcha
|
2
4
|
module ControllerExt
|
3
5
|
module ClassMethods
|
4
6
|
def invisible_captcha(options = {})
|
5
|
-
if
|
6
|
-
|
7
|
+
if options.key?(:prepend)
|
8
|
+
prepend_before_action(options) do
|
7
9
|
detect_spam(options)
|
8
10
|
end
|
9
11
|
else
|
10
|
-
|
12
|
+
before_action(options) do
|
11
13
|
detect_spam(options)
|
12
14
|
end
|
13
15
|
end
|
@@ -19,7 +21,7 @@ module InvisibleCaptcha
|
|
19
21
|
def detect_spam(options = {})
|
20
22
|
if timestamp_spam?(options)
|
21
23
|
on_timestamp_spam(options)
|
22
|
-
elsif honeypot_spam?(options)
|
24
|
+
elsif honeypot_spam?(options) || spinner_spam?
|
23
25
|
on_spam(options)
|
24
26
|
end
|
25
27
|
end
|
@@ -28,11 +30,7 @@ module InvisibleCaptcha
|
|
28
30
|
if action = options[:on_timestamp_spam]
|
29
31
|
send(action)
|
30
32
|
else
|
31
|
-
|
32
|
-
redirect_back(fallback_location: root_path, flash: { error: InvisibleCaptcha.timestamp_error_message })
|
33
|
-
else
|
34
|
-
redirect_to :back, flash: { error: InvisibleCaptcha.timestamp_error_message }
|
35
|
-
end
|
33
|
+
redirect_back(fallback_location: root_path, flash: { error: InvisibleCaptcha.timestamp_error_message })
|
36
34
|
end
|
37
35
|
end
|
38
36
|
|
@@ -53,24 +51,33 @@ module InvisibleCaptcha
|
|
53
51
|
|
54
52
|
return false unless enabled
|
55
53
|
|
56
|
-
|
54
|
+
timestamp = session.delete(:invisible_captcha_timestamp)
|
57
55
|
|
58
56
|
# Consider as spam if timestamp not in session, cause that means the form was not fetched at all
|
59
|
-
unless
|
60
|
-
|
57
|
+
unless timestamp
|
58
|
+
warn_spam("Invisible Captcha timestamp not found in session.")
|
61
59
|
return true
|
62
60
|
end
|
63
61
|
|
64
|
-
time_to_submit = Time.zone.now - DateTime.iso8601(
|
62
|
+
time_to_submit = Time.zone.now - DateTime.iso8601(timestamp)
|
65
63
|
threshold = options[:timestamp_threshold] || InvisibleCaptcha.timestamp_threshold
|
66
64
|
|
67
65
|
# Consider as spam if form submitted too quickly
|
68
66
|
if time_to_submit < threshold
|
69
|
-
|
67
|
+
warn_spam("Invisible Captcha timestamp threshold not reached (took #{time_to_submit.to_i}s).")
|
70
68
|
return true
|
71
69
|
end
|
72
70
|
|
73
|
-
|
71
|
+
false
|
72
|
+
end
|
73
|
+
|
74
|
+
def spinner_spam?
|
75
|
+
if InvisibleCaptcha.spinner_enabled && params[:spinner] != session[:invisible_captcha_spinner]
|
76
|
+
warn_spam("Invisible Captcha spinner value mismatch")
|
77
|
+
return true
|
78
|
+
end
|
79
|
+
|
80
|
+
false
|
74
81
|
end
|
75
82
|
|
76
83
|
def honeypot_spam?(options = {})
|
@@ -81,8 +88,8 @@ module InvisibleCaptcha
|
|
81
88
|
# If honeypot is defined for this controller-action, search for:
|
82
89
|
# - honeypot: params[:subtitle]
|
83
90
|
# - honeypot with scope: params[:topic][:subtitle]
|
84
|
-
if params[honeypot].present? || (
|
85
|
-
|
91
|
+
if params[honeypot].present? || params.dig(scope, honeypot).present?
|
92
|
+
warn_spam("Invisible Captcha honeypot param '#{honeypot}' was present.")
|
86
93
|
return true
|
87
94
|
else
|
88
95
|
# No honeypot spam detected, remove honeypot from params to avoid UnpermittedParameters exceptions
|
@@ -92,7 +99,7 @@ module InvisibleCaptcha
|
|
92
99
|
else
|
93
100
|
InvisibleCaptcha.honeypots.each do |default_honeypot|
|
94
101
|
if params[default_honeypot].present?
|
95
|
-
|
102
|
+
warn_spam("Invisible Captcha honeypot param '#{default_honeypot}' was present.")
|
96
103
|
return true
|
97
104
|
end
|
98
105
|
end
|
@@ -101,8 +108,19 @@ module InvisibleCaptcha
|
|
101
108
|
false
|
102
109
|
end
|
103
110
|
|
104
|
-
def
|
111
|
+
def warn_spam(message)
|
105
112
|
logger.warn("Potential spam detected for IP #{request.remote_ip}. #{message}")
|
113
|
+
|
114
|
+
ActiveSupport::Notifications.instrument(
|
115
|
+
'invisible_captcha.spam_detected',
|
116
|
+
message: message,
|
117
|
+
remote_ip: request.remote_ip,
|
118
|
+
user_agent: request.user_agent,
|
119
|
+
controller: params[:controller],
|
120
|
+
action: params[:action],
|
121
|
+
url: request.url,
|
122
|
+
params: request.filtered_parameters
|
123
|
+
)
|
106
124
|
end
|
107
125
|
end
|
108
126
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module InvisibleCaptcha
|
2
4
|
module ViewHelpers
|
3
5
|
# Builds the honeypot html
|
@@ -8,9 +10,17 @@ module InvisibleCaptcha
|
|
8
10
|
#
|
9
11
|
# @return [String] the generated html
|
10
12
|
def invisible_captcha(honeypot = nil, scope = nil, options = {})
|
11
|
-
|
13
|
+
@captcha_ocurrences = 0 unless defined?(@captcha_ocurrences)
|
14
|
+
@captcha_ocurrences += 1
|
15
|
+
|
16
|
+
if InvisibleCaptcha.timestamp_enabled || InvisibleCaptcha.spinner_enabled
|
12
17
|
session[:invisible_captcha_timestamp] = Time.zone.now.iso8601
|
13
18
|
end
|
19
|
+
|
20
|
+
if InvisibleCaptcha.spinner_enabled && @captcha_ocurrences == 1
|
21
|
+
session[:invisible_captcha_spinner] = InvisibleCaptcha.encode("#{session[:invisible_captcha_timestamp]}-#{current_request.remote_ip}")
|
22
|
+
end
|
23
|
+
|
14
24
|
build_invisible_captcha(honeypot, scope, options)
|
15
25
|
end
|
16
26
|
|
@@ -22,6 +32,10 @@ module InvisibleCaptcha
|
|
22
32
|
|
23
33
|
private
|
24
34
|
|
35
|
+
def current_request
|
36
|
+
@request ||= request
|
37
|
+
end
|
38
|
+
|
25
39
|
def build_invisible_captcha(honeypot = nil, scope = nil, options = {})
|
26
40
|
if honeypot.is_a?(Hash)
|
27
41
|
options = honeypot
|
@@ -42,6 +56,9 @@ module InvisibleCaptcha
|
|
42
56
|
concat styles unless InvisibleCaptcha.injectable_styles
|
43
57
|
concat label_tag(build_label_name(honeypot, scope), label)
|
44
58
|
concat text_field_tag(build_input_name(honeypot, scope), nil, default_honeypot_options.merge(options))
|
59
|
+
if InvisibleCaptcha.spinner_enabled
|
60
|
+
concat hidden_field_tag("spinner", session[:invisible_captcha_spinner])
|
61
|
+
end
|
45
62
|
end
|
46
63
|
end
|
47
64
|
|
@@ -54,7 +71,9 @@ module InvisibleCaptcha
|
|
54
71
|
|
55
72
|
return if visible
|
56
73
|
|
57
|
-
|
74
|
+
nonce = content_security_policy_nonce if options[:nonce]
|
75
|
+
|
76
|
+
content_tag(:style, media: 'screen', nonce: nonce) do
|
58
77
|
".#{css_class} {#{InvisibleCaptcha.css_strategy}}"
|
59
78
|
end
|
60
79
|
end
|
data/spec/controllers_spec.rb
CHANGED
@@ -1,39 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
|
2
4
|
render_views
|
3
5
|
|
4
|
-
def switchable_post(action, params = {})
|
5
|
-
if ::Rails::VERSION::STRING > '5'
|
6
|
-
post action, params: params
|
7
|
-
else
|
8
|
-
post action, params
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
|
-
def switchable_put(action, params = {})
|
13
|
-
if ::Rails::VERSION::STRING > '5'
|
14
|
-
put action, params: params
|
15
|
-
else
|
16
|
-
put action, params
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
6
|
before(:each) do
|
21
7
|
@controller = TopicsController.new
|
22
8
|
request.env['HTTP_REFERER'] = 'http://test.host/topics'
|
9
|
+
|
23
10
|
InvisibleCaptcha.init!
|
24
11
|
InvisibleCaptcha.timestamp_threshold = 1
|
12
|
+
InvisibleCaptcha.spinner_enabled = false
|
25
13
|
end
|
26
14
|
|
27
15
|
context 'without invisible_captcha_timestamp in session' do
|
28
16
|
it 'fails like if it was submitted too fast' do
|
29
|
-
|
17
|
+
post :create, params: { topic: { title: 'foo' } }
|
30
18
|
|
31
19
|
expect(response).to redirect_to 'http://test.host/topics'
|
32
20
|
expect(flash[:error]).to eq(InvisibleCaptcha.timestamp_error_message)
|
33
21
|
end
|
34
22
|
|
35
23
|
it 'passes if disabled at action level' do
|
36
|
-
|
24
|
+
post :copy, params: { topic: { title: 'foo' } }
|
37
25
|
|
38
26
|
expect(flash[:error]).not_to be_present
|
39
27
|
expect(response.body).to be_present
|
@@ -41,7 +29,8 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
|
|
41
29
|
|
42
30
|
it 'passes if disabled at app level' do
|
43
31
|
InvisibleCaptcha.timestamp_enabled = false
|
44
|
-
|
32
|
+
|
33
|
+
post :create, params: { topic: { title: 'foo' } }
|
45
34
|
|
46
35
|
expect(flash[:error]).not_to be_present
|
47
36
|
expect(response.body).to be_present
|
@@ -54,7 +43,7 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
|
|
54
43
|
end
|
55
44
|
|
56
45
|
it 'fails if submission before timestamp_threshold' do
|
57
|
-
|
46
|
+
post :create, params: { topic: { title: 'foo' } }
|
58
47
|
|
59
48
|
expect(response).to redirect_to 'http://test.host/topics'
|
60
49
|
expect(flash[:error]).to eq(InvisibleCaptcha.timestamp_error_message)
|
@@ -64,7 +53,7 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
|
|
64
53
|
end
|
65
54
|
|
66
55
|
it 'allows a custom on_timestamp_spam callback' do
|
67
|
-
|
56
|
+
put :update, params: { id: 1, topic: { title: 'bar' } }
|
68
57
|
|
69
58
|
expect(response.status).to eq(204)
|
70
59
|
end
|
@@ -77,7 +66,7 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
|
|
77
66
|
end
|
78
67
|
end
|
79
68
|
|
80
|
-
expect {
|
69
|
+
expect { put :update, params: { id: 1, topic: { title: 'bar' } } }
|
81
70
|
.to change { session[:invisible_captcha_timestamp] }
|
82
71
|
.to be_present
|
83
72
|
end
|
@@ -86,10 +75,12 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
|
|
86
75
|
it 'passes if submission on or after timestamp_threshold' do
|
87
76
|
sleep InvisibleCaptcha.timestamp_threshold
|
88
77
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
78
|
+
post :create, params: {
|
79
|
+
topic: {
|
80
|
+
title: 'foobar',
|
81
|
+
author: 'author',
|
82
|
+
body: 'body that passes validation'
|
83
|
+
}
|
93
84
|
}
|
94
85
|
|
95
86
|
expect(flash[:error]).not_to be_present
|
@@ -102,7 +93,7 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
|
|
102
93
|
it 'allow to set a custom timestamp_threshold per action' do
|
103
94
|
sleep 2 # custom threshold
|
104
95
|
|
105
|
-
|
96
|
+
post :publish, params: { id: 1 }
|
106
97
|
|
107
98
|
expect(flash[:error]).not_to be_present
|
108
99
|
expect(response.body).to be_present
|
@@ -113,33 +104,90 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
|
|
113
104
|
context 'honeypot attribute' do
|
114
105
|
before(:each) do
|
115
106
|
session[:invisible_captcha_timestamp] = Time.zone.now.iso8601
|
107
|
+
|
116
108
|
# Wait for valid submission
|
117
109
|
sleep InvisibleCaptcha.timestamp_threshold
|
118
110
|
end
|
119
111
|
|
120
112
|
it 'fails with spam' do
|
121
|
-
|
113
|
+
post :create, params: { topic: { subtitle: 'foo' } }
|
122
114
|
|
123
115
|
expect(response.body).to be_blank
|
124
116
|
end
|
125
117
|
|
126
118
|
it 'passes with no spam' do
|
127
|
-
|
119
|
+
post :create, params: { topic: { title: 'foo' } }
|
128
120
|
|
129
121
|
expect(response.body).to be_present
|
130
122
|
end
|
131
123
|
|
132
124
|
it 'allow a custom on_spam callback' do
|
133
|
-
|
125
|
+
put :update, params: { id: 1, topic: { subtitle: 'foo' } }
|
134
126
|
|
135
127
|
expect(response.body).to redirect_to(new_topic_path)
|
136
128
|
end
|
137
129
|
|
138
130
|
it 'honeypot is removed from params if you use a custom honeypot' do
|
139
|
-
|
131
|
+
post :create, params: { topic: { title: 'foo', subtitle: '' } }
|
140
132
|
|
141
133
|
expect(flash[:error]).not_to be_present
|
142
134
|
expect(@controller.params[:topic].key?(:subtitle)).to eq(false)
|
143
135
|
end
|
136
|
+
|
137
|
+
describe 'ActiveSupport::Notifications' do
|
138
|
+
let(:dummy_handler) { double(handle_event: nil) }
|
139
|
+
|
140
|
+
let!(:subscriber) do
|
141
|
+
subscriber = ActiveSupport::Notifications.subscribe('invisible_captcha.spam_detected') do |*args, data|
|
142
|
+
dummy_handler.handle_event(data)
|
143
|
+
end
|
144
|
+
|
145
|
+
subscriber
|
146
|
+
end
|
147
|
+
|
148
|
+
after { ActiveSupport::Notifications.unsubscribe(subscriber) }
|
149
|
+
|
150
|
+
it 'dispatches an `invisible_captcha.spam_detected` event' do
|
151
|
+
expect(dummy_handler).to receive(:handle_event).once.with(
|
152
|
+
message: "Invisible Captcha honeypot param 'subtitle' was present.",
|
153
|
+
remote_ip: '0.0.0.0',
|
154
|
+
user_agent: 'Rails Testing',
|
155
|
+
controller: 'topics',
|
156
|
+
action: 'create',
|
157
|
+
url: 'http://test.host/topics',
|
158
|
+
params: {
|
159
|
+
topic: { subtitle: "foo"},
|
160
|
+
controller: 'topics',
|
161
|
+
action: 'create'
|
162
|
+
}
|
163
|
+
)
|
164
|
+
|
165
|
+
post :create, params: { topic: { subtitle: 'foo' } }
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
context 'spinner attribute' do
|
171
|
+
before(:each) do
|
172
|
+
InvisibleCaptcha.spinner_enabled = true
|
173
|
+
InvisibleCaptcha.secret = 'secret'
|
174
|
+
session[:invisible_captcha_timestamp] = Time.zone.now.iso8601
|
175
|
+
session[:invisible_captcha_spinner] = '32ab649161f9f6faeeb323746de1a25d'
|
176
|
+
|
177
|
+
# Wait for valid submission
|
178
|
+
sleep InvisibleCaptcha.timestamp_threshold
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'fails with no spam, but mismatch of spinner' do
|
182
|
+
post :create, params: { topic: { title: 'foo' }, spinner: 'mismatch' }
|
183
|
+
|
184
|
+
expect(response.body).to be_blank
|
185
|
+
end
|
186
|
+
|
187
|
+
it 'passes with no spam and spinner match' do
|
188
|
+
post :create, params: { topic: { title: 'foo' }, spinner: '32ab649161f9f6faeeb323746de1a25d' }
|
189
|
+
|
190
|
+
expect(response.body).to be_present
|
191
|
+
end
|
144
192
|
end
|
145
193
|
end
|
@@ -14,13 +14,8 @@ Dummy::Application.configure do
|
|
14
14
|
|
15
15
|
# Disable serving static files from the `/public` folder by default since
|
16
16
|
# Apache or NGINX already handles this.
|
17
|
-
|
18
|
-
|
19
|
-
config.public_file_server.headers = {'Cache-Control' => 'public, max-age=3600'}
|
20
|
-
else
|
21
|
-
config.serve_static_files = true
|
22
|
-
config.static_cache_control = "public, max-age=3600"
|
23
|
-
end
|
17
|
+
config.public_file_server.enabled = true
|
18
|
+
config.public_file_server.headers = {'Cache-Control' => 'public, max-age=3600'}
|
24
19
|
|
25
20
|
# Show full error reports and disable caching.
|
26
21
|
config.consider_all_requests_local = true
|
@@ -39,4 +34,4 @@ Dummy::Application.configure do
|
|
39
34
|
|
40
35
|
# Print deprecation notices to the stderr.
|
41
36
|
config.active_support.deprecation = :stderr
|
42
|
-
end
|
37
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
ENV['RAILS_ENV'] = 'test'
|
2
4
|
|
3
5
|
require File.expand_path("../dummy/config/environment.rb", __FILE__)
|
@@ -5,6 +7,7 @@ require 'rspec/rails'
|
|
5
7
|
require 'invisible_captcha'
|
6
8
|
|
7
9
|
RSpec.configure do |config|
|
10
|
+
config.include ActionDispatch::ContentSecurityPolicy::Request, type: :helper
|
8
11
|
config.disable_monkey_patching!
|
9
12
|
config.order = :random
|
10
13
|
config.expect_with :rspec
|
@@ -12,17 +15,3 @@ RSpec.configure do |config|
|
|
12
15
|
mocks.verify_partial_doubles = true
|
13
16
|
end
|
14
17
|
end
|
15
|
-
|
16
|
-
# Rails 4.2 call `initialize` inside `recycle!`. However Ruby 2.6 doesn't allow calling `initialize` twice.
|
17
|
-
# More info: https://github.com/rails/rails/issues/34790
|
18
|
-
if RUBY_VERSION >= "2.6.0" && Rails.version < "5"
|
19
|
-
module ActionController
|
20
|
-
class TestResponse < ActionDispatch::TestResponse
|
21
|
-
def recycle!
|
22
|
-
@mon_mutex_owner_object_id = nil
|
23
|
-
@mon_mutex = nil
|
24
|
-
initialize
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
data/spec/view_helpers_spec.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
RSpec.describe InvisibleCaptcha::ViewHelpers, type: :helper do
|
2
4
|
before(:each) do
|
3
|
-
allow(Time.zone).to receive(:now).and_return(Time.zone.parse('Feb 19 1986'))
|
4
5
|
allow(InvisibleCaptcha).to receive(:css_strategy).and_return("display:none;")
|
6
|
+
allow_any_instance_of(ActionDispatch::ContentSecurityPolicy::Request).to receive(:content_security_policy_nonce).and_return('123')
|
5
7
|
|
6
8
|
# to test content_for and provide
|
7
9
|
@view_flow = ActionView::OutputFlow.new
|
@@ -26,6 +28,10 @@ RSpec.describe InvisibleCaptcha::ViewHelpers, type: :helper do
|
|
26
28
|
expect(invisible_captcha(:subtitle, :topic, { class: 'foo_class' })).to match(/class="foo_class"/)
|
27
29
|
end
|
28
30
|
|
31
|
+
it 'with CSP nonce' do
|
32
|
+
expect(invisible_captcha(:subtitle, :topic, { nonce: true })).to match(/nonce="123"/)
|
33
|
+
end
|
34
|
+
|
29
35
|
it 'generated html + styles' do
|
30
36
|
InvisibleCaptcha.honeypots = [:foo_id]
|
31
37
|
output = invisible_captcha.gsub("\"", "'")
|
@@ -52,6 +58,18 @@ RSpec.describe InvisibleCaptcha::ViewHelpers, type: :helper do
|
|
52
58
|
end
|
53
59
|
end
|
54
60
|
|
61
|
+
context "should have spinner field" do
|
62
|
+
it 'that exists by default, spinner_enabled is true' do
|
63
|
+
InvisibleCaptcha.spinner_enabled = true
|
64
|
+
expect(invisible_captcha).to match(/spinner/)
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'that does not exist if spinner_enabled is false' do
|
68
|
+
InvisibleCaptcha.spinner_enabled = false
|
69
|
+
expect(invisible_captcha).not_to match(/spinner/)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
55
73
|
it 'should set spam timestamp' do
|
56
74
|
invisible_captcha
|
57
75
|
expect(session[:invisible_captcha_timestamp]).to eq(Time.zone.now.iso8601)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: invisible_captcha
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Marc Anguera Insa
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-02-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -16,30 +16,16 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: '5.0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: '5.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rspec-rails
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - "~>"
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '3.1'
|
34
|
-
type: :development
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - "~>"
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '3.1'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: appraisal
|
43
29
|
requirement: !ruby/object:Gem::Requirement
|
44
30
|
requirements:
|
45
31
|
- - ">="
|
@@ -53,21 +39,7 @@ dependencies:
|
|
53
39
|
- !ruby/object:Gem::Version
|
54
40
|
version: '0'
|
55
41
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - "~>"
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '3.0'
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - "~>"
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '3.0'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: byebug
|
42
|
+
name: appraisal
|
71
43
|
requirement: !ruby/object:Gem::Requirement
|
72
44
|
requirements:
|
73
45
|
- - ">="
|
@@ -80,7 +52,7 @@ dependencies:
|
|
80
52
|
- - ">="
|
81
53
|
- !ruby/object:Gem::Version
|
82
54
|
version: '0'
|
83
|
-
description: Unobtrusive, flexible and
|
55
|
+
description: Unobtrusive, flexible and complete spam protection for Rails applications
|
84
56
|
using honeypot strategy for better user experience.
|
85
57
|
email:
|
86
58
|
- srmarc.ai@gmail.com
|
@@ -97,12 +69,9 @@ files:
|
|
97
69
|
- LICENSE
|
98
70
|
- README.md
|
99
71
|
- Rakefile
|
100
|
-
- gemfiles/rails_3.2.gemfile
|
101
|
-
- gemfiles/rails_4.2.gemfile
|
102
|
-
- gemfiles/rails_5.0.gemfile
|
103
|
-
- gemfiles/rails_5.1.gemfile
|
104
72
|
- gemfiles/rails_5.2.gemfile
|
105
73
|
- gemfiles/rails_6.0.gemfile
|
74
|
+
- gemfiles/rails_6.1.gemfile
|
106
75
|
- invisible_captcha.gemspec
|
107
76
|
- lib/invisible_captcha.rb
|
108
77
|
- lib/invisible_captcha/controller_ext.rb
|
@@ -113,6 +82,7 @@ files:
|
|
113
82
|
- spec/controllers_spec.rb
|
114
83
|
- spec/dummy/README.md
|
115
84
|
- spec/dummy/Rakefile
|
85
|
+
- spec/dummy/app/assets/config/manifest.js
|
116
86
|
- spec/dummy/app/assets/javascripts/application.js
|
117
87
|
- spec/dummy/app/assets/stylesheets/application.css
|
118
88
|
- spec/dummy/app/controllers/application_controller.rb
|
@@ -175,11 +145,12 @@ requirements: []
|
|
175
145
|
rubygems_version: 3.0.3
|
176
146
|
signing_key:
|
177
147
|
specification_version: 4
|
178
|
-
summary:
|
148
|
+
summary: Honeypot spam protection for Rails
|
179
149
|
test_files:
|
180
150
|
- spec/controllers_spec.rb
|
181
151
|
- spec/dummy/README.md
|
182
152
|
- spec/dummy/Rakefile
|
153
|
+
- spec/dummy/app/assets/config/manifest.js
|
183
154
|
- spec/dummy/app/assets/javascripts/application.js
|
184
155
|
- spec/dummy/app/assets/stylesheets/application.css
|
185
156
|
- spec/dummy/app/controllers/application_controller.rb
|
data/gemfiles/rails_4.2.gemfile
DELETED
data/gemfiles/rails_5.0.gemfile
DELETED