invisible_captcha 0.9.3 → 0.10.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
  SHA1:
3
- metadata.gz: 80d28e320294b2c40e8df6975a0efe8ce36ae9b3
4
- data.tar.gz: 27fbdf58a83d4a84475e05cea92d706dc6c37b96
3
+ metadata.gz: 0b8877b6b9e63a8e3469df1b1e4d564c323798f4
4
+ data.tar.gz: aa93c8cd7004683493f06ba0513bc4cce274f261
5
5
  SHA512:
6
- metadata.gz: d49d4f389ea60c174c1ab89d31fdb2414f0a7c8626a3a75a11ab1e0cd18fd1e3fbee07ff21d1b6456249c2b98e49748c6dbec829ef99403956f299ecaca3a5ba
7
- data.tar.gz: 7d8b1e7937307cbf2493de51cf2e38b8acf090455756fa719c2482530fe90a64d73c9fa07cef7da2f1aacb4d6dd011d350ee5c557cbe626b30290f05c33b16d4
6
+ metadata.gz: 32dca9e9c96528181b6665854d07b2edba9441390613824975617f26d263e747475688979224f09d6b582c3c762fa4f438517924534f361ffc7d266ca78d3d39
7
+ data.tar.gz: f8a13ba2c4885e921668617788d751050ae47a86714c6537a645fbb40071996634a603f31f4a6ff1cdbe723bfc2dbf940a9f82874306baaf98728ea1f8c23cba
@@ -1,40 +1,30 @@
1
1
  language: ruby
2
-
3
2
  cache: bundler
4
-
5
3
  sudo: false
6
-
7
4
  rvm:
8
5
  - ruby-head
9
- - 2.4.1
10
- - 2.3.4
11
- - 2.2.7
12
- - 2.1
13
- - 1.9.3
14
-
6
+ - 2.4.2
7
+ - 2.3.5
8
+ - 2.2.8
9
+ - 2.1.10
15
10
  gemfile:
16
11
  - gemfiles/rails_5.1.gemfile
17
12
  - gemfiles/rails_5.0.gemfile
18
13
  - gemfiles/rails_4.2.gemfile
19
14
  - gemfiles/rails_4.1.gemfile
20
15
  - gemfiles/rails_3.2.gemfile
21
-
22
16
  matrix:
23
17
  exclude:
24
- - rvm: 1.9.3
25
- gemfile: gemfiles/rails_4.2.gemfile
26
- - rvm: 1.9.3
18
+ - rvm: 2.1.10
27
19
  gemfile: gemfiles/rails_5.0.gemfile
28
- - rvm: 1.9.3
20
+ - rvm: 2.1.10
29
21
  gemfile: gemfiles/rails_5.1.gemfile
30
- - rvm: 2.1
31
- gemfile: gemfiles/rails_5.0.gemfile
32
- - rvm: 2.1
33
- gemfile: gemfiles/rails_5.1.gemfile
34
- - rvm: 2.4.1
22
+ - rvm: 2.4.2
35
23
  gemfile: gemfiles/rails_4.1.gemfile
36
- - rvm: 2.4.1
24
+ - rvm: 2.4.2
37
25
  gemfile: gemfiles/rails_3.2.gemfile
26
+ - rvm: ruby-head
27
+ gemfile: gemfiles/rails_4.1.gemfile
38
28
  - rvm: ruby-head
39
29
  gemfile: gemfiles/rails_3.2.gemfile
40
30
  allow_failures:
data/README.md CHANGED
@@ -4,19 +4,21 @@
4
4
 
5
5
  > Simple and flexible spam protection solution for Rails applications.
6
6
 
7
- It is based on the `honeypot` strategy to provide a better user experience. It also provides a time-sensitive form submission.
7
+ Invisible Captcha provides different techniques to protect your application against spambots.
8
8
 
9
- **Background**
9
+ The main protection is a solution based on the `honeypot` principle, which provides a better user experience, since there is no extra steps for real users, but for the bots.
10
10
 
11
- The strategy is about adding an input field into the form that:
11
+ Essentially, the strategy consists on adding an input field :honey_pot: into the form that:
12
12
 
13
13
  * shouldn't be visible by the real users
14
14
  * should be left empty by the real users
15
15
  * will most be filled by spam bots
16
16
 
17
+ It also comes with a time-sensitive :hourglass: form submission.
18
+
17
19
  ## Installation
18
20
 
19
- Invisible Captcha is tested against Rails `>= 3.2` and Ruby `>= 1.9.3`.
21
+ Invisible Captcha is tested against Rails `>= 3.2` and Ruby `>= 2.1`.
20
22
 
21
23
  Add this line to you Gemfile:
22
24
 
@@ -64,7 +66,7 @@ class TopicsController < ApplicationController
64
66
  end
65
67
  ```
66
68
 
67
- Note that isn't mandatory to specify a `honeypot` attribute (nor 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:
69
+ Note that is not mandatory to specify a `honeypot` attribute (nor 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:
68
70
 
69
71
  ```erb
70
72
  <%= form_tag(new_contact_path) do |f| %>
@@ -87,20 +89,23 @@ This section contains a description of all plugin options and customizations.
87
89
  You can customize:
88
90
 
89
91
  * `sentence_for_humans`: text for real users if input field was visible. By default, it uses I18n (see below).
90
- * `honeypots`: collection of default honeypots. Used by the view helper, called with no args, to generate a random honeypot field name.
92
+ * `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.
91
93
  * `visual_honeypots`: make honeypots visible, also useful to test/debug your implementation.
92
94
  * `timestamp_threshold`: fastest time (in seconds) to expect a human to submit the form (see [original article by Yoav Aner](http://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"`).
93
95
  * `timestamp_enabled`: option to disable the time threshold check at application level. Could be useful, for example, on some testing scenarios. By default, true.
