invisible_captcha 1.1.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +35 -0
- data/Appraisals +9 -18
- data/CHANGELOG.md +13 -0
- data/LICENSE +1 -1
- data/README.md +25 -15
- data/Rakefile +1 -6
- data/gemfiles/{rails_4.2.gemfile → rails_6.1.gemfile} +1 -1
- data/gemfiles/{rails_5.0.gemfile → rails_7.0.gemfile} +1 -1
- data/invisible_captcha.gemspec +7 -4
- data/lib/invisible_captcha/controller_ext.rb +24 -17
- data/lib/invisible_captcha/form_helpers.rb +1 -1
- data/lib/invisible_captcha/version.rb +1 -1
- data/lib/invisible_captcha/view_helpers.rb +17 -6
- data/lib/invisible_captcha.rb +15 -3
- data/spec/controllers_spec.rb +103 -51
- data/spec/dummy/app/controllers/topics_controller.rb +12 -0
- data/spec/dummy/app/views/layouts/application.html.erb +1 -2
- data/spec/dummy/config/application.rb +0 -2
- data/spec/dummy/config/environments/development.rb +1 -4
- data/spec/dummy/config/environments/test.rb +3 -8
- data/spec/dummy/config/routes.rb +2 -0
- data/spec/dummy/{app/assets/stylesheets/application.css → public/styles.css} +9 -4
- data/spec/spec_helper.rb +8 -17
- data/spec/view_helpers_spec.rb +15 -9
- metadata +61 -30
- data/.travis.yml +0 -27
- data/gemfiles/rails_5.1.gemfile +0 -7
- data/spec/dummy/app/assets/config/manifest.js +0 -2
- data/spec/dummy/app/assets/javascripts/application.js +0 -1
- data/spec/dummy/app/mailers/.gitkeep +0 -0
- data/spec/dummy/config/environments/production.rb +0 -86
- data/spec/dummy/lib/assets/.gitkeep +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f15e5223696c06e82c8ab6182a4396a9e4e4bf3acd6e425296e60cc6b49cb225
|
4
|
+
data.tar.gz: e35b51231012ae92b236f81eb1966a16ea3c7b02862d03977bf23478d968538d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ccc4299595595e513fa8f8eb472233b83fa334608b3cd4fcba3d1316b8f1819d83e7cc3d2678dce0ac954c52a4b90ecf0643424d0358cd8dd3412c4aa04ac391
|
7
|
+
data.tar.gz: 1fcceba58cb21d931e6d8f7f49a3a1649230628442e18f3664a80d5ac983dbec0bc5caeeda17cacc748bfbfabce93bd189f695bc0d123d4bd2ef0d94a20cdb3e
|
@@ -0,0 +1,35 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on: [push, pull_request]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
test:
|
7
|
+
name: CI
|
8
|
+
runs-on: ubuntu-latest
|
9
|
+
env:
|
10
|
+
BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile
|
11
|
+
strategy:
|
12
|
+
fail-fast: false
|
13
|
+
matrix:
|
14
|
+
ruby: ["2.7", "3.0", "3.1", "3.2"]
|
15
|
+
gemfile: [rails_6.0, rails_6.1, rails_7.0]
|
16
|
+
exclude:
|
17
|
+
- ruby: "3.1"
|
18
|
+
gemfile: rails_6.0
|
19
|
+
- ruby: "3.2"
|
20
|
+
gemfile: rails_6.0
|
21
|
+
include:
|
22
|
+
- ruby: "2.7"
|
23
|
+
gemfile: rails_5.2
|
24
|
+
- ruby: head
|
25
|
+
gemfile: rails_7.0
|
26
|
+
steps:
|
27
|
+
- uses: actions/checkout@v3
|
28
|
+
- uses: ruby/setup-ruby@v1
|
29
|
+
with:
|
30
|
+
ruby-version: ${{ matrix.ruby }}
|
31
|
+
bundler-cache: true
|
32
|
+
- run: bundle exec rspec
|
33
|
+
continue-on-error: ${{ endsWith(matrix.ruby, 'head') }}
|
34
|
+
- name: Upload coverage reports to Codecov
|
35
|
+
uses: codecov/codecov-action@v3
|
data/Appraisals
CHANGED
@@ -1,19 +1,10 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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"
|
1
|
+
%w(
|
2
|
+
7.0
|
3
|
+
6.1
|
4
|
+
6.0
|
5
|
+
5.2
|
6
|
+
).each do |version|
|
7
|
+
appraise "rails-#{version}" do
|
8
|
+
gem "rails", "~> #{version}.0"
|
9
|
+
end
|
19
10
|
end
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,17 @@
|
|
2
2
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
4
4
|
|
5
|
+
## [2.1.0]
|
6
|
+
|
7
|
+
- Drop official support for EOL Rubies: 2.5 and 2.6
|
8
|
+
- Allow random honeypots to be scoped (#117)
|
9
|
+
|
10
|
+
## [2.0.0]
|
11
|
+
|
12
|
+
- New spinner, IP based, validation check (#89)
|
13
|
+
- Drop official support for unmaintained Rails versions: 5.1, 5.0 and 4.2 (#86)
|
14
|
+
- Drop official support for EOL Rubies: 2.4 and 2.3 (#86)
|
15
|
+
|
5
16
|
## [1.1.0]
|
6
17
|
|
7
18
|
- New option `prepend: true` for the controller macro (#77)
|
@@ -119,6 +130,8 @@ All notable changes to this project will be documented in this file.
|
|
119
130
|
|
120
131
|
- First version of controller filters
|
121
132
|
|
133
|
+
[2.1.0]: https://github.com/markets/invisible_captcha/compare/v2.0.0...v2.1.0
|
134
|
+
[2.0.0]: https://github.com/markets/invisible_captcha/compare/v1.1.0...v2.0.0
|
122
135
|
[1.1.0]: https://github.com/markets/invisible_captcha/compare/v1.0.1...v1.1.0
|
123
136
|
[1.0.1]: https://github.com/markets/invisible_captcha/compare/v1.0.0...v1.0.1
|
124
137
|
[1.0.0]: https://github.com/markets/invisible_captcha/compare/v0.13.0...v1.0.0
|
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -1,9 +1,10 @@
|
|
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://
|
4
|
+
[![Build Status](https://github.com/markets/invisible_captcha/workflows/CI/badge.svg)](https://github.com/markets/invisible_captcha/actions)
|
5
|
+
[![codecov](https://codecov.io/gh/markets/invisible_captcha/branch/master/graph/badge.svg?token=nADSa6rbhM)](https://codecov.io/gh/markets/invisible_captcha)
|
5
6
|
|
6
|
-
>
|
7
|
+
> Complete and flexible spam protection solution for Rails applications.
|
7
8
|
|
8
9
|
Invisible Captcha provides different techniques to protect your application against spambots.
|
9
10
|
|
@@ -15,11 +16,13 @@ Essentially, the strategy consists on adding an input field :honey_pot: into the
|
|
15
16
|
- should be left empty by the real users
|
16
17
|
- will most likely be filled by spam bots
|
17
18
|
|
18
|
-
It also comes with
|
19
|
+
It also comes with:
|
20
|
+
- a time-sensitive :hourglass: form submission
|
21
|
+
- an IP based :mag: spinner validation
|
19
22
|
|
20
23
|
## Installation
|
21
24
|
|
22
|
-
Invisible Captcha is tested against Rails `>=
|
25
|
+
Invisible Captcha is tested against Rails `>= 5.2` and Ruby `>= 2.7`.
|
23
26
|
|
24
27
|
Add this line to your Gemfile and then execute `bundle install`:
|
25
28
|
|
@@ -61,6 +64,8 @@ class TopicsController < ApplicationController
|
|
61
64
|
end
|
62
65
|
```
|
63
66
|
|
67
|
+
You should _not_ name your method `on_spam`, as this will collide with an internal method of the same name.
|
68
|
+
|
64
69
|
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
70
|
|
66
71
|
```erb
|
@@ -95,6 +100,8 @@ invisible_captcha only: [:new_contact]
|
|
95
100
|
|
96
101
|
You can place `<%= flash[:error] %>` next to `:alert` and `:notice` message types, if you have them in your `app/views/layouts/application.html.erb`.
|
97
102
|
|
103
|
+
**NOTE:** This gem relies on data set by the backend, so in order to properly work, your forms should be rendered by Rails. Forms generated via JavaScript are not going to work well.
|
104
|
+
|
98
105
|
## Options and customization
|
99
106
|
|
100
107
|
This section contains a description of all plugin options and customizations.
|
@@ -104,12 +111,14 @@ This section contains a description of all plugin options and customizations.
|
|
104
111
|
You can customize:
|
105
112
|
|
106
113
|
- `sentence_for_humans`: text for real users if input field was visible. By default, it uses I18n (see below).
|
107
|
-
- `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 are running multiple Rails instances behind a load balancer
|
114
|
+
- `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)). Beware that Chrome may ignore `autocomplete="off"`. Thus, consider not to use field names, which would be autocompleted, like for example `name`, `country`.
|
108
115
|
- `visual_honeypots`: make honeypots visible, also useful to test/debug your implementation.
|
109
116
|
- `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"`).
|
110
117
|
- `timestamp_enabled`: option to disable the time threshold check at application level. Could be useful, for example, on some testing scenarios. By default, true.
|
111
118
|
- `timestamp_error_message`: flash error message thrown when form submitted quicker than the `timestamp_threshold` value. It uses I18n by default.
|
112
119
|
- `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.
|
120
|
+
- `spinner_enabled`: option to disable the IP spinner validation. By default, true.
|
121
|
+
- `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.
|
113
122
|
|
114
123
|
To change these defaults, add the following to an initializer (recommended `config/initializers/invisible_captcha.rb`):
|
115
124
|
|
@@ -117,9 +126,10 @@ To change these defaults, add the following to an initializer (recommended `conf
|
|
117
126
|
InvisibleCaptcha.setup do |config|
|
118
127
|
# config.honeypots << ['more', 'fake', 'attribute', 'names']
|
119
128
|
# config.visual_honeypots = false
|
120
|
-
# config.timestamp_threshold =
|
129
|
+
# config.timestamp_threshold = 2
|
121
130
|
# config.timestamp_enabled = true
|
122
131
|
# config.injectable_styles = false
|
132
|
+
# config.spinner_enabled = true
|
123
133
|
|
124
134
|
# Leave these unset if you want to use I18n (see below)
|
125
135
|
# config.sentence_for_humans = 'If you are a human, ignore this field'
|
@@ -141,6 +151,8 @@ InvisibleCaptcha.setup do |config|
|
|
141
151
|
end
|
142
152
|
```
|
143
153
|
|
154
|
+
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.
|
155
|
+
|
144
156
|
### Controller method options:
|
145
157
|
|
146
158
|
The `invisible_captcha` method accepts some options:
|
@@ -148,7 +160,7 @@ The `invisible_captcha` method accepts some options:
|
|
148
160
|
- `only`: apply to given controller actions.
|
149
161
|
- `except`: exclude to given controller actions.
|
150
162
|
- `honeypot`: name of custom honeypot.
|
151
|
-
- `scope`: name of scope, ie: 'topic[subtitle]' -> 'topic' is the scope.
|
163
|
+
- `scope`: name of scope, ie: 'topic[subtitle]' -> 'topic' is the scope. By default, it's inferred from the `controller_name`.
|
152
164
|
- `on_spam`: custom callback to be called on spam detection.
|
153
165
|
- `timestamp_enabled`: enable/disable this technique at action level.
|
154
166
|
- `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]`.
|
@@ -190,8 +202,8 @@ To set up a global event handler, [subscribe](https://guides.rubyonrails.org/act
|
|
190
202
|
# config/initializers/invisible_captcha.rb
|
191
203
|
|
192
204
|
ActiveSupport::Notifications.subscribe('invisible_captcha.spam_detected') do |*args, data|
|
193
|
-
AwesomeLogger.warn(data[:message], data)
|
194
|
-
SpamRequest.create(data)
|
205
|
+
AwesomeLogger.warn(data[:message], data) # Log to an external logging service.
|
206
|
+
SpamRequest.create(data) # Record the blocked request in your database.
|
195
207
|
end
|
196
208
|
```
|
197
209
|
|
@@ -199,7 +211,7 @@ The `data` passed to the subscriber is hash containing information about the req
|
|
199
211
|
|
200
212
|
```ruby
|
201
213
|
{
|
202
|
-
message: "
|
214
|
+
message: "Honeypot param 'subtitle' was present.",
|
203
215
|
remote_ip: '127.0.0.1',
|
204
216
|
user_agent: 'Chrome 77',
|
205
217
|
controller: 'users',
|
@@ -213,7 +225,7 @@ The `data` passed to the subscriber is hash containing information about the req
|
|
213
225
|
}
|
214
226
|
```
|
215
227
|
|
216
|
-
|
228
|
+
**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.
|
217
229
|
|
218
230
|
### Content Security Policy
|
219
231
|
|
@@ -240,13 +252,11 @@ And in your view helper, you need to pass `nonce: true` to the `invisible_captch
|
|
240
252
|
<%= invisible_captcha nonce: true %>
|
241
253
|
```
|
242
254
|
|
243
|
-
**
|
255
|
+
**NOTE:** 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:
|
244
256
|
|
245
257
|
* https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
|
246
258
|
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
|
247
259
|
|
248
|
-
Note that Content Security Policy only works on Rails 5.2 and up.
|
249
|
-
|
250
260
|
### I18n
|
251
261
|
|
252
262
|
`invisible_captcha` tries to use I18n when it's available by default. The keys it looks for are the following:
|
@@ -309,7 +319,7 @@ $ bundle exec appraisal rspec
|
|
309
319
|
Run specs against specific version:
|
310
320
|
|
311
321
|
```
|
312
|
-
$ bundle exec appraisal rails-
|
322
|
+
$ bundle exec appraisal rails-6.0 rspec
|
313
323
|
```
|
314
324
|
|
315
325
|
### Demo
|
data/Rakefile
CHANGED
@@ -1,11 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "bundler/gem_tasks"
|
4
|
-
require 'rspec/core/rake_task'
|
5
|
-
|
6
|
-
RSpec::Core::RakeTask.new(:spec)
|
7
|
-
|
8
|
-
task :default => :spec
|
9
4
|
|
10
5
|
desc 'Start development Rails app'
|
11
6
|
task :web do
|
@@ -16,4 +11,4 @@ task :web do
|
|
16
11
|
|
17
12
|
Dir.chdir(app_path)
|
18
13
|
exec("rails s -p #{port}")
|
19
|
-
end
|
14
|
+
end
|
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,8 +15,11 @@ 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.2'
|
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 'webrick'
|
23
|
+
spec.add_development_dependency 'simplecov'
|
24
|
+
spec.add_development_dependency 'simplecov-cobertura'
|
22
25
|
end
|
@@ -21,7 +21,7 @@ module InvisibleCaptcha
|
|
21
21
|
def detect_spam(options = {})
|
22
22
|
if timestamp_spam?(options)
|
23
23
|
on_timestamp_spam(options)
|
24
|
-
elsif honeypot_spam?(options)
|
24
|
+
elsif honeypot_spam?(options) || spinner_spam?
|
25
25
|
on_spam(options)
|
26
26
|
end
|
27
27
|
end
|
@@ -30,11 +30,7 @@ module InvisibleCaptcha
|
|
30
30
|
if action = options[:on_timestamp_spam]
|
31
31
|
send(action)
|
32
32
|
else
|
33
|
-
|
34
|
-
redirect_back(fallback_location: root_path, flash: { error: InvisibleCaptcha.timestamp_error_message })
|
35
|
-
else
|
36
|
-
redirect_to :back, flash: { error: InvisibleCaptcha.timestamp_error_message }
|
37
|
-
end
|
33
|
+
redirect_back(fallback_location: root_path, flash: { error: InvisibleCaptcha.timestamp_error_message })
|
38
34
|
end
|
39
35
|
end
|
40
36
|
|
@@ -55,24 +51,33 @@ module InvisibleCaptcha
|
|
55
51
|
|
56
52
|
return false unless enabled
|
57
53
|
|
58
|
-
|
54
|
+
timestamp = session.delete(:invisible_captcha_timestamp)
|
59
55
|
|
60
56
|
# Consider as spam if timestamp not in session, cause that means the form was not fetched at all
|
61
|
-
unless
|
62
|
-
warn_spam("
|
57
|
+
unless timestamp
|
58
|
+
warn_spam("Timestamp not found in session.")
|
63
59
|
return true
|
64
60
|
end
|
65
61
|
|
66
|
-
time_to_submit = Time.zone.now - DateTime.iso8601(
|
62
|
+
time_to_submit = Time.zone.now - DateTime.iso8601(timestamp)
|
67
63
|
threshold = options[:timestamp_threshold] || InvisibleCaptcha.timestamp_threshold
|
68
64
|
|
69
65
|
# Consider as spam if form submitted too quickly
|
70
66
|
if time_to_submit < threshold
|
71
|
-
warn_spam("
|
67
|
+
warn_spam("Timestamp threshold not reached (took #{time_to_submit.to_i}s).")
|
68
|
+
return true
|
69
|
+
end
|
70
|
+
|
71
|
+
false
|
72
|
+
end
|
73
|
+
|
74
|
+
def spinner_spam?
|
75
|
+
if InvisibleCaptcha.spinner_enabled && params[:spinner] != session[:invisible_captcha_spinner]
|
76
|
+
warn_spam("Spinner value mismatch")
|
72
77
|
return true
|
73
78
|
end
|
74
79
|
|
75
|
-
|
80
|
+
false
|
76
81
|
end
|
77
82
|
|
78
83
|
def honeypot_spam?(options = {})
|
@@ -83,8 +88,8 @@ module InvisibleCaptcha
|
|
83
88
|
# If honeypot is defined for this controller-action, search for:
|
84
89
|
# - honeypot: params[:subtitle]
|
85
90
|
# - honeypot with scope: params[:topic][:subtitle]
|
86
|
-
if params[honeypot].present? || (
|
87
|
-
warn_spam("
|
91
|
+
if params[honeypot].present? || params.dig(scope, honeypot).present?
|
92
|
+
warn_spam("Honeypot param '#{honeypot}' was present.")
|
88
93
|
return true
|
89
94
|
else
|
90
95
|
# No honeypot spam detected, remove honeypot from params to avoid UnpermittedParameters exceptions
|
@@ -93,8 +98,8 @@ module InvisibleCaptcha
|
|
93
98
|
end
|
94
99
|
else
|
95
100
|
InvisibleCaptcha.honeypots.each do |default_honeypot|
|
96
|
-
if params[default_honeypot].present?
|
97
|
-
warn_spam("
|
101
|
+
if params[default_honeypot].present? || params.dig(scope, default_honeypot).present?
|
102
|
+
warn_spam("Honeypot param '#{scope}.#{default_honeypot}' was present.")
|
98
103
|
return true
|
99
104
|
end
|
100
105
|
end
|
@@ -104,7 +109,9 @@ module InvisibleCaptcha
|
|
104
109
|
end
|
105
110
|
|
106
111
|
def warn_spam(message)
|
107
|
-
|
112
|
+
message = "[Invisible Captcha] Potential spam detected for IP #{request.remote_ip}. #{message}"
|
113
|
+
|
114
|
+
logger.warn(message)
|
108
115
|
|
109
116
|
ActiveSupport::Notifications.instrument(
|
110
117
|
'invisible_captcha.spam_detected',
|
@@ -10,9 +10,17 @@ module InvisibleCaptcha
|
|
10
10
|
#
|
11
11
|
# @return [String] the generated html
|
12
12
|
def invisible_captcha(honeypot = nil, scope = nil, options = {})
|
13
|
-
|
13
|
+
@captcha_ocurrences = 0 unless defined?(@captcha_ocurrences)
|
14
|
+
@captcha_ocurrences += 1
|
15
|
+
|
16
|
+
if InvisibleCaptcha.timestamp_enabled || InvisibleCaptcha.spinner_enabled
|
14
17
|
session[:invisible_captcha_timestamp] = Time.zone.now.iso8601
|
15
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
|
+
|
16
24
|
build_invisible_captcha(honeypot, scope, options)
|
17
25
|
end
|
18
26
|
|
@@ -24,6 +32,10 @@ module InvisibleCaptcha
|
|
24
32
|
|
25
33
|
private
|
26
34
|
|
35
|
+
def current_request
|
36
|
+
@request ||= request
|
37
|
+
end
|
38
|
+
|
27
39
|
def build_invisible_captcha(honeypot = nil, scope = nil, options = {})
|
28
40
|
if honeypot.is_a?(Hash)
|
29
41
|
options = honeypot
|
@@ -44,6 +56,9 @@ module InvisibleCaptcha
|
|
44
56
|
concat styles unless InvisibleCaptcha.injectable_styles
|
45
57
|
concat label_tag(build_label_name(honeypot, scope), label)
|
46
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], id: nil)
|
61
|
+
end
|
47
62
|
end
|
48
63
|
end
|
49
64
|
|
@@ -56,11 +71,7 @@ module InvisibleCaptcha
|
|
56
71
|
|
57
72
|
return if visible
|
58
73
|
|
59
|
-
nonce = if
|
60
|
-
content_security_policy_nonce if options[:nonce]
|
61
|
-
else
|
62
|
-
nil
|
63
|
-
end
|
74
|
+
nonce = content_security_policy_nonce if options[:nonce]
|
64
75
|
|
65
76
|
content_tag(:style, media: 'screen', nonce: nonce) do
|
66
77
|
".#{css_class} {#{InvisibleCaptcha.css_strategy}}"
|
data/lib/invisible_captcha.rb
CHANGED
@@ -15,7 +15,9 @@ module InvisibleCaptcha
|
|
15
15
|
:timestamp_threshold,
|
16
16
|
:timestamp_enabled,
|
17
17
|
:visual_honeypots,
|
18
|
-
:injectable_styles
|
18
|
+
:injectable_styles,
|
19
|
+
:spinner_enabled,
|
20
|
+
:secret
|
19
21
|
|
20
22
|
def init!
|
21
23
|
# Default sentence for real users if text field was visible
|
@@ -33,9 +35,15 @@ module InvisibleCaptcha
|
|
33
35
|
# Make honeypots visibles
|
34
36
|
self.visual_honeypots = false
|
35
37
|
|
36
|
-
# If enabled, you should call anywhere in
|
37
|
-
#
|
38
|
+
# If enabled, you should call anywhere in your layout the following helper, to inject the honeypot styles:
|
39
|
+
# <%= invisible_captcha_styles %>
|
38
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)
|
39
47
|
end
|
40
48
|
|
41
49
|
def sentence_for_humans
|
@@ -70,6 +78,10 @@ module InvisibleCaptcha
|
|
70
78
|
].sample
|
71
79
|
end
|
72
80
|
|
81
|
+
def encode(value)
|
82
|
+
Digest::MD5.hexdigest("#{self.secret}-#{value}")
|
83
|
+
end
|
84
|
+
|
73
85
|
private
|
74
86
|
|
75
87
|
def call_lambda_or_return(obj)
|