invisible_captcha 0.9.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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