94
96
  * `timestamp_error_message`: flash error message thrown when form submitted quicker than the `timestamp_threshold` value. It uses I18n by default.
97
+ * `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.
95
98
 
96
99
  To change these defaults, add the following to an initializer (recommended `config/initializers/invisible_captcha.rb`):
97
100
 
98
101
  ```ruby
99
102
  InvisibleCaptcha.setup do |config|
100
- config.honeypots << 'another_fake_attribute'
101
- config.visual_honeypots = false
102
- config.timestamp_threshold = 4
103
- config.timestamp_enabled = true
103
+ # config.honeypots << ['more', 'fake', 'attribute', 'names']
104
+ # config.visual_honeypots = false
105
+ # config.timestamp_threshold = 4
106
+ # config.timestamp_enabled = true
107
+ # config.injectable_styles = false
108
+
104
109
  # Leave these unset if you want to use I18n (see below)
105
110
  # config.sentence_for_humans = 'If you are a human, ignore this field'
106
111
  # config.timestamp_error_message = 'Sorry, that was too quick! Please resubmit.'
@@ -113,9 +118,10 @@ The `invisible_captcha` method accepts some options:
113
118
 
114
119
  * `only`: apply to given controller actions.
115
120
  * `except`: exclude to given controller actions.
116
- * `honeypot`: name of honeypot.
121
+ * `honeypot`: name of custom honeypot.
117
122
  * `scope`: name of scope, ie: 'topic[subtitle]' -> 'topic' is the scope.
118
123
  * `on_spam`: custom callback to be called on spam detection.
124
+ * `timestamp_threshold`: enable/disable this technique at action level.
119
125
  * `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]`.
120
126
  * `timestamp_threshold`: custom threshold per controller/action. Overrides the global value for `InvisibleCaptcha.timestamp_threshold`.
121
127
 
@@ -125,12 +131,18 @@ Using the view/form helper you can override some defaults for the given instance
125
131
 
126
132
  ```erb
127
133
  <%= form_for(@topic) do |f| %>
128
- <%= f.invisible_captcha :subtitle, visual_honeypots: true, sentence_for_humans: "Ei, don't fill on this input!" %>
134
+ <%= f.invisible_captcha :subtitle, visual_honeypots: true, sentence_for_humans: "hey! leave this input empty!" %>
129
135
  <!-- or -->
130
- <%= invisible_captcha visual_honeypots: true, sentence_for_humans: "Ei, don't fill on this input!" %>
136
+ <%= invisible_captcha visual_honeypots: true, sentence_for_humans: "hey! leave this input empty!" %>
131
137
  <% end %>
132
138
  ```
133
139
 
140
+ You can also pass html options to the input:
141
+
142
+ ```erb
143
+ <%= invisible_captcha :subtitle, :topic, id: "your_id", class: "your_class" %>
144
+ ```
145
+
134
146
  ### I18n
135
147
 
136
148
  `invisible_captcha` tries to use I18n when it's available by default. The keys it looks for are the following:
@@ -163,9 +175,12 @@ $ bundle exec rspec
163
175
  Run the test suite against all supported versions:
164
176
 
165
177
  ```
166
- $ bundle exec appraisal rake
178
+ $ bundle exec appraisal install
179
+ $ bundle exec appraisal rspec
167
180
  ```
168
181
 
182
+ ### Demo
183
+
169
184
  Start a sample Rails app ([source code](spec/dummy)) with `InvisibleCaptcha` integrated:
170
185
 
171
186
  ```
data/Rakefile CHANGED
@@ -13,5 +13,5 @@ task :web do
13
13
  puts "Starting application in http://localhost:#{port} ... \n"
14
14
 
15
15
  Dir.chdir(app_path)
16
- `rails s -p #{port}`
16
+ exec("rails s -p #{port}")
17
17
  end
@@ -20,6 +20,6 @@ Gem::Specification.new do |spec|
20
20
  spec.add_development_dependency 'rspec-rails', '~> 3.1'
21
21
  spec.add_development_dependency 'appraisal'
22
22
  spec.add_development_dependency 'test-unit', '~> 3.0'
23
- spec.add_development_dependency 'mime-types', '< 3.0'
23
+ spec.add_development_dependency 'byebug'
24
24
  end
25
25
 
@@ -7,45 +7,39 @@ require 'invisible_captcha/railtie'
7
7
  module InvisibleCaptcha
8
8
  class << self
9
9
  attr_writer :sentence_for_humans,
10
- :timestamp_error_message,
11
- :error_message
10
+ :timestamp_error_message
12
11
 
13
12
  attr_accessor :honeypots,
14
13
  :timestamp_threshold,
15
14
  :timestamp_enabled,
16
- :visual_honeypots
15
+ :visual_honeypots,
16
+ :injectable_styles
17
17
 
18
18
  def init!
19
19
  # Default sentence for real users if text field was visible
20
20
  self.sentence_for_humans = -> { I18n.t('invisible_captcha.sentence_for_humans', default: 'If you are a human, ignore this field') }
21
21
 
22
- # Default error message for validator
23
- self.error_message = -> { I18n.t('invisible_captcha.error_message', default: 'You are a robot!') }
24
-
25
- # Default fake fields for controller based workflow
26
- self.honeypots = ['foo_id', 'bar_id', 'baz_id']
22
+ # Timestamp check enabled by default
23
+ self.timestamp_enabled = true
27
24
 
28
25
  # Fastest time (in seconds) to expect a human to submit the form
29
26
  self.timestamp_threshold = 4
30
27
 
31
- # Timestamp check enabled by default
32
- self.timestamp_enabled = true
33
-
34
28
  # Default error message for validator when form submitted too quickly
