invisible_captcha 1.1.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d931568dd6707074ae2b5f48f6a5552283d482a4d87f27b1c72852dfce3d9cb
4
- data.tar.gz: 4e78f33d0a7c4be1a774de1e98e55d7fd2f7ff84e1a407d66a565c6e99779fa9
3
+ metadata.gz: 86acfe71e903568702c63c261bfd32a066dbc947d1c2fa2b4956178a7de591db
4
+ data.tar.gz: a9a8768c5bcfb9a7656e0638dde4bf40e169719385bf947dace7fa4d34f03656
5
5
  SHA512:
6
- metadata.gz: 3fbe8b6755bbc31fb26fbf9d71f01b7c9dbe919feaaaf12f78f86f7e53e93bb9f04f52c83469ea59840714908d834abf84625a542e7f55de2b7f0cd1d877986c
7
- data.tar.gz: 380701e4ddf138445faafb293ed94fe368d58eb7d995d44b3f871362550d5c2bb27d537d00c04cdf89501bc951ab227d3fe7c721389ba1f163327f4d8093ced4
6
+ metadata.gz: ec1ed1b9f7bef2e753f7b736dbce8124b5cd4c699e4075cbd15fdade99e6f332025e3088f0754315b18f8bde48b7e883c3470f840a535a3631f2e5c659c415df
7
+ data.tar.gz: 3698ed54f31c8f87730dcd92e14c2076e10aaa98a14a765e7d3be8bfd0e259385572b5d3252b8328fb4a539634bb8e086270b2e7e34117ab76a15ab4f42c095b
data/.travis.yml CHANGED
@@ -2,26 +2,17 @@ language: ruby
2
2
  cache: bundler
3
3
  rvm:
4
4
  - ruby-head
5
+ - 3.0.0
6
+ - 2.7.2
5
7
  - 2.6.5
6
- - 2.5.7
7
- - 2.4.9
8
- - 2.3.8
8
+ - 2.5.8
9
9
  gemfile:
10
+ - gemfiles/rails_6.1.gemfile
10
11
  - gemfiles/rails_6.0.gemfile
11
12
  - gemfiles/rails_5.2.gemfile
12
- - gemfiles/rails_5.1.gemfile
13
- - gemfiles/rails_5.0.gemfile
14
- - gemfiles/rails_4.2.gemfile
15
- before_install:
16
- # Rails 4.x requires Bundler version < 2.0.
17
- - "find /home/travis/.rvm/rubies -wholename '*default/bundler-*.gemspec' -delete"
18
- - rvm @global do gem uninstall bundler -a -x
19
- - rvm @global do yes | gem install bundler -v '< 2'
20
13
  matrix:
21
14
  exclude:
22
- - rvm: 2.4.9
23
- gemfile: gemfiles/rails_6.0.gemfile
24
- - rvm: 2.3.8
25
- gemfile: gemfiles/rails_6.0.gemfile
15
+ - rvm: 3.0.0
16
+ gemfile: gemfiles/rails_5.2.gemfile
26
17
  allow_failures:
27
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,15 +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
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
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
+
5
11
  ## [1.1.0]
6
12
 
