invisible_captcha 0.6.5 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -1
  3. data/.travis.yml +5 -0
  4. data/README.md +107 -47
  5. data/Rakefile +17 -1
  6. data/invisible_captcha.gemspec +2 -2
  7. data/lib/invisible_captcha.rb +28 -24
  8. data/lib/invisible_captcha/controller_ext.rb +48 -0
  9. data/lib/invisible_captcha/form_helpers.rb +3 -7
  10. data/lib/invisible_captcha/railtie.rb +9 -0
  11. data/lib/invisible_captcha/validator.rb +3 -7
  12. data/lib/invisible_captcha/version.rb +1 -1
  13. data/lib/invisible_captcha/view_helpers.rb +25 -23
  14. data/spec/controllers_spec.rb +25 -0
  15. data/spec/dummy/README.md +3 -0
  16. data/spec/dummy/Rakefile +7 -0
  17. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  18. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  19. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  20. data/spec/dummy/app/controllers/topics_controller.rb +27 -0
  21. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  22. data/spec/dummy/app/mailers/.gitkeep +0 -0
  23. data/spec/dummy/app/models/.gitkeep +0 -0
  24. data/spec/dummy/app/models/topic.rb +18 -0
  25. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  26. data/spec/dummy/app/views/topics/new.html.erb +17 -0
  27. data/spec/dummy/config.ru +4 -0
  28. data/spec/dummy/config/application.rb +20 -0
  29. data/spec/dummy/config/boot.rb +5 -0
  30. data/spec/dummy/config/environment.rb +5 -0
  31. data/spec/dummy/config/environments/development.rb +30 -0
  32. data/spec/dummy/config/environments/production.rb +67 -0
  33. data/spec/dummy/config/environments/test.rb +34 -0
  34. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  35. data/spec/dummy/config/initializers/inflections.rb +15 -0
  36. data/spec/dummy/config/initializers/invisible_captcha.rb +6 -0
  37. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  38. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  39. data/spec/dummy/config/initializers/session_store.rb +8 -0
  40. data/spec/dummy/config/initializers/wrap_parameters.rb +9 -0
  41. data/spec/dummy/config/locales/en.yml +5 -0
  42. data/spec/dummy/config/routes.rb +5 -0
  43. data/spec/dummy/lib/assets/.gitkeep +0 -0
  44. data/spec/dummy/log/.gitkeep +0 -0
  45. data/spec/dummy/public/404.html +26 -0
  46. data/spec/dummy/public/422.html +26 -0
  47. data/spec/dummy/public/500.html +25 -0
  48. data/spec/dummy/public/favicon.ico +0 -0
  49. data/spec/dummy/script/rails +5 -0
  50. data/spec/helpers_spec.rb +43 -0
  51. data/spec/invisible_captcha_spec.rb +19 -0
  52. data/spec/spec_helper.rb +16 -0
  53. data/spec/validator_spec.rb +12 -0
  54. metadata +100 -24
  55. data/lib/invisible_captcha/controller_methods.rb +0 -21
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 57c21321c483942e15254c832843c09cbbba7830
4
+ data.tar.gz: 6c18c874fdd903093e1695a362064fd2d2b274cc
5
+ SHA512:
6
+ metadata.gz: ed8400648117aac3086d26176f2f9d67eb615f737f4078ed5c817bb3604ac8d85b295266c92bcca9ceeba25953ee68601a69a79044b804b3287bfd8c28b1eaf0
7
+ data.tar.gz: be584800363bf741d078f3a09f931e98544da1af91ce7757b78b2d5e4e8f896ab93583f974a7a471f8da20ba37c0fdb85f48e52b1be163cdeba3716b6acc0ca1
data/.gitignore CHANGED
@@ -1,4 +1,6 @@
1
1
  .bundle
2
2
  pkg
3
3
  .rvmrc