35
29
  self.timestamp_error_message = -> { I18n.t('invisible_captcha.timestamp_error_message', default: 'Sorry, that was too quick! Please resubmit.') }
36
30
 
37
31
  # Make honeypots visibles
38
32
  self.visual_honeypots = false
33
+
34
+ # If enabled, you should call anywhere in of your layout the following helper, to inject the honeypot styles:
35
+ # <%= invisible_captcha_styles %>
36
+ self.injectable_styles = false
39
37
  end
40
38
 
41
39
  def sentence_for_humans
42
40
  call_lambda_or_return(@sentence_for_humans)
43
41
  end
44
42
 
45
- def error_message
46
- call_lambda_or_return(@error_message)
47
- end
48
-
49
43
  def timestamp_error_message
50
44
  call_lambda_or_return(@timestamp_error_message)
51
45
  end
@@ -54,10 +48,26 @@ module InvisibleCaptcha
54
48
  yield(self) if block_given?
55
49
  end
56
50
 
51
+ def honeypots
52
+ @honeypots ||= (1..5).map { generate_random_honeypot }
53
+ end
54
+
55
+ def generate_random_honeypot
56
+ "abcdefghijkl-mnopqrstuvwxyz".chars.sample(rand(10..20)).join
57
+ end
58
+
57
59
  def get_honeypot
58
60
  honeypots.sample
59
61
  end
60
62
 
63
+ def css_strategy
64
+ [
65
+ "display:none;",
66
+ "position:absolute!important;top:-9999px;left:-9999px;",
67
+ "position:absolute!important;height:1px;width:1px;overflow:hidden;"
68
+ ].sample
69
+ end
70
+
61
71
  private
62
72
 
63
73
  def call_lambda_or_return(obj)
@@ -2,7 +2,7 @@ module InvisibleCaptcha
2
2
  module ControllerExt
3
3
  module ClassMethods
4
4
  def invisible_captcha(options = {})
5
- if respond_to? :before_action
5
+ if respond_to?(:before_action)
6
6
  before_action(options) do
7
7
  detect_spam(options)
8
8
  end
@@ -14,15 +14,17 @@ module InvisibleCaptcha
14
14
  end
15
15
  end
16
16
 
17
+ private
18
+
17
19
  def detect_spam(options = {})
18
- if invisible_captcha_timestamp?(options)
19
- on_timestamp_spam_action(options)
20
- elsif invisible_captcha?(options)
21
- on_spam_action(options)
20
+ if timestamp_spam?(options)
21
+ on_timestamp_spam(options)
22
+ elsif honeypot_spam?(options)
23
+ on_spam(options)
22
24
  end
23
25
  end
24
26
 
25
- def on_timestamp_spam_action(options = {})
27
+ def on_timestamp_spam(options = {})
26
28
  if action = options[:on_timestamp_spam]
27
29
  send(action)
28
30
  else
@@ -34,23 +36,23 @@ module InvisibleCaptcha
34
36
  end
35
37
  end
36
38
 
37
- def on_spam_action(options = {})
39
+ def on_spam(options = {})
38
40
  if action = options[:on_spam]
39
41
  send(action)
40
42
  else
41
- default_on_spam
43
+ head(200)
42
44
  end
43
45
  end
44
46
 
45
- def default_on_spam
46
- head(200)
47
- end
48
-
49
- def invisible_captcha_timestamp?(options = {})
50
- unless InvisibleCaptcha.timestamp_enabled
51
- return false
47
+ def timestamp_spam?(options = {})
48
+ enabled = if options.key?(:timestamp_enabled)
49
+ options[:timestamp_enabled]
50
+ else
51
+ InvisibleCaptcha.timestamp_enabled
52
52
  end
53
53
 
54
+ return false unless enabled
55
+
54
56
  timestamp = session[:invisible_captcha_timestamp]
55
57
 
56
58
  # Consider as spam if timestamp not in session, cause that means the form was not fetched at all
@@ -60,31 +62,38 @@ module InvisibleCaptcha
60
62
  end
61
63
 
62
64
  time_to_submit = Time.zone.now - DateTime.iso8601(timestamp)
65
+ threshold = options[:timestamp_threshold] || InvisibleCaptcha.timestamp_threshold
63
66
 
64
67
  # Consider as spam if form submitted too quickly
65
- if time_to_submit < (options[:timestamp_threshold] || InvisibleCaptcha.timestamp_threshold)
68
+ if time_to_submit < threshold
66
69
  logger.warn("Potential spam detected for IP #{request.env['REMOTE_ADDR']}. Invisible Captcha timestamp threshold not reached (took #{time_to_submit.to_i}s).")
67
70
  return true
68
71
  end
72
+
69
73
  false
70
74
  end
71
75
 
72
- def invisible_captcha?(options = {})
76
+ def honeypot_spam?(options = {})
73
77
  honeypot = options[:honeypot]
74
78
  scope = options[:scope] || controller_name.singularize
75
79
 
76
80
  if honeypot
77
- # If honeypot is presented, search for:
81
+ # If honeypot is defined for this controller-action, search for:
78
82
  # - honeypot: params[:subtitle]
79
83
  # - honeypot with scope: params[:topic][:subtitle]
80
84
  if params[honeypot].present? || (params[scope] && params[scope][honeypot].present?)
81
85
  return true
86
+ else
87
+ # No honeypot spam detected, remove honeypot from params to avoid UnpermittedParameters exceptions
88
+ params.delete(honeypot) if params.key?(honeypot)
89
+ params[scope].try(:delete, honeypot) if params.key?(scope)
82
90
  end
83
91
  else
84
- InvisibleCaptcha.honeypots.each do |field|
85
- return true if params[field].present?
92
+ InvisibleCaptcha.honeypots.each do |default_honeypot|
93
+ return true if params[default_honeypot].present?
86
94
  end
87
95
  end
96
+
88
97
  false
89
98
  end
90
99
  end
@@ -1,3 +1,3 @@
1
1
  module InvisibleCaptcha
2
- VERSION = "0.9.3"
2
+ VERSION = "0.10.0"
3
3
  end
@@ -4,14 +4,22 @@ module InvisibleCaptcha
4
4
  #
5
5
  # @param honeypot [Symbol] name of honeypot, ie: subtitle => input name: subtitle
6
6
  # @param scope [Symbol] name of honeypot scope, ie: topic => input name: topic[subtitle]
7
+ # @param options [Hash] html_options for input and invisible_captcha options
8
+ #
7
9
  # @return [String] the generated html
8
10
  def invisible_captcha(honeypot = nil, scope = nil, options = {})
9
11
  if InvisibleCaptcha.timestamp_enabled
10
- session[:invisible_captcha_timestamp] ||= Time.zone.now.iso8601
12
+ session[:invisible_captcha_timestamp] = Time.zone.now.iso8601
11
13
  end
12
14
  build_invisible_captcha(honeypot, scope, options)
13
15
  end
14
16
 
17
+ def invisible_captcha_styles
18
+ if content_for?(:invisible_captcha_styles)
19
+ content_for(:invisible_captcha_styles)
20
+ end
21
+ end
22
+
15
23
  private
16
24
 
17
25
  def build_invisible_captcha(honeypot = nil, scope = nil, options = {})
@@ -20,30 +28,34 @@ module InvisibleCaptcha
20
28
  honeypot = nil
21
29
  end
22
30
 
23
- honeypot = honeypot ? honeypot.to_s : InvisibleCaptcha.get_honeypot
24
- label = options[:sentence_for_humans] || InvisibleCaptcha.sentence_for_humans
25
- html_id = generate_html_id(honeypot, scope)
31
+ honeypot = honeypot ? honeypot.to_s : InvisibleCaptcha.get_honeypot
32
+ label = options.delete(:sentence_for_humans) || InvisibleCaptcha.sentence_for_humans
33
+ css_class = "#{honeypot}_#{Time.zone.now.to_i}"
34
+
35
+ styles = visibility_css(css_class, options)
26
36
 
27
- content_tag(:div, :id => html_id) do
28
- concat visibility_css(html_id, options)
37
+ provide(:invisible_captcha_styles) do
38
+ styles
39
+ end if InvisibleCaptcha.injectable_styles
40
+
41
+ content_tag(:div, class: css_class) do
42
+ concat styles unless InvisibleCaptcha.injectable_styles
29
43
  concat label_tag(build_label_name(honeypot, scope), label)
30
- concat text_field_tag(build_text_field_name(honeypot, scope))
44
+ concat text_field_tag(build_text_field_name(honeypot, scope), nil, options.merge(tabindex: -1))
31
45
  end
32
46
  end
33
47
 
34
- def generate_html_id(honeypot, scope = nil)
35
- "#{scope || honeypot}_#{Time.zone.now.to_i}"
36
- end
37
-
38
- def visibility_css(container_id, options)
39
- visibility = if options.key?(:visual_honeypots)
40
- options[:visual_honeypots]
48
+ def visibility_css(css_class, options)
49
+ visible = if options.key?(:visual_honeypots)
50
+ options.delete(:visual_honeypots)
41
51
  else
42
52
  InvisibleCaptcha.visual_honeypots
43
53
  end
44
54
 
45
- content_tag(:style, :type => 'text/css', :media => 'screen', :scoped => 'scoped') do
46
- "##{container_id} { display:none; }" unless visibility
55
+ return if visible
56
+
57
+ content_tag(:style, media: 'screen') do
58
+ ".#{css_class} {#{InvisibleCaptcha.css_strategy}}"
47
59
  end
48
60
  end
49
61
 
@@ -19,25 +19,29 @@ describe InvisibleCaptcha::ControllerExt, type: :controller do
19
19
  end
20
20
  end
21
21
 
22
- before do
22
+ before(:each) do
23
23
  @controller = TopicsController.new
24
+ request.env['HTTP_REFERER'] = 'http://test.host/topics'
25
+ InvisibleCaptcha.init!
24
26
  InvisibleCaptcha.timestamp_threshold = 1
25
- InvisibleCaptcha.timestamp_enabled = true
26
27
  end
27
28
 
28
29
  context 'without invisible_captcha_timestamp in session' do
29
30
  it 'fails like if it was submitted too fast' do
30
- request.env['HTTP_REFERER'] = 'http://test.host/topics'
31
31
  switchable_post :create, topic: { title: 'foo' }
32
32
 
33
33
  expect(response).to redirect_to 'http://test.host/topics'
34
34
  expect(flash[:error]).to eq(InvisibleCaptcha.timestamp_error_message)
35
35
  end
36
- end
37
36
 
38
- context 'without invisible_captcha_timestamp in session and timestamp_enabled=false' do
39
- it 'does not fail like if it was submitted too fast' do
40
- request.env['HTTP_REFERER'] = 'http://test.host/topics'
37
+ it 'passes if disabled at action level' do
38
+ switchable_post :copy, topic: { title: 'foo' }
39
+
40
+ expect(flash[:error]).not_to be_present
41
+ expect(response.body).to be_present
42
+ end
43
+
44
+ it 'passes if disabled at app level' do
41
45
  InvisibleCaptcha.timestamp_enabled = false
42
46
  switchable_post :create, topic: { title: 'foo' }
43
47
 
@@ -47,22 +51,21 @@ describe InvisibleCaptcha::ControllerExt, type: :controller do
47
51
  end
48
52
 
49
53
  context 'submission timestamp_threshold' do