7
13
  - New option `prepend: true` for the controller macro (#77)
@@ -119,6 +125,7 @@ All notable changes to this project will be documented in this file.
119
125
 
120
126
  - First version of controller filters
121
127
 
128
+ [2.0.0]: https://github.com/markets/invisible_captcha/compare/v1.1.0...v2.0.0
122
129
  [1.1.0]: https://github.com/markets/invisible_captcha/compare/v1.0.1...v1.1.0
123
130
  [1.0.1]: https://github.com/markets/invisible_captcha/compare/v1.0.0...v1.0.1
124
131
  [1.0.0]: https://github.com/markets/invisible_captcha/compare/v0.13.0...v1.0.0
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2012-2019 Marc Anguera Insa
1
+ Copyright 2012-2021 Marc Anguera Insa
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,9 +1,9 @@
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.org/markets/invisible_captcha.svg)](https://travis-ci.org/markets/invisible_captcha)
4
+ [![Build Status](https://travis-ci.com/markets/invisible_captcha.svg?branch=master)](https://travis-ci.com/markets/invisible_captcha)
5
5
 
6
- > Simple and flexible spam protection solution for Rails applications.
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
 
@@ -15,11 +15,13 @@ Essentially, the strategy consists on adding an input field :honey_pot: into the
15
15
  - should be left empty by the real users
16
16
  - will most likely be filled by spam bots
17
17
 
18
- It also comes with a time-sensitive :hourglass: form submission.
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 `>= 4.2` and Ruby `>= 2.3`.
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
 
@@ -104,12 +106,14 @@ This section contains a description of all plugin options and customizations.
104
106
  You can customize:
105
107
 
106
108
  - `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. See [Multiple Rails instances](#multiple-rails-instances).
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).
108
110
  - `visual_honeypots`: make honeypots visible, also useful to test/debug your implementation.
109
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"`).
110
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.
111
113
  - `timestamp_error_message`: flash error message thrown when form submitted quicker than the `timestamp_threshold` value. It uses I18n by default.
112
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.
113
117
 
114
118
  To change these defaults, add the following to an initializer (recommended `config/initializers/invisible_captcha.rb`):
115
119
 
@@ -117,9 +121,10 @@ To change these defaults, add the following to an initializer (recommended `conf
117
121
  InvisibleCaptcha.setup do |config|
118
122
  # config.honeypots << ['more', 'fake', 'attribute', 'names']
119
123
  # config.visual_honeypots = false
120
- # config.timestamp_threshold = 4
124
+ # config.timestamp_threshold = 2
121
125
  # config.timestamp_enabled = true
122
126
  # config.injectable_styles = false
127
+ # config.spinner_enabled = true
123
128
 
124
129
  # Leave these unset if you want to use I18n (see below)
125
130
  # config.sentence_for_humans = 'If you are a human, ignore this field'
@@ -141,6 +146,8 @@ InvisibleCaptcha.setup do |config|
141
146
  end
142
147
  ```
143
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
+
144
151
  ### Controller method options:
145
152
 
146
153
  The `invisible_captcha` method accepts some options:
@@ -148,7 +155,7 @@ The `invisible_captcha` method accepts some options:
148
155
  - `only`: apply to given controller actions.
149
156
  - `except`: exclude to given controller actions.
150
157
  - `honeypot`: name of custom honeypot.
151
- - `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`.
152
159
  - `on_spam`: custom callback to be called on spam detection.
153
160
  - `timestamp_enabled`: enable/disable this technique at action level.
154
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]`.
@@ -190,8 +197,8 @@ To set up a global event handler, [subscribe](https://guides.rubyonrails.org/act
190
197
  # config/initializers/invisible_captcha.rb
191
198
 
192
199
  ActiveSupport::Notifications.subscribe('invisible_captcha.spam_detected') do |*args, data|
193
- AwesomeLogger.warn(data[:message], data) # Log to an external logging service.
194
- SpamRequest.create(data) # Record the blocked request in your database.
200
+ AwesomeLogger.warn(data[:message], data) # Log to an external logging service.
201
+ SpamRequest.create(data) # Record the blocked request in your database.
195
202
  end
196
203
  ```
197
204
 
@@ -245,8 +252,6 @@ And in your view helper, you need to pass `nonce: true` to the `invisible_captch
245
252
  * https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
246
253
  * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
247
254
 
248
- Note that Content Security Policy only works on Rails 5.2 and up.
249
-
250
255
  ### I18n
251
256
 
252
257
  `invisible_captcha` tries to use I18n when it's available by default. The keys it looks for are the following:
@@ -309,7 +314,7 @@ $ bundle exec appraisal rspec
309
314
  Run specs against specific version:
310
315
 
311
316
  ```
312
- $ bundle exec appraisal rails-5.2 rspec
317
+ $ bundle exec appraisal rails-6.0 rspec
313
318
  ```
314
319
 
315
320
  ### Demo
@@ -2,6 +2,6 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "rails", "~> 4.2.0"
5
+ gem "rails", "~> 6.1.0"
6
6
 
7
7
  gemspec path: "../"
@@ -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 simple spam protection for Rails applications using honeypot strategy for better user experience."
9
- spec.summary = "Simple honeypot protection for RoR apps"
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,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', '>= 4.2'
18
+ spec.add_dependency 'rails', '>= 5.0'
19
19
 
20
- spec.add_development_dependency 'rspec-rails', '~> 3.1'
20
+ spec.add_development_dependency 'rspec-rails'
21
21
  spec.add_development_dependency 'appraisal'
22
22
  end
@@ -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 of your layout the following helper, to inject the honeypot styles:
37
- # <%= invisible_captcha_styles %>
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)
@@ -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
- if respond_to?(:redirect_back)
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,15 +51,15 @@ module InvisibleCaptcha
55
51
 
56
52
  return false unless enabled
57
53
 
58
- @invisible_captcha_timestamp ||= session.delete(:invisible_captcha_timestamp)
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 @invisible_captcha_timestamp
57
+ unless timestamp
62
58
  warn_spam("Invisible Captcha timestamp not found in session.")
63
59
  return true
64
60
  end
65
61
 
66
- time_to_submit = Time.zone.now - DateTime.iso8601(@invisible_captcha_timestamp)
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
@@ -72,7 +68,16 @@ module InvisibleCaptcha
72
68
  return true
73
69
  end
74
70
 
75
- return false
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
76
81
  end
77
82
 
78
83
  def honeypot_spam?(options = {})
@@ -83,7 +88,7 @@ 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? || (params[scope] && params[scope][honeypot].present?)
91
+ if params[honeypot].present? || params.dig(scope, honeypot).present?
87
92
  warn_spam("Invisible Captcha honeypot param '#{honeypot}' was present.")
88
93
  return true
89
94
  else
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module InvisibleCaptcha
4
- VERSION = "1.1.0"
4
+ VERSION = "2.0.0"
5
5
  end
@@ -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
- if InvisibleCaptcha.timestamp_enabled
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])
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 Rails.version >= '5.2'
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}}"
@@ -3,39 +3,25 @@
3
3
  RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
4
4
  render_views
5
5
 
6
- def switchable_post(action, params = {})
7
- if Rails.version > '5'
8
- post action, params: params
9
- else
10
- post action, params
11
- end
12
- end
13
-
14
- def switchable_put(action, params = {})
15
- if Rails.version > '5'
16
- put action, params: params
17
- else
18
- put action, params
19
- end
20
- end
21
-
22
6
  before(:each) do
23
7
  @controller = TopicsController.new
24
8
  request.env['HTTP_REFERER'] = 'http://test.host/topics'
9
+
25
10
  InvisibleCaptcha.init!
26
11
  InvisibleCaptcha.timestamp_threshold = 1
12
+ InvisibleCaptcha.spinner_enabled = false
27
13
  end
28
14
 
29
15
  context 'without invisible_captcha_timestamp in session' do
30
16
  it 'fails like if it was submitted too fast' do
31
- switchable_post :create, topic: { title: 'foo' }
17
+ post :create, params: { topic: { title: 'foo' } }
32
18
 
33
19
  expect(response).to redirect_to 'http://test.host/topics'
34
20
  expect(flash[:error]).to eq(InvisibleCaptcha.timestamp_error_message)
35
21
  end
36
22
 
37
23
  it 'passes if disabled at action level' do
38
- switchable_post :copy, topic: { title: 'foo' }
24
+ post :copy, params: { topic: { title: 'foo' } }
39
25
 
40
26
  expect(flash[:error]).not_to be_present
41
27
  expect(response.body).to be_present
@@ -43,7 +29,8 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
43
29
 
44
30
  it 'passes if disabled at app level' do
45
31
  InvisibleCaptcha.timestamp_enabled = false
46
- switchable_post :create, topic: { title: 'foo' }
32
+
33
+ post :create, params: { topic: { title: 'foo' } }
47
34
 
48
35
  expect(flash[:error]).not_to be_present
49
36
  expect(response.body).to be_present
@@ -56,7 +43,7 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
56
43
  end
57
44
 
58
45
  it 'fails if submission before timestamp_threshold' do
59
- switchable_post :create, topic: { title: 'foo' }
46
+ post :create, params: { topic: { title: 'foo' } }
60
47
 
61
48
  expect(response).to redirect_to 'http://test.host/topics'
62
49
  expect(flash[:error]).to eq(InvisibleCaptcha.timestamp_error_message)
@@ -66,7 +53,7 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
66
53
  end
67
54
 
68
55
  it 'allows a custom on_timestamp_spam callback' do
69
- switchable_put :update, id: 1, topic: { title: 'bar' }
56
+ put :update, params: { id: 1, topic: { title: 'bar' } }
70
57
 
71
58
  expect(response.status).to eq(204)
72
59
  end
@@ -79,7 +66,7 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
79
66
  end
80
67
  end
81
68
 
82
- expect { switchable_put :update, id: 1, topic: { title: 'bar' } }
69
+ expect { put :update, params: { id: 1, topic: { title: 'bar' } } }
83
70
  .to change { session[:invisible_captcha_timestamp] }
84
71
  .to be_present
85
72
  end
@@ -88,10 +75,12 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
88
75
  it 'passes if submission on or after timestamp_threshold' do
89
76
  sleep InvisibleCaptcha.timestamp_threshold
90
77
 
91
- switchable_post :create, topic: {
92
- title: 'foobar',
93
- author: 'author',
94
- body: 'body that passes validation'
78
+ post :create, params: {
79
+ topic: {
80
+ title: 'foobar',
81
+ author: 'author',
82
+ body: 'body that passes validation'
83
+ }
95
84
  }
96
85
 
97
86
  expect(flash[:error]).not_to be_present
@@ -104,7 +93,7 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
104
93
  it 'allow to set a custom timestamp_threshold per action' do
105
94
  sleep 2 # custom threshold
106
95
 
107
- switchable_post :publish, id: 1
96
+ post :publish, params: { id: 1 }
108
97
 
109
98
  expect(flash[:error]).not_to be_present
110
99
  expect(response.body).to be_present
@@ -115,30 +104,31 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
115
104
  context 'honeypot attribute' do
116
105
  before(:each) do
117
106
  session[:invisible_captcha_timestamp] = Time.zone.now.iso8601
107
+
118
108
  # Wait for valid submission
119
109
  sleep InvisibleCaptcha.timestamp_threshold
120
110
  end
121
111
 
122
112
  it 'fails with spam' do
123
- switchable_post :create, topic: { subtitle: 'foo' }
113
+ post :create, params: { topic: { subtitle: 'foo' } }
124
114
 
125
115
  expect(response.body).to be_blank
126
116
  end
127
117
 
128
118
  it 'passes with no spam' do
129
- switchable_post :create, topic: { title: 'foo' }
119
+ post :create, params: { topic: { title: 'foo' } }
130
120
 
131
121
  expect(response.body).to be_present
132
122
  end
133
123
 
134
124
  it 'allow a custom on_spam callback' do
135
- switchable_put :update, id: 1, topic: { subtitle: 'foo' }
125
+ put :update, params: { id: 1, topic: { subtitle: 'foo' } }
136
126
 
137
127
  expect(response.body).to redirect_to(new_topic_path)
138
128
  end
139
129
 
140
130
  it 'honeypot is removed from params if you use a custom honeypot' do
141
- switchable_post :create, topic: { title: 'foo', subtitle: '' }
131
+ post :create, params: { topic: { title: 'foo', subtitle: '' } }
142
132
 
143
133
  expect(flash[:error]).not_to be_present
144
134
  expect(@controller.params[:topic].key?(:subtitle)).to eq(false)
@@ -155,31 +145,49 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
155
145
  subscriber
156
146
  end
157
147
 
158
- after { ActiveSupport::Notifications.unsubscribe(subscriber) }
148
+ after { ActiveSupport::Notifications.unsubscribe(subscriber) }
159
149
 
160
150
  it 'dispatches an `invisible_captcha.spam_detected` event' do
161
- # Skip the `with` matcher for Rails < 5 due to issues comparing arguments passed to / recived by the dummy event handler.
162
- # https://github.com/markets/invisible_captcha/pull/62#issuecomment-552218501
163
- if Rails.version > '5'
164
- expect(dummy_handler).to receive(:handle_event).once.with(
165
- message: "Invisible Captcha honeypot param 'subtitle' was present.",
166
- remote_ip: '0.0.0.0',
167
- user_agent: 'Rails Testing',
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"},
168
160
  controller: 'topics',
169
- action: 'create',
170
- url: 'http://test.host/topics',
171
- params: {
172
- topic: { subtitle: "foo"},
173
- controller: 'topics',
174
- action: 'create'
175
- }
176
- )
177
- else
178
- expect(dummy_handler).to receive(:handle_event).once
179
- end
161
+ action: 'create'
162
+ }
163
+ )
180
164
 
181
- switchable_post :create, topic: { subtitle: 'foo' }
165
+ post :create, params: { topic: { subtitle: 'foo' } }
182
166
  end
183
167
  end
184
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
192
+ end
185
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
- if Rails.version >= "5.0.0"
18
- config.public_file_server.enabled = true
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
data/spec/spec_helper.rb CHANGED
@@ -7,9 +7,7 @@ require 'rspec/rails'
7
7
  require 'invisible_captcha'
8
8
 
9
9
  RSpec.configure do |config|
10
- if Rails.version >= '5.2'
11
- config.include ActionDispatch::ContentSecurityPolicy::Request, type: :helper
12
- end
10
+ config.include ActionDispatch::ContentSecurityPolicy::Request, type: :helper
13
11
  config.disable_monkey_patching!
14
12
  config.order = :random
15
13
  config.expect_with :rspec
@@ -17,17 +15,3 @@ RSpec.configure do |config|
17
15
  mocks.verify_partial_doubles = true
18
16
  end
19
17
  end
20
-
21
- # Rails 4.2 call `initialize` inside `recycle!`. However Ruby 2.6 doesn't allow calling `initialize` twice.
22
- # More info: https://github.com/rails/rails/issues/34790
23
- if RUBY_VERSION >= "2.6.0" && Rails.version < "5"
24
- module ActionController
25
- class TestResponse < ActionDispatch::TestResponse
26
- def recycle!
27
- @mon_mutex_owner_object_id = nil
28
- @mon_mutex = nil
29
- initialize
30
- end
31
- end
32
- end
33
- end
@@ -2,12 +2,8 @@
2
2
 
3
3
  RSpec.describe InvisibleCaptcha::ViewHelpers, type: :helper do
4
4
  before(:each) do
5
- allow(Time.zone).to receive(:now).and_return(Time.zone.parse('Feb 19 1986'))
6
5
  allow(InvisibleCaptcha).to receive(:css_strategy).and_return("display:none;")
7
-
8
- if Rails.version >= '5.2'
9
- allow_any_instance_of(ActionDispatch::ContentSecurityPolicy::Request).to receive(:content_security_policy_nonce).and_return('123')
10
- end
6
+ allow_any_instance_of(ActionDispatch::ContentSecurityPolicy::Request).to receive(:content_security_policy_nonce).and_return('123')
11
7
 
12
8
  # to test content_for and provide
13
9
  @view_flow = ActionView::OutputFlow.new
@@ -32,10 +28,8 @@ RSpec.describe InvisibleCaptcha::ViewHelpers, type: :helper do
32
28
  expect(invisible_captcha(:subtitle, :topic, { class: 'foo_class' })).to match(/class="foo_class"/)
33
29
  end
34
30
 
35
- if Rails.version >= '5.2'
36
- it 'with CSP nonce' do
37
- expect(invisible_captcha(:subtitle, :topic, { nonce: true })).to match(/nonce="123"/)
38
- end
31
+ it 'with CSP nonce' do
32
+ expect(invisible_captcha(:subtitle, :topic, { nonce: true })).to match(/nonce="123"/)
39
33
  end
40
34
 
41
35
  it 'generated html + styles' do
@@ -64,6 +58,18 @@ RSpec.describe InvisibleCaptcha::ViewHelpers, type: :helper do
64
58
  end
65
59
  end
66
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
+
67
73
  it 'should set spam timestamp' do
68
74
  invisible_captcha
69
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: 1.1.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: 2020-09-01 00:00:00.000000000 Z
11
+ date: 2021-02-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '4.2'
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: '4.2'
26
+ version: '5.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rspec-rails
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '3.1'
33
+ version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '3.1'
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: appraisal
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -52,7 +52,7 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
- description: Unobtrusive, flexible and simple spam protection for Rails applications
55
+ description: Unobtrusive, flexible and complete spam protection for Rails applications
56
56
  using honeypot strategy for better user experience.
57
57
  email:
58
58
  - srmarc.ai@gmail.com
@@ -69,11 +69,9 @@ files:
69
69
  - LICENSE
70
70
  - README.md
71
71
  - Rakefile
72
- - gemfiles/rails_4.2.gemfile
73
- - gemfiles/rails_5.0.gemfile
74
- - gemfiles/rails_5.1.gemfile
75
72
  - gemfiles/rails_5.2.gemfile
76
73
  - gemfiles/rails_6.0.gemfile
74
+ - gemfiles/rails_6.1.gemfile
77
75
  - invisible_captcha.gemspec
78
76
  - lib/invisible_captcha.rb
79
77
  - lib/invisible_captcha/controller_ext.rb
@@ -147,7 +145,7 @@ requirements: []
147
145
  rubygems_version: 3.0.3
148
146
  signing_key:
149
147
  specification_version: 4
150
- summary: Simple honeypot protection for RoR apps
148
+ summary: Honeypot spam protection for Rails
151
149
  test_files:
152
150
  - spec/controllers_spec.rb
153
151
  - spec/dummy/README.md
@@ -1,7 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "rails", "~> 5.0.0"
6
-
7
- gemspec path: "../"
@@ -1,7 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "rails", "~> 5.1.0"
6
-
7
- gemspec path: "../"