4
- Gemfile.lock
4
+ Gemfile.lock
5
+ spec/dummy/log/*.log
6
+ spec/dummy/tmp/
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1
4
+ - 2.0
5
+ - 1.9.3
data/README.md CHANGED
@@ -1,106 +1,166 @@
1
1
  # Invisible Captcha
2
- Simple and flexible spam protection solution for Rails applications using honeypot strategy and for better user experience.
3
- Support for ActiveRecord (and ActiveModel) forms and for non-RESTful resources.
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/invisible_captcha.svg)](http://badge.fury.io/rb/invisible_captcha) [![Build Status](https://travis-ci.org/markets/invisible_captcha.svg)](https://travis-ci.org/markets/invisible_captcha)
4
+
5
+ Simple and flexible spam protection solution for Rails applications. Based on the `honeypot` strategy to provide a better user experience.
6
+
7
+ **Background**
8
+
9
+ The strategy is based on adding an input field into the form that:
10
+
11
+ * shouldn't be visible by the real users
12
+ * should be left empty by the real users
13
+ * will most be filled by spam bots
4
14
 
5
15
  ## Installation
16
+
6
17
  Add this line to you Gemfile:
7
18
 
8
19
  ```
9
20
  gem 'invisible_captcha'
10
21
  ```
11
22
 
12
- Or install gem manually:
23
+ Or install the gem manually:
13
24
 
14
25
  ```
15
- gem install invisible_captcha
26
+ $ gem install invisible_captcha
16
27
  ```
17
28
 
18
29
  ## Usage
19
- There are different ways to implement:
20
30
 
21
- ### Model style
31
+ There are different ways to implement, at Controller level or Model level:
32
+
33
+ ### Controller style
34
+
22
35
  View code:
23
36
 
24
37
  ```erb
25
- <%= form_for(@topic) do |f| %>
26
- <%= f.invisible_captcha :subtitle %>
38
+ <%= form_tag(create_topic_path) do |f| %>
39
+ <%= invisible_captcha %>
27
40
  <% end %>
28
41
  ```
29
42
 
30
- Model code:
43
+ Controller code:
31
44
 
32
45
  ```ruby
33
- class Topic < ActiveRecord::Base
34
- attr_accessor :subtitle # virtual attribute, the honeypot
35
- validates :subtitle, :invisible_captcha => true
46
+ class TopicsController < ApplicationController
47
+ invisible_captcha only: [:create, :update]
36
48
  end
37
49
  ```
38
50
 
39
- If you are using [strong_parameters](https://github.com/rails/strong_parameters), don't forget to keep the honeypot attribute into the params hash:
51
+ This method will act as a `before_filter` that triggers when spam is detected (honeypot field has some value). By default it responds with no content (only headers: `head(200)`). But you are able to define your own callback by passing a method to the `on_spam` option:
52
+
40
53
  ```ruby
41
- def topic_params
42
- params.require(:topic).permit(:subtitle)
54
+ invisible_captcha only: [:create, :update], on_spam: :your_on_spam_callback_method
55
+
56
+ private
57
+
58
+ def your_on_spam_callback_method
59
+ redirect_to root_path
43
60
  end
44
61
  ```
45
62
 
46
- ### Controller style
47
- View code:
63
+ [Check here a complete list of allowed options.](#controller-method-options)
64
+
65
+ ### Controller style (resource oriented):
66
+
67
+ In your form:
48
68
 
49
69
  ```erb
50
70
  <%= form_for(@topic) do |f| %>
51
- <%= invisible_captcha %>
71
+ <%= f.invisible_captcha :subtitle %>
72
+ <!-- or -->
73
+ <%= invisible_captcha :subtitle, :topic %>
52
74
  <% end %>
53
75
  ```
54
76
 
55
- Controller code:
77
+ In your controller:
56
78
 
57
79
  ```ruby
58
- class TopicsController < ApplicationController
59
- before_filter :check_invisible_captcha # :only => [:create, :update]
60
- # your controller code
61
- end
80
+ invisible_captcha only: [:create, :update], honeypot: :subtitle
62
81
  ```
63
82
 
64
- This filter triggers when the spam is the in params and responds without content (only headers). If you desire a different behaviour, you can use a provided method to check manualy if invisible captcha (fake field) is present:
65
-
66
- ```ruby
67
- if invisible_captcha?
68
- # spam present
69
- else
70
- # no spam
71
- end
72
- ```
83
+ ### Model style
73
84
 
74
- ### Controller style (resource oriented):
85
+ View code:
75
86
 
76
- In your form:
77
87
  ```erb
78
88
  <%= form_for(@topic) do |f| %>
79
89
  <%= f.invisible_captcha :subtitle %>
80
90
  <% end %>
81
91
  ```
82
92
 
83
- In your controller:
93
+ Model code:
94
+
84
95
  ```ruby
85
- def create
86
- if invisible_captcha?(:topic, :subtitle)
87
- head 200 # or redirect_to new_topic_path
88
- else
89
- # regular workflow
90
- end
96
+ class Topic < ActiveRecord::Base
97
+ attr_accessor :subtitle # define a virtual attribute, the honeypot
98
+ validates :subtitle, :invisible_captcha => true
91
99
  end
92
100
  ```
93
101
 
94
- ### Setup
95
- If you want to customize some defaults, add the following to an initializer (config/initializers/invisible_captcha.rb):
102
+ If you are using [strong_parameters](https://github.com/rails/strong_parameters) (by default in Rails 4), don't forget to keep the honeypot attribute into the params hash:
103
+
104
+ ```ruby
105
+ def topic_params
106
+ params.require(:topic).permit(:subtitle)
107
+ end
108
+ ```
109
+
110
+ ## Options and customization
111
+
112
+ This section contains the option list of `invisible_captcha` method (controllers side) and the plugin setup options (initializer).
113
+
114
+ ### Controller method options:
115
+
116
+ The `invisible_captcha` method accepts some options:
117
+
118
+ * `only`: apply to given controller actions.
119
+ * `except`: exclude to given controller actions.
120
+ * `honeypot`: name of honeypot.
121
+ * `scope`: name of scope, ie: 'topic[subtitle]' -> 'topic' is the scope.
122
+ * `on_spam`: custom callback to be called on spam detection.
123
+
124
+ ### Plugin options:
125
+
126
+ You also can customize some plugin options:
127
+
128
+ * `sentence_for_humans`: text for real users if input field was visible.
129
+ * `error_message`: error message thrown by model validation (only model implementation).
130
+ * `honeypots`: collection of default honeypots, used by the view helper, called with no args, to generate the honeypot field name
131
+ * `visual_honeypots`: make honeypots visible, useful to test/debug your implementation.
132
+
133
+ To change these defaults, add the following to an initializer (recommended `config/initializers/invisible_captcha.rb`):
96
134
 
97
135
  ```ruby
98
- InvisibleCaptcha.setup do |ic|
99
- ic.sentence_for_humans = 'If you are a human, ignore this field'
100
- ic.error_message = 'You are a robot!'
101
- ic.fake_fields << 'another_fake_field'
136
+ InvisibleCaptcha.setup do |config|
137
+ config.sentence_for_humans = 'If you are a human, ignore this field'
138
+ config.error_message = 'You are a robot!'
139
+ config.honeypots += 'fake_resource_title'
140
+ config.visual_honeypots = false
102
141
  end
103
142
  ```
104
143
 
144
+ ## Contribute
145
+
146
+ Any kind of idea, feedback or bug report are welcome! Open an [issue](https://github.com/markets/invisible_captcha/issues) or send a [pull request](https://github.com/markets/invisible_captcha/pulls).
147
+
148
+ ## Development
149
+
150
+ Clone/fork this repository and start to hack.
151
+
152
+ Run test suite:
153
+
154
+ ```
155
+ $ rspec
156
+ ```
157
+
158
+ Start a sample Rails app ([source code](spec/dummy)) with `InvisibleCaptcha` integrated:
159
+
160
+ ```
161
+ $ rake web # PORT=4000 (default: 3000)
162
+ ```
163
+
105
164
  ## License
165
+
106
166
  Copyright (c) 2012-2014 Marc Anguera. Invisible Captcha is released under the [MIT](LICENSE) License.
data/Rakefile CHANGED
@@ -1 +1,17 @@
1
- require "bundler/gem_tasks"
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
8
+ desc 'Start development Rails app'
9
+ task :web do
10
+ app_path = 'spec/dummy'
11
+ port = ENV['PORT'] || 3000
12
+
13
+ puts "Starting application in http://localhost:#{port} ... \n"
14
+
15
+ Dir.chdir(app_path)
16
+ `rails s -p #{port}`
17
+ end
@@ -16,8 +16,8 @@ Gem::Specification.new do |spec|
16
16
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
17
17
  spec.require_paths = ["lib"]
18
18
 
19
- spec.add_dependency "activemodel", ">= 0"
19
+ spec.add_dependency 'rails'
20
20
 
21
- spec.add_development_dependency "debugger"
21
+ spec.add_development_dependency 'rspec-rails', '~> 3.1'
22
22
  end
23
23
 
@@ -1,32 +1,36 @@
1
1
  require 'invisible_captcha/version'
2
- require 'invisible_captcha/controller_methods'
2
+ require 'invisible_captcha/controller_ext'
3
3
  require 'invisible_captcha/view_helpers'
4
4
  require 'invisible_captcha/form_helpers'
5
5
  require 'invisible_captcha/validator'
6
+ require 'invisible_captcha/railtie'
6
7
 
7
8
  module InvisibleCaptcha
8
- # Default sentence for humans if text field is visible
9
- mattr_accessor :sentence_for_humans
10
- self.sentence_for_humans = 'If you are a human, ignore this field'
11
-
12
- # Default error message for validator
13
- mattr_accessor :error_message
14
- self.error_message = 'You are a robot!'
15
-
16
- # Default fake fields for controller based workflow
17
- mattr_accessor :fake_fields
18
- self.fake_fields = ['foo_id', 'bar_id', 'baz_id']
19
-
20
- # InvisibleCaptcha.setup do |ic|
21
- # ic.sentence_for_humans = 'Another sentence'
22
- # ic.error_message = 'Another error message'
23
- # ic.fake_fields << 'another_fake_field'
24
- # end
25
- def self.setup
26
- yield(self)
27
- end
9
+ class << self
10
+ attr_accessor :sentence_for_humans, :error_message, :honeypots, :visual_honeypots
11
+
12
+ def init!
13
+ # Default sentence for real users if text field was visible
14
+ self.sentence_for_humans = 'If you are a human, ignore this field'
15
+
16
+ # Default error message for validator
17
+ self.error_message = 'You are a robot!'
18
+
19
+ # Default fake fields for controller based workflow
20
+ self.honeypots = ['foo_id', 'bar_id', 'baz_id']
28
21
 
29
- def self.fake_field
30
- self.fake_fields.sample
22
+ # Make honeypots visibles
23
+ self.visual_honeypots = false
24
+ end
25
+
26
+ def setup
27
+ yield(self) if block_given?
28
+ end
29
+
30
+ def get_honeypot
31
+ honeypots.sample
32
+ end
31
33
  end
32
- end
34
+ end
35
+
36
+ InvisibleCaptcha.init!
@@ -0,0 +1,48 @@
1
+ module InvisibleCaptcha
2
+ module ControllerExt
3
+ module ClassMethods
4
+ def invisible_captcha(options = {})
5
+ before_filter(options) do
6
+ detect_spam(options)
7
+ end
8
+ end
9
+ end
10
+
11
+ def detect_spam(options = {})
12
+ if invisible_captcha?(options)
13
+ on_spam_action(options)
14
+ end
15
+ end
16
+
17
+ def on_spam_action(options = {})
18
+ if action = options[:on_spam]
19
+ send(action)
20
+ else
21
+ default_on_spam
22
+ end
23
+ end
24
+
25
+ def default_on_spam
26
+ head(200)
27
+ end
28
+
29
+ def invisible_captcha?(options = {})
30
+ honeypot = options[:honeypot]
31
+ scope = options[:scope] || controller_name.singularize
32
+
33
+ if honeypot
34
+ # If honeypot is presented, search for:
35
+ # - honeypot: params[:subtitle]
36
+ # - honeypot with scope: params[:topic][:subtitle]
37
+ if params[honeypot].present? || (params[scope] && params[scope][honeypot].present?)
38
+ return true
39
+ end
40
+ else
41
+ InvisibleCaptcha.honeypots.each do |field|
42
+ return true if params[field].present?
43
+ end
44
+ end
45
+ false
46
+ end
47
+ end
48
+ end
@@ -1,11 +1,7 @@
1
1
  module InvisibleCaptcha
2
2
  module FormHelpers
3
-
4
- def invisible_captcha(method)
5
- @template.invisible_captcha(self.object_name, method)
3
+ def invisible_captcha(honeypot)
4
+ @template.invisible_captcha(honeypot, self.object_name)
6
5
  end
7
-
8
6
  end
9
- end
10
-
11
- ActionView::Helpers::FormBuilder.send :include, InvisibleCaptcha::FormHelpers
7
+ end
@@ -0,0 +1,9 @@
1
+ module InvisibleCaptcha
2
+ class Railtie < Rails::Railtie
3
+ ActionController::Base.send :include, InvisibleCaptcha::ControllerExt
4
+ ActionController::Base.send :extend, InvisibleCaptcha::ControllerExt::ClassMethods
5
+ ActionView::Base.send :include, InvisibleCaptcha::ViewHelpers
6
+ ActionView::Helpers::FormBuilder.send :include, InvisibleCaptcha::FormHelpers
7
+ ActiveModel::Validations::InvisibleCaptchaValidator = InvisibleCaptcha::InvisibleCaptchaValidator
8
+ end
9
+ end
@@ -2,7 +2,6 @@ require 'active_model/validator'
2
2
 
3
3
  module InvisibleCaptcha
4
4
  class InvisibleCaptchaValidator < ActiveModel::EachValidator
5
-
6
5
  def validate_each(record, attribute, value)
7
6
  if invisible_captcha?(record, attribute)
8
7
  record.errors.clear
@@ -12,11 +11,8 @@ module InvisibleCaptcha
12
11
 
13
12
  private
14
13
 
15
- def invisible_captcha?(object, attribute)
16
- object.send(attribute).present?
14
+ def invisible_captcha?(object, honeypot)
15
+ object.send(honeypot).present?
17
16
  end
18
-
19
17
  end
20
- end
21
-
22
- ActiveModel::Validations::InvisibleCaptchaValidator = InvisibleCaptcha::InvisibleCaptchaValidator
18
+ end
@@ -1,3 +1,3 @@
1
1
  module InvisibleCaptcha
2
- VERSION = "0.6.5"
2
+ VERSION = "0.7.0"
3
3
  end
@@ -1,50 +1,52 @@
1
1
  module InvisibleCaptcha
2
2
  module ViewHelpers
3
-
4
- def invisible_captcha(resource = nil, method = nil)
5
- build_invisible_captcha(resource, method)
3
+ # Builds the honeypot html
4
+ #
5
+ # @param honeypot [Symbol] name of honeypot, ie: subtitle => input name: subtitle
6
+ # @param scope [Symbol] name of honeypot scope, ie: topic => input name: topic[subtitle]
7
+ # @return [String] the generated html
8
+ def invisible_captcha(honeypot = nil, scope = nil)
9
+ build_invisible_captcha(honeypot, scope)
6
10
  end
7
11
 
8
12
  private
9
13
 
10
- def build_invisible_captcha(resource = nil, method = nil)
11
- resource = resource ? resource.to_s : InvisibleCaptcha.fake_field
12
- label = InvisibleCaptcha.sentence_for_humans
13
- html_id = generate_html_id(resource)
14
+ def build_invisible_captcha(honeypot = nil, scope = nil)
15
+ honeypot = honeypot ? honeypot.to_s : InvisibleCaptcha.get_honeypot
16
+ label = InvisibleCaptcha.sentence_for_humans
17
+ html_id = generate_html_id(honeypot, scope)
14
18
 
15
19
  content_tag(:div, :id => html_id) do
16
20
  insert_inline_css(html_id) +
17
- label_tag(build_label_name(resource, method), label) +
18
- text_field_tag(build_text_field_name(resource, method))
21
+ label_tag(build_label_name(honeypot, scope), label) +
22
+ text_field_tag(build_text_field_name(honeypot, scope))
19
23
  end.html_safe
20
24
  end
21
25
 
22
- def generate_html_id(resource)
23
- "#{resource}_#{Time.now.to_i}"
26
+ def generate_html_id(honeypot, scope = nil)
27
+ "#{scope || honeypot}_#{Time.now.to_i}"
24
28
  end
25
29
 
26
30
  def insert_inline_css(container_id)
27
31
  content_tag(:style, :type => 'text/css', :media => 'screen', :scoped => 'scoped') do
28
- "##{container_id} { display:none; }"
32
+ "##{container_id} { display:none; }" unless InvisibleCaptcha.visual_honeypots
29
33
  end
30
34
  end
31
35
 
32
- def build_label_name(resource, method = nil)
33
- if method.present?
34
- "#{resource}_#{method}"
36
+ def build_label_name(honeypot, scope = nil)
37
+ if scope.present?
38
+ "#{scope}_#{honeypot}"
35
39
  else
36
- resource
40
+ honeypot
37
41
  end
38
42
  end
39
43
 
40
- def build_text_field_name(resource, method = nil)
41
- if method.present?
42
- "#{resource}[#{method}]"
44
+ def build_text_field_name(honeypot, scope = nil)
45
+ if scope.present?
46
+ "#{scope}[#{honeypot}]"
43
47
  else
44
- resource
48
+ honeypot
45
49
  end
46
50
  end
47
51
  end
48
- end
49
-
50
- ActionView::Base.send :include, InvisibleCaptcha::ViewHelpers
52
+ end