50
- before do
54
+ before(:each) do
51
55
  session[:invisible_captcha_timestamp] = Time.zone.now.iso8601
52
56
  end
53
57
 
54
58
  it 'fails if submission before timestamp_threshold' do
55
- request.env['HTTP_REFERER'] = 'http://test.host/topics'
56
59
  switchable_post :create, topic: { title: 'foo' }
57
60
 
58
61
  expect(response).to redirect_to 'http://test.host/topics'
59
62
  expect(flash[:error]).to eq(InvisibleCaptcha.timestamp_error_message)
60
63
  end
61
64
 
62
- it 'allow custom on_timestamp_spam callback' do
65
+ it 'allow a custom on_timestamp_spam callback' do
63
66
  switchable_put :update, id: 1, topic: { title: 'bar' }
64
67
 
65
- expect(response).to redirect_to(root_path)
68
+ expect(response.status).to eq(204)
66
69
  end
67
70
 
68
71
  context 'successful submissions' do
@@ -87,7 +90,7 @@ describe InvisibleCaptcha::ControllerExt, type: :controller do
87
90
  end
88
91
 
89
92
  context 'honeypot attribute' do
90
- before do
93
+ before(:each) do
91
94
  session[:invisible_captcha_timestamp] = Time.zone.now.iso8601
92
95
  # Wait for valid submission
93
96
  sleep InvisibleCaptcha.timestamp_threshold
@@ -105,10 +108,17 @@ describe InvisibleCaptcha::ControllerExt, type: :controller do
105
108
  expect(response.body).to be_present
106
109
  end
107
110
 
108
- it 'allow custom on_spam callback' do
111
+ it 'allow a custom on_spam callback' do
109
112
  switchable_put :update, id: 1, topic: { subtitle: 'foo' }
110
113
 
111
114
  expect(response.body).to redirect_to(new_topic_path)
112
115
  end
116
+
117
+ it 'honeypot is removed from params if you use a custom honeypot' do
118
+ switchable_post :create, topic: { title: 'foo', subtitle: '' }
119
+
120
+ expect(flash[:error]).not_to be_present
121
+ expect(@controller.params[:topic].key?(:subtitle)).to eq(false)
122
+ end
113
123
  end
114
124
  end
@@ -1,3 +1,9 @@
1
1
  # Dummy App
2
2
 
3
3
  Dummy Rails Application to test `Invisible Captcha`.
4
+
5
+ It's also used as a demo application to show `Invisible Captcha` in action. You can run the app by using the following command, from the root of the project:
6
+
7
+ > bundle exec rake web
8
+
9
+ [« Back to Docs](https://github.com/markets/invisible_captcha#invisible-captcha)
@@ -1,15 +1 @@
1
- // This is a manifest file that'll be compiled into application.js, which will include all the files
2
- // listed below.
3
- //
4
- // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
- // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6
- //
7
- // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
- // the compiled file.
9
- //
10
- // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
11
- // GO AFTER THE REQUIRES BELOW.
12
- //
13
- //= require jquery
14
- //= require jquery_ujs
15
- //= require_tree .
1
+ console.log('Hi from Invisible Captcha!');
@@ -1,13 +1,31 @@
1
- /*
2
- * This is a manifest file that'll be compiled into application.css, which will include all the files
3
- * listed below.
4
- *
5
- * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
- * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7
- *
8
- * You're free to add application-wide styles to this file and they'll appear at the top of the
9
- * compiled file, but it's generally better to create a new file per style scope.
10
- *
11
- *= require_self
12
- *= require_tree .
13
- */
1
+ body {
2
+ font-family: Arial, Helvetica, sans-serif;
3
+ background-color: #ccc;
4
+ margin: 2em;
5
+ }
6
+
7
+ h1 {
8
+ border-bottom: 3px solid;
9
+ }
10
+
11
+ input,
12
+ textarea {
13
+ border: 0;
14
+ margin-bottom: 1.5em;
15
+ }
16
+
17
+ input {
18
+ height: 2em;
19
+ }
20
+
21
+ button {
22
+ background-color: #f0f0f0;
23
+ border-radius: 0.25em;
24
+ height: 3em;
25
+ width: 10em;
26
+ font-size: 1em;
27
+ }
28
+
29
+ .errors {
30
+ color: darkred;
31
+ }
@@ -1,10 +1,18 @@
1
1
  class TopicsController < ApplicationController
2
2
  invisible_captcha honeypot: :subtitle, only: :create
3
+
3
4
  invisible_captcha honeypot: :subtitle, only: :update,
4
5
  on_spam: :custom_callback,
5
6
  on_timestamp_spam: :custom_timestamp_callback
7
+
6
8
  invisible_captcha honeypot: :subtitle, only: :publish, timestamp_threshold: 2
7
9
 
10
+ invisible_captcha honeypot: :subtitle, only: :copy, timestamp_enabled: false
11
+
12
+ def index
13
+ redirect_to new_topic_path
14
+ end
15
+
8
16
  def new
9
17
  @topic = Topic.new
10
18
  end
@@ -26,6 +34,16 @@ class TopicsController < ApplicationController
26
34
  redirect_to new_topic_path
27
35
  end
28
36
 
37
+ def copy
38
+ @topic = Topic.new(params[:topic])
39
+
40
+ if @topic.valid?
41
+ redirect_to new_topic_path(context: params[:context]), notice: 'Success!'
42
+ else
43
+ render action: 'new'
44
+ end
45
+ end
46
+
29
47
  private
30
48
 
31
49
  def custom_callback
@@ -33,6 +51,6 @@ class TopicsController < ApplicationController
33
51
  end
34
52
 
35
53
  def custom_timestamp_callback
36
- redirect_to root_path
54
+ head(204)
37
55
  end
38
56
  end
@@ -2,9 +2,11 @@ class Topic
2
2
  include ActiveModel::Validations
3
3
  include ActiveModel::Conversion
4
4
 
5
- attr_accessor :title, :body, :subtitle
5
+ attr_accessor :title, :author, :body, :subtitle
6
6
 
7
- validates :title, presence: true
7
+ validates :title, length: { minimum: 5 }
8
+ validates :author, presence: true
9
+ validates :body, length: { minimum: 10 }
8
10
 
9
11
  def initialize(attributes = {})
10
12
  attributes.each do |name, value|
@@ -5,21 +5,25 @@
5
5
  <%= stylesheet_link_tag "application", :media => "all" %>
6
6
  <%= javascript_include_tag "application" %>
7
7
  <%= csrf_meta_tags %>
8
+ <%= invisible_captcha_styles %>
8
9
  </head>
9
10
  <body>
11
+ <h1>InvisibleCaptcha v<%= InvisibleCaptcha::VERSION %> - Demo</h1>
10
12
 
11
- <%= link_to "Default", new_topic_path %> |
12
- <%= link_to "With visual honeypots", new_topic_path(context: "visual_honeypots") %>
13
+ <p>
14
+ <%= link_to "Default settings", new_topic_path %> |
15
+ <%= link_to "With visual honeypots", new_topic_path(context: "visual_honeypots") %> |
16
+ <%= link_to "With timestamp disabled", new_topic_path(context: "timestamp_disabled") %>
17
+ </p>
13
18
 
14
- <% flash.each do |key, value| %>
15
- <ul>
16
- <li>
17
- <%= "[#{key.upcase}] #{value}" %>
18
- </li>
19
- </ul>
20
- <% end %>
19
+ <% flash.each do |key, value| %>
20
+ <ul class="errors">
21
+ <li>
22
+ <%= "[#{key.upcase}] #{value}" %>
23
+ </li>
24
+ </ul>
25
+ <% end %>
21
26
 
22
- <%= yield %>
23
-
24
- </body>
27
+ <%= yield %>
28
+ </body>
25
29
  </html>
@@ -1,8 +1,6 @@
1
- <h1>New topic</h1>
2
-
3
1
  <% if @topic.errors.any? %>
4
- <div>
5
- <h2><%= pluralize(@topic.errors.count, "error") %> prohibited this record from being saved:</h2>
2
+ <div class="errors">
3
+ <strong><%= pluralize(@topic.errors.count, "error") %> prohibited this record from being saved:</strong>
6
4
  <ul>
7
5
  <% @topic.errors.full_messages.each do |msg| %>
8
6
  <li><%= msg %></li>
@@ -11,13 +9,13 @@
11
9
  </div>
12
10
  <% end %>
13
11
 
14
- <%= form_for(@topic) do |f| %>
12
+ <%= form_for(@topic, url: { action: params[:context] == 'timestamp_disabled' ? :copy : :create }) do |f| %>
15
13
  <%= hidden_field_tag :context, params[:context] %>
16
14
 
17
- <% if params[:context].blank? || params[:context] == 'default' %>
18
- <%= f.invisible_captcha :subtitle %>
19
- <% else %>
15
+ <% if params[:context] && params[:context] == 'visual_honeypots' %>
20
16
  <%= f.invisible_captcha :subtitle, visual_honeypots: true %>
17
+ <% else %>
18
+ <%= f.invisible_captcha :subtitle %>
21
19
  <% end %>
22
20
 
23
21
  <div class="field">
@@ -25,12 +23,17 @@
25
23
  <%= f.text_field :title %>
26
24
  </div>
27
25
 
26
+ <div class="field">
27
+ <%= f.label :author %><br />
28
+ <%= f.text_field :author %>
29
+ </div>
30
+
28
31
  <div class="field">
29
32
  <%= f.label :body %><br />
30
- <%= f.text_area :body %>
33
+ <%= f.text_area :body, rows: 10, cols: 40 %>
31
34
  </div>
32
35
 
33
36
  <div class="actions">
34
- <%= f.submit %>
37
+ <%= f.button 'Save' %>
35
38
  </div>
36
- <% end %>
39
+ <% end %>
@@ -4,6 +4,7 @@ require 'action_controller/railtie'
4
4
  require 'action_view/railtie'
5
5
  require 'action_mailer/railtie'
6
6
  require 'active_model/railtie'
7
+ require 'sprockets/railtie'
7
8
 
8
9
  # Require the gems listed in Gemfile, including any gems
9
10
  # you've limited to :test, :development, or :production.
@@ -31,6 +31,9 @@ Dummy::Application.configure do
31
31
  # yet still be able to expire them through the digest params.
32
32
  # config.assets.digest = true
33
33
 
34
+ # quiet assets
35
+ config.assets.quiet = true
36
+
34
37
  # Adds additional error checking when serving assets at runtime.
35
38
  # Checks for improperly declared sprockets dependencies.
36
39
  # Raises helpful error messages.
@@ -1,8 +1,7 @@
1
1
  Rails.application.routes.draw do
2
- resources :topics, only: [:new, :create, :update] do
3
- member do
4
- post :publish
5
- end
2
+ resources :topics do
3
+ post :publish, on: :member
4
+ post :copy, on: :collection
6
5
  end
7
6
 
8
7
  root to: 'topics#new'
@@ -5,10 +5,10 @@ describe InvisibleCaptcha do
5
5
  InvisibleCaptcha.init!
6
6
 
7
7
  expect(InvisibleCaptcha.sentence_for_humans).to eq('If you are a human, ignore this field')
8
- expect(InvisibleCaptcha.error_message).to eq('You are a robot!')
9
8
  expect(InvisibleCaptcha.timestamp_threshold).to eq(4.seconds)
10
9
  expect(InvisibleCaptcha.timestamp_error_message).to eq('Sorry, that was too quick! Please resubmit.')
11
- expect(InvisibleCaptcha.honeypots).to eq(['foo_id', 'bar_id', 'baz_id'])
10
+ expect(InvisibleCaptcha.honeypots).to be_an_instance_of(Array)
11
+ expect(InvisibleCaptcha.injectable_styles).to eq(false)
12
12
  end
13
13
 
14
14
  it 'allow setup via block' do
@@ -27,28 +27,23 @@ describe InvisibleCaptcha do
27
27
  I18n.backend.store_translations(:en,
28
28
  'invisible_captcha' => {
29
29
  'sentence_for_humans' => "Can't touch this",
30
- 'error_message' => 'MR ROBOT',
31
30
  'timestamp_error_message' => 'Fast and furious' })
32
31
 
33
32
  I18n.backend.store_translations(:fr,
34
33
  'invisible_captcha' => {
35
34
  'sentence_for_humans' => 'Ne touchez pas',
36
- 'error_message' => 'Mon dieu, un robot!',
37
35
  'timestamp_error_message' => 'Plus doucement SVP' })
38
36
 
39
37
  I18n.locale = :en
40
38
  expect(InvisibleCaptcha.sentence_for_humans).to eq("Can't touch this")
41
- expect(InvisibleCaptcha.error_message).to eq('MR ROBOT')
42
39
  expect(InvisibleCaptcha.timestamp_error_message).to eq('Fast and furious')
43
40
 
44
41
  I18n.locale = :fr
45
42
  expect(InvisibleCaptcha.sentence_for_humans).to eq('Ne touchez pas')
46
- expect(InvisibleCaptcha.error_message).to eq('Mon dieu, un robot!')
47
43
  expect(InvisibleCaptcha.timestamp_error_message).to eq('Plus doucement SVP')
48
44
 
49
45
  I18n.backend.reload!
50
46
  expect(InvisibleCaptcha.sentence_for_humans).to eq('If you are a human, ignore this field')
51
- expect(InvisibleCaptcha.error_message).to eq('You are a robot!')
52
47
  expect(InvisibleCaptcha.timestamp_error_message).to eq('Sorry, that was too quick! Please resubmit.')
53
48
  end
54
49
  end
@@ -1,81 +1,75 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe InvisibleCaptcha::ViewHelpers, type: :helper do
4
- def helper_output(honeypot = nil, scope = nil, options = {})
5
- honeypot ||= InvisibleCaptcha.get_honeypot
6
- input_id = build_label_name(honeypot, scope)
7
- input_name = build_text_field_name(honeypot, scope)
8
- html_id = generate_html_id(honeypot, scope)
9
- visibilty = if options.key?(:visual_honeypots)
10
- options[:visual_honeypots]
11
- else
12
- InvisibleCaptcha.visual_honeypots
13
- end
14
- style_attributes, input_attributes = if Gem::Version.new(Rails.version) > Gem::Version.new("4.2.0")
15
- [
16
- 'type="text/css" media="screen" scoped="scoped"',
17
- "type=\"text\" name=\"#{input_name}\" id=\"#{input_id}\""
18
- ]
19
- else
20
- [
21
- 'media="screen" scoped="scoped" type="text/css"',
22
- "id=\"#{input_id}\" name=\"#{input_name}\" type=\"text\""
23
- ]
24
- end
4
+ before(:each) do
5
+ allow(Time.zone).to receive(:now).and_return(Time.zone.parse('Feb 19 1986'))
6
+ allow(InvisibleCaptcha).to receive(:css_strategy).and_return("display:none;")
25
7
 
26
- %{
27
- <div id="#{html_id}">
28
- <style #{style_attributes}>#{visibilty ? '' : "##{html_id} { display:none; }"}</style>
29
- <label for="#{input_id}">#{InvisibleCaptcha.sentence_for_humans}</label>
30
- <input #{input_attributes} />
31
- </div>
32
- }.gsub(/\s+/, ' ').strip.gsub('> <', '><')
33
- end
8
+ # to test content_for and provide
9
+ @view_flow = ActionView::OutputFlow.new
34
10
 
35
- before do
36
- allow(Time.zone).to receive(:now).and_return(Time.zone.parse('Feb 19 1986'))
37
- InvisibleCaptcha.visual_honeypots = false
38
- InvisibleCaptcha.timestamp_enabled = true
11
+ InvisibleCaptcha.init!
39
12
  end
40
13
 
41
14
  it 'with no arguments' do
42
15
  InvisibleCaptcha.honeypots = [:foo_id]
43
- expect(invisible_captcha).to eq(helper_output)
16
+ expect(invisible_captcha).to match(/name="foo_id"/)
44
17
  end
45
18
 
46
19
  it 'with specific honeypot' do
47
- expect(invisible_captcha(:subtitle)).to eq(helper_output(:subtitle))
20
+ expect(invisible_captcha(:subtitle)).to match(/name="subtitle"/)
48
21
  end
49
22
 
50
23
  it 'with specific honeypot and scope' do
51
- expect(invisible_captcha(:subtitle, :topic)).to eq(helper_output(:subtitle, :topic))
24
+ expect(invisible_captcha(:subtitle, :topic)).to match(/name="topic\[subtitle\]"/)
25
+ end
26
+
27
+ it 'with custom html options' do
28
+ expect(invisible_captcha(:subtitle, :topic, { class: 'foo_class' })).to match(/class="foo_class"/)
29
+ end
30
+
31
+ it 'generated html + styles' do
32
+ InvisibleCaptcha.honeypots = [:foo_id]
33
+ output = invisible_captcha.gsub("\"", "'")
34
+ regexp = %r{<div class='foo_id_\w*'><style.*>.foo_id_\w* {display:none;}</style><label.*>#{InvisibleCaptcha.sentence_for_humans}.*<input.*name='foo_id'.*tabindex='-1'.*</div>}
35
+
36
+ expect(output).to match(regexp)
52
37
  end
53
38
 
54
39
  context "honeypot visibilty" do
55
40
  it 'visible from defaults' do
56
- InvisibleCaptcha.honeypots = [:foo_id]
57
41
  InvisibleCaptcha.visual_honeypots = true
58
42
 
59
- expect(invisible_captcha).to eq(helper_output)
43
+ expect(invisible_captcha).not_to match(/display:none/)
60
44
  end
61
45
 
62
46
  it 'visible from given instance (default override)' do
63
- InvisibleCaptcha.honeypots = [:foo_id]
64
-
65
- expect(invisible_captcha(visual_honeypots: true)).to eq(helper_output(nil, nil, visual_honeypots: true))
47
+ expect(invisible_captcha(visual_honeypots: true)).not_to match(/display:none/)
66
48
  end
67
49
 
68
50
  it 'invisible from given instance (default override)' do
69
- InvisibleCaptcha.honeypots = [:foo_id]
70
51
  InvisibleCaptcha.visual_honeypots = true
71
52
 
72
- expect(invisible_captcha(visual_honeypots: false)).to eq(helper_output(nil, nil, visual_honeypots: false))
53
+ expect(invisible_captcha(visual_honeypots: false)).to match(/display:none/)
73
54
  end
74
55
  end
75
56
 
76
57
  it 'should set spam timestamp' do
77
- InvisibleCaptcha.honeypots = [:foo_id]
78
58
  invisible_captcha
79
59
  expect(session[:invisible_captcha_timestamp]).to eq(Time.zone.now.iso8601)
80
60
  end
61
+
62
+ context 'injectable_styles option' do
63
+ it 'by default, render styles along with the honeypot' do
64
+ expect(invisible_captcha).to match(/display:none/)
65
+ expect(helper.content_for(:invisible_captcha_styles)).to be_blank
66
+ end
67
+
68
+ it 'if injectable_styles is set, do not append styles inline' do
69
+ InvisibleCaptcha.injectable_styles = true
70
+
71
+ expect(invisible_captcha).not_to match(/display:none;/)
72
+ expect(helper.content_for(:invisible_captcha_styles)).to match(/display:none;/)
73
+ end
74
+ end
81
75
  end
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.9.3
4
+ version: 0.10.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: 2017-07-17 00:00:00.000000000 Z
11
+ date: 2017-12-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -67,19 +67,19 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '3.0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: mime-types
70
+ name: byebug
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - "<"
73
+ - - ">="
74
74
  - !ruby/object:Gem::Version
75
- version: '3.0'
75
+ version: '0'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - "<"
80
+ - - ">="
81
81
  - !ruby/object:Gem::Version
82
- version: '3.0'
82
+ version: '0'
83
83
  description: Unobtrusive, flexible and simple spam protection for Rails applications
84
84
  using honeypot strategy for better user experience.
85
85
  email:
@@ -116,7 +116,6 @@ files:
116
116
  - spec/dummy/app/controllers/topics_controller.rb
117
117
  - spec/dummy/app/helpers/application_helper.rb
118
118
  - spec/dummy/app/mailers/.gitkeep
119
- - spec/dummy/app/models/.gitkeep
120
119
  - spec/dummy/app/models/topic.rb
121
120
  - spec/dummy/app/views/layouts/application.html.erb
122
121
  - spec/dummy/app/views/topics/new.html.erb
@@ -135,7 +134,6 @@ files:
135
134
  - spec/dummy/config/initializers/cookies_serializer.rb
136
135
  - spec/dummy/config/initializers/filter_parameter_logging.rb
137
136
  - spec/dummy/config/initializers/inflections.rb
138
- - spec/dummy/config/initializers/invisible_captcha.rb
139
137
  - spec/dummy/config/initializers/mime_types.rb
140
138
  - spec/dummy/config/initializers/secret_token.rb
141
139
  - spec/dummy/config/initializers/session_store.rb
@@ -172,7 +170,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
172
170
  version: '0'
173
171
  requirements: []
174
172
  rubyforge_project:
175
- rubygems_version: 2.5.2
173
+ rubygems_version: 2.6.13
176
174
  signing_key:
177
175
  specification_version: 4
178
176
  summary: Simple honeypot protection for RoR apps
@@ -186,7 +184,6 @@ test_files:
186
184
  - spec/dummy/app/controllers/topics_controller.rb
187
185
  - spec/dummy/app/helpers/application_helper.rb
188
186
  - spec/dummy/app/mailers/.gitkeep
189
- - spec/dummy/app/models/.gitkeep
190
187
  - spec/dummy/app/models/topic.rb
191
188
  - spec/dummy/app/views/layouts/application.html.erb
192
189
  - spec/dummy/app/views/topics/new.html.erb
@@ -205,7 +202,6 @@ test_files:
205
202
  - spec/dummy/config/initializers/cookies_serializer.rb
206
203
  - spec/dummy/config/initializers/filter_parameter_logging.rb
207
204
  - spec/dummy/config/initializers/inflections.rb
208
- - spec/dummy/config/initializers/invisible_captcha.rb
209
205
  - spec/dummy/config/initializers/mime_types.rb
210
206
  - spec/dummy/config/initializers/secret_token.rb
211
207
  - spec/dummy/config/initializers/session_store.rb
File without changes
@@ -1,6 +0,0 @@
1
- InvisibleCaptcha.setup do |config|
2
- # config.sentence_for_humans = 'If you are a human, ignore this field'
3
- # config.error_message = 'You are a robot!'
4
- # config.honeypots += 'fake_resource_title'
5
- # config.visual_honeypots = false
6
- end