invisible_captcha 0.8.0 → 0.9.1
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 +4 -4
- data/.gitignore +5 -1
- data/.rspec +2 -0
- data/.travis.yml +13 -2
- data/Appraisals +11 -0
- data/LICENSE +1 -1
- data/README.md +62 -63
- data/gemfiles/rails_3.2.gemfile +7 -0
- data/gemfiles/rails_4.1.gemfile +7 -0
- data/gemfiles/rails_4.2.gemfile +7 -0
- data/invisible_captcha.gemspec +5 -2
- data/lib/invisible_captcha/controller_ext.rb +35 -2
- data/lib/invisible_captcha/railtie.rb +1 -3
- data/lib/invisible_captcha/version.rb +1 -1
- data/lib/invisible_captcha/view_helpers.rb +5 -2
- data/lib/invisible_captcha.rb +38 -5
- data/spec/controllers_spec.rb +84 -11
- data/spec/dummy/app/controllers/topics_controller.rb +12 -1
- data/spec/dummy/app/models/topic.rb +0 -1
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +29 -0
- data/spec/dummy/config/application.rb +16 -5
- data/spec/dummy/config/boot.rb +2 -4
- data/spec/dummy/config/environment.rb +3 -3
- data/spec/dummy/config/environments/development.rb +23 -12
- data/spec/dummy/config/environments/production.rb +45 -33
- data/spec/dummy/config/environments/test.rb +19 -11
- data/spec/dummy/config/initializers/assets.rb +11 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +6 -5
- data/spec/dummy/config/initializers/mime_types.rb +0 -1
- data/spec/dummy/config/initializers/session_store.rb +1 -6
- data/spec/dummy/config/initializers/wrap_parameters.rb +7 -2
- data/spec/dummy/config/locales/en.yml +20 -2
- data/spec/dummy/config/routes.rb +6 -2
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/invisible_captcha_spec.rb +36 -1
- data/spec/spec_helper.rb +1 -1
- data/spec/view_helpers_spec.rb +22 -4
- metadata +67 -9
- data/lib/invisible_captcha/validator.rb +0 -15
- data/spec/dummy/script/rails +0 -5
- data/spec/validator_spec.rb +0 -12
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 6667c856fdf4fb2ae0147d0d2d3d0531f8fab862
         | 
| 4 | 
            +
              data.tar.gz: 7d2dadae6a9010c3614166343221fc0064fe9bc2
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: a76b88b892d95a1d72fcf5f2bffb2165c0608554feab107ca5a61e25db4a6b2e4eb3349ecba9b7e0c817d2766c23bb6ebd6723fa20717f8b3518ea38583ebab8
         | 
| 7 | 
            +
              data.tar.gz: b15a66cfab3ec429aaafcdcd310e66f178cf17ab1375d58f0f0454b75b00059600295d5b54df404630ba8ceb0d596a84b6f688312bdeb2c04e696d7d211ddf04
         | 
    
        data/.gitignore
    CHANGED
    
    
    
        data/.rspec
    ADDED
    
    
    
        data/.travis.yml
    CHANGED
    
    
    
        data/Appraisals
    ADDED
    
    
    
        data/LICENSE
    CHANGED
    
    
    
        data/README.md
    CHANGED
    
    | @@ -2,11 +2,13 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            [](http://badge.fury.io/rb/invisible_captcha) [](https://travis-ci.org/markets/invisible_captcha)
         | 
| 4 4 |  | 
| 5 | 
            -
            Simple and flexible spam protection solution for Rails applications. | 
| 5 | 
            +
            > Simple and flexible spam protection solution for Rails applications.
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            It is based on the `honeypot` strategy to provide a better user experience. It also provides a time-sensitive form submission.
         | 
| 6 8 |  | 
| 7 9 | 
             
            **Background**
         | 
| 8 10 |  | 
| 9 | 
            -
            The strategy is  | 
| 11 | 
            +
            The strategy is about adding an input field into the form that:
         | 
| 10 12 |  | 
| 11 13 | 
             
            * shouldn't be visible by the real users
         | 
| 12 14 | 
             
            * should be left empty by the real users
         | 
| @@ -14,6 +16,8 @@ The strategy is based on adding an input field into the form that: | |
| 14 16 |  | 
| 15 17 | 
             
            ## Installation
         | 
| 16 18 |  | 
| 19 | 
            +
            Invisible Captcha is tested against Rails `>= 3.2` and Ruby `>= 1.9.3`.
         | 
| 20 | 
            +
             | 
| 17 21 | 
             
            Add this line to you Gemfile:
         | 
| 18 22 |  | 
| 19 23 | 
             
            ```
         | 
| @@ -28,15 +32,13 @@ $ gem install invisible_captcha | |
| 28 32 |  | 
| 29 33 | 
             
            ## Usage
         | 
| 30 34 |  | 
| 31 | 
            -
            There are different ways to implement, at Controller level or Model level:
         | 
| 32 | 
            -
             | 
| 33 | 
            -
            ### Controller style
         | 
| 34 | 
            -
             | 
| 35 35 | 
             
            View code:
         | 
| 36 36 |  | 
| 37 37 | 
             
            ```erb
         | 
| 38 | 
            -
            <%=  | 
| 39 | 
            -
              <%= invisible_captcha %>
         | 
| 38 | 
            +
            <%= form_for(@topic) do |f| %>
         | 
| 39 | 
            +
              <%= f.invisible_captcha :subtitle %>
         | 
| 40 | 
            +
              <!-- or -->
         | 
| 41 | 
            +
              <%= invisible_captcha :subtitle, :topic %>
         | 
| 40 42 | 
             
            <% end %>
         | 
| 41 43 | 
             
            ```
         | 
| 42 44 |  | 
| @@ -44,67 +46,36 @@ Controller code: | |
| 44 46 |  | 
| 45 47 | 
             
            ```ruby
         | 
| 46 48 | 
             
            class TopicsController < ApplicationController
         | 
| 47 | 
            -
              invisible_captcha only: [:create, :update]
         | 
| 49 | 
            +
              invisible_captcha only: [:create, :update], honeypot: :subtitle
         | 
| 48 50 | 
             
            end
         | 
| 49 51 | 
             
            ```
         | 
| 50 52 |  | 
| 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:
         | 
| 53 | 
            +
            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)`). This is a good default, since the bot will surely read the response code and will think that it has achieved to submit the form properly. But, anyway, you are able to define your own callback by passing a method to the `on_spam` option:
         | 
| 52 54 |  | 
| 53 55 | 
             
            ```ruby
         | 
| 54 | 
            -
             | 
| 56 | 
            +
            class TopicsController < ApplicationController
         | 
| 57 | 
            +
              invisible_captcha only: [:create, :update], on_spam: :your_spam_callback_method
         | 
| 55 58 |  | 
| 56 | 
            -
            private
         | 
| 59 | 
            +
              private
         | 
| 57 60 |  | 
| 58 | 
            -
            def  | 
| 59 | 
            -
             | 
| 61 | 
            +
              def your_spam_callback_method
         | 
| 62 | 
            +
                redirect_to root_path
         | 
| 63 | 
            +
              end
         | 
| 60 64 | 
             
            end
         | 
| 61 65 | 
             
            ```
         | 
| 62 66 |  | 
| 63 | 
            -
             | 
| 64 | 
            -
             | 
| 65 | 
            -
            ### Controller style (resource oriented):
         | 
| 66 | 
            -
             | 
| 67 | 
            -
            In your form:
         | 
| 68 | 
            -
             | 
| 69 | 
            -
            ```erb
         | 
| 70 | 
            -
            <%= form_for(@topic) do |f| %>
         | 
| 71 | 
            -
              <%= f.invisible_captcha :subtitle %>
         | 
| 72 | 
            -
              <!-- or -->
         | 
| 73 | 
            -
              <%= invisible_captcha :subtitle, :topic %>
         | 
| 74 | 
            -
            <% end %>
         | 
| 75 | 
            -
            ```
         | 
| 76 | 
            -
             | 
| 77 | 
            -
            In your controller:
         | 
| 78 | 
            -
             | 
| 79 | 
            -
            ```ruby
         | 
| 80 | 
            -
            invisible_captcha only: [:create, :update], honeypot: :subtitle
         | 
| 81 | 
            -
            ```
         | 
| 82 | 
            -
             | 
| 83 | 
            -
            ### Model style
         | 
| 84 | 
            -
             | 
| 85 | 
            -
            View code:
         | 
| 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:
         | 
| 86 68 |  | 
| 87 69 | 
             
            ```erb
         | 
| 88 | 
            -
            <%=  | 
| 89 | 
            -
              <%=  | 
| 70 | 
            +
            <%= form_tag(new_contact_path) do |f| %>
         | 
| 71 | 
            +
              <%= invisible_captcha %>
         | 
| 90 72 | 
             
            <% end %>
         | 
| 91 73 | 
             
            ```
         | 
| 92 74 |  | 
| 93 | 
            -
             | 
| 75 | 
            +
            In you controller:
         | 
| 94 76 |  | 
| 95 | 
            -
            ```ruby
         | 
| 96 | 
            -
            class Topic < ActiveRecord::Base
         | 
| 97 | 
            -
              attr_accessor :subtitle # define a virtual attribute, the honeypot
         | 
| 98 | 
            -
              validates :subtitle, :invisible_captcha => true
         | 
| 99 | 
            -
            end
         | 
| 100 77 | 
             
            ```
         | 
| 101 | 
            -
             | 
| 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
         | 
| 78 | 
            +
            invisible_captcha only: [:new_contact]
         | 
| 108 79 | 
             
            ```
         | 
| 109 80 |  | 
| 110 81 | 
             
            ## Options and customization
         | 
| @@ -115,19 +86,24 @@ This section contains a description of all plugin options and customizations. | |
| 115 86 |  | 
| 116 87 | 
             
            You can customize:
         | 
| 117 88 |  | 
| 118 | 
            -
            * `sentence_for_humans`: text for real users if input field was visible.
         | 
| 119 | 
            -
            * ` | 
| 120 | 
            -
            * `honeypots`: collection of default honeypots, used by the view helper, called with no args, to generate the honeypot field name
         | 
| 89 | 
            +
            * `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.
         | 
| 121 91 | 
             
            * `visual_honeypots`: make honeypots visible, also useful to test/debug your implementation.
         | 
| 92 | 
            +
            * `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 | 
            +
            * `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 | 
            +
            * `timestamp_error_message`: flash error message thrown when form submitted quicker than the `timestamp_threshold` value. It uses I18n by default.
         | 
| 122 95 |  | 
| 123 96 | 
             
            To change these defaults, add the following to an initializer (recommended `config/initializers/invisible_captcha.rb`):
         | 
| 124 97 |  | 
| 125 98 | 
             
            ```ruby
         | 
| 126 99 | 
             
            InvisibleCaptcha.setup do |config|
         | 
| 127 | 
            -
              config. | 
| 128 | 
            -
              config.error_message       = 'You are a robot!'
         | 
| 129 | 
            -
              config.honeypots          += 'fake_resource_title'
         | 
| 100 | 
            +
              config.honeypots           << 'another_fake_attribute'
         | 
| 130 101 | 
             
              config.visual_honeypots    = false
         | 
| 102 | 
            +
              config.timestamp_threshold = 4
         | 
| 103 | 
            +
              config.timestamp_enabled   = true
         | 
| 104 | 
            +
              # Leave these unset if you want to use I18n (see below)
         | 
| 105 | 
            +
              # config.sentence_for_humans     = 'If you are a human, ignore this field'
         | 
| 106 | 
            +
              # config.timestamp_error_message = 'Sorry, that was too quick! Please resubmit.'
         | 
| 131 107 | 
             
            end
         | 
| 132 108 | 
             
            ```
         | 
| 133 109 |  | 
| @@ -140,6 +116,8 @@ The `invisible_captcha` method accepts some options: | |
| 140 116 | 
             
            * `honeypot`: name of honeypot.
         | 
| 141 117 | 
             
            * `scope`: name of scope, ie: 'topic[subtitle]' -> 'topic' is the scope.
         | 
| 142 118 | 
             
            * `on_spam`: custom callback to be called on spam detection.
         | 
| 119 | 
            +
            * `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 | 
            +
            * `timestamp_threshold`: custom threshold per controller/action. Overrides the global value for `InvisibleCaptcha.timestamp_threshold`.
         | 
| 143 121 |  | 
| 144 122 | 
             
            ### View helpers options:
         | 
| 145 123 |  | 
| @@ -153,26 +131,47 @@ Using the view/form helper you can override some defaults for the given instance | |
| 153 131 | 
             
            <% end %>
         | 
| 154 132 | 
             
            ```
         | 
| 155 133 |  | 
| 134 | 
            +
            ### I18n
         | 
| 135 | 
            +
             | 
| 136 | 
            +
            `invisible_captcha` tries to use I18n when it's available by default. The keys it looks for are the following:
         | 
| 137 | 
            +
             | 
| 138 | 
            +
            ```yaml
         | 
| 139 | 
            +
            en:
         | 
| 140 | 
            +
              invisible_captcha:
         | 
| 141 | 
            +
                sentence_for_humans: "If you are human, ignore this field"
         | 
| 142 | 
            +
                timestamp_error_message: "Sorry, that was too quick! Please resubmit."
         | 
| 143 | 
            +
            ```
         | 
| 144 | 
            +
             | 
| 145 | 
            +
            You can override the english ones in your own i18n config files as well as add new ones for other locales.
         | 
| 146 | 
            +
             | 
| 147 | 
            +
            If you intend to use I18n with `invisible_captcha`, you _must not_ set `sentence_for_humans` or `timestamp_error_message` to strings in the setup phase.
         | 
| 148 | 
            +
             | 
| 156 149 | 
             
            ## Contribute
         | 
| 157 150 |  | 
| 158 151 | 
             
            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).
         | 
| 159 152 |  | 
| 160 153 | 
             
            ## Development
         | 
| 161 154 |  | 
| 162 | 
            -
            Clone/fork this repository  | 
| 155 | 
            +
            Clone/fork this repository, start to hack on it and send a pull request.
         | 
| 156 | 
            +
             | 
| 157 | 
            +
            Run the test suite:
         | 
| 158 | 
            +
             | 
| 159 | 
            +
            ```
         | 
| 160 | 
            +
            $ bundle exec rspec
         | 
| 161 | 
            +
            ```
         | 
| 163 162 |  | 
| 164 | 
            -
            Run test suite:
         | 
| 163 | 
            +
            Run the test suite against all supported versions:
         | 
| 165 164 |  | 
| 166 165 | 
             
            ```
         | 
| 167 | 
            -
            $  | 
| 166 | 
            +
            $ bundle exec appraisal rake
         | 
| 168 167 | 
             
            ```
         | 
| 169 168 |  | 
| 170 169 | 
             
            Start a sample Rails app ([source code](spec/dummy)) with `InvisibleCaptcha` integrated:
         | 
| 171 170 |  | 
| 172 171 | 
             
            ```
         | 
| 173 | 
            -
            $ rake web # PORT=4000 (default: 3000)
         | 
| 172 | 
            +
            $ bundle exec rake web # PORT=4000 (default: 3000)
         | 
| 174 173 | 
             
            ```
         | 
| 175 174 |  | 
| 176 175 | 
             
            ## License
         | 
| 177 176 |  | 
| 178 | 
            -
            Copyright (c) 2012- | 
| 177 | 
            +
            Copyright (c) 2012-2016 Marc Anguera. Invisible Captcha is released under the [MIT](LICENSE) License.
         | 
    
        data/invisible_captcha.gemspec
    CHANGED
    
    | @@ -6,8 +6,8 @@ Gem::Specification.new do |spec| | |
| 6 6 | 
             
              spec.version       = InvisibleCaptcha::VERSION
         | 
| 7 7 | 
             
              spec.authors       = ["Marc Anguera Insa"]
         | 
| 8 8 | 
             
              spec.email         = ["srmarc.ai@gmail.com"]
         | 
| 9 | 
            -
              spec.description   =  | 
| 10 | 
            -
              spec.summary       =  | 
| 9 | 
            +
              spec.description   = "Unobtrusive, flexible and simple spam protection for Rails applications using honeypot strategy for better user experience."
         | 
| 10 | 
            +
              spec.summary       = "Simple honeypot protection for RoR apps"
         | 
| 11 11 | 
             
              spec.homepage      = "https://github.com/markets/invisible_captcha"
         | 
| 12 12 | 
             
              spec.license       = "MIT"
         | 
| 13 13 |  | 
| @@ -19,5 +19,8 @@ Gem::Specification.new do |spec| | |
| 19 19 | 
             
              spec.add_dependency 'rails'
         | 
| 20 20 |  | 
| 21 21 | 
             
              spec.add_development_dependency 'rspec-rails', '~> 3.1'
         | 
| 22 | 
            +
              spec.add_development_dependency 'appraisal'
         | 
| 23 | 
            +
              spec.add_development_dependency 'test-unit', '~> 3.0'
         | 
| 24 | 
            +
              spec.add_development_dependency 'mime-types', '< 3.0'
         | 
| 22 25 | 
             
            end
         | 
| 23 26 |  | 
| @@ -9,11 +9,21 @@ module InvisibleCaptcha | |
| 9 9 | 
             
                end
         | 
| 10 10 |  | 
| 11 11 | 
             
                def detect_spam(options = {})
         | 
| 12 | 
            -
                  if  | 
| 12 | 
            +
                  if invisible_captcha_timestamp?(options)
         | 
| 13 | 
            +
                    on_timestamp_spam_action(options)
         | 
| 14 | 
            +
                  elsif invisible_captcha?(options)
         | 
| 13 15 | 
             
                    on_spam_action(options)
         | 
| 14 16 | 
             
                  end
         | 
| 15 17 | 
             
                end
         | 
| 16 18 |  | 
| 19 | 
            +
                def on_timestamp_spam_action(options = {})
         | 
| 20 | 
            +
                  if action = options[:on_timestamp_spam]
         | 
| 21 | 
            +
                    send(action)
         | 
| 22 | 
            +
                  else
         | 
| 23 | 
            +
                    redirect_to :back, flash: { error: InvisibleCaptcha.timestamp_error_message }
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 17 27 | 
             
                def on_spam_action(options = {})
         | 
| 18 28 | 
             
                  if action = options[:on_spam]
         | 
| 19 29 | 
             
                    send(action)
         | 
| @@ -26,6 +36,29 @@ module InvisibleCaptcha | |
| 26 36 | 
             
                  head(200)
         | 
| 27 37 | 
             
                end
         | 
| 28 38 |  | 
| 39 | 
            +
                def invisible_captcha_timestamp?(options = {})
         | 
| 40 | 
            +
                  unless InvisibleCaptcha.timestamp_enabled
         | 
| 41 | 
            +
                    return false
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  timestamp = session[:invisible_captcha_timestamp]
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  # Consider as spam if timestamp not in session, cause that means the form was not fetched at all
         | 
| 47 | 
            +
                  unless timestamp
         | 
| 48 | 
            +
                    logger.warn("Potential spam detected for IP #{request.env['REMOTE_ADDR']}. Invisible Captcha timestamp not found in session.")
         | 
| 49 | 
            +
                    return true
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                  time_to_submit = Time.zone.now - DateTime.iso8601(timestamp)
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  # Consider as spam if form submitted too quickly
         | 
| 55 | 
            +
                  if time_to_submit < (options[:timestamp_threshold] || InvisibleCaptcha.timestamp_threshold)
         | 
| 56 | 
            +
                    logger.warn("Potential spam detected for IP #{request.env['REMOTE_ADDR']}. Invisible Captcha timestamp threshold not reached (took #{time_to_submit.to_i}s).")
         | 
| 57 | 
            +
                    return true
         | 
| 58 | 
            +
                  end
         | 
| 59 | 
            +
                  false
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
             | 
| 29 62 | 
             
                def invisible_captcha?(options = {})
         | 
| 30 63 | 
             
                  honeypot = options[:honeypot]
         | 
| 31 64 | 
             
                  scope    = options[:scope] || controller_name.singularize
         | 
| @@ -45,4 +78,4 @@ module InvisibleCaptcha | |
| 45 78 | 
             
                  false
         | 
| 46 79 | 
             
                end
         | 
| 47 80 | 
             
              end
         | 
| 48 | 
            -
            end
         | 
| 81 | 
            +
            end
         | 
| @@ -10,8 +10,6 @@ module InvisibleCaptcha | |
| 10 10 | 
             
                    include InvisibleCaptcha::ViewHelpers
         | 
| 11 11 | 
             
                    ActionView::Helpers::FormBuilder.send :include, InvisibleCaptcha::FormHelpers
         | 
| 12 12 | 
             
                  end
         | 
| 13 | 
            -
             | 
| 14 | 
            -
                  ActiveModel::Validations::InvisibleCaptchaValidator = InvisibleCaptcha::InvisibleCaptchaValidator
         | 
| 15 13 | 
             
                end
         | 
| 16 14 | 
             
              end
         | 
| 17 | 
            -
            end
         | 
| 15 | 
            +
            end
         | 
| @@ -6,6 +6,9 @@ module InvisibleCaptcha | |
| 6 6 | 
             
                # @param scope [Symbol] name of honeypot scope, ie: topic => input name: topic[subtitle]
         | 
| 7 7 | 
             
                # @return [String] the generated html
         | 
| 8 8 | 
             
                def invisible_captcha(honeypot = nil, scope = nil, options = {})
         | 
| 9 | 
            +
                  if InvisibleCaptcha.timestamp_enabled
         | 
| 10 | 
            +
                    session[:invisible_captcha_timestamp] ||= Time.zone.now.iso8601
         | 
| 11 | 
            +
                  end
         | 
| 9 12 | 
             
                  build_invisible_captcha(honeypot, scope, options)
         | 
| 10 13 | 
             
                end
         | 
| 11 14 |  | 
| @@ -29,7 +32,7 @@ module InvisibleCaptcha | |
| 29 32 | 
             
                end
         | 
| 30 33 |  | 
| 31 34 | 
             
                def generate_html_id(honeypot, scope = nil)
         | 
| 32 | 
            -
                  "#{scope || honeypot}_#{Time.now.to_i}"
         | 
| 35 | 
            +
                  "#{scope || honeypot}_#{Time.zone.now.to_i}"
         | 
| 33 36 | 
             
                end
         | 
| 34 37 |  | 
| 35 38 | 
             
                def visibility_css(container_id, options)
         | 
| @@ -60,4 +63,4 @@ module InvisibleCaptcha | |
| 60 63 | 
             
                  end
         | 
| 61 64 | 
             
                end
         | 
| 62 65 | 
             
              end
         | 
| 63 | 
            -
            end
         | 
| 66 | 
            +
            end
         | 
    
        data/lib/invisible_captcha.rb
    CHANGED
    
    | @@ -2,27 +2,54 @@ require 'invisible_captcha/version' | |
| 2 2 | 
             
            require 'invisible_captcha/controller_ext'
         | 
| 3 3 | 
             
            require 'invisible_captcha/view_helpers'
         | 
| 4 4 | 
             
            require 'invisible_captcha/form_helpers'
         | 
| 5 | 
            -
            require 'invisible_captcha/validator'
         | 
| 6 5 | 
             
            require 'invisible_captcha/railtie'
         | 
| 7 6 |  | 
| 8 7 | 
             
            module InvisibleCaptcha
         | 
| 9 8 | 
             
              class << self
         | 
| 10 | 
            -
                 | 
| 9 | 
            +
                attr_writer :sentence_for_humans,
         | 
| 10 | 
            +
                            :timestamp_error_message,
         | 
| 11 | 
            +
                            :error_message
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                attr_accessor :honeypots,
         | 
| 14 | 
            +
                              :timestamp_threshold,
         | 
| 15 | 
            +
                              :timestamp_enabled,
         | 
| 16 | 
            +
                              :visual_honeypots
         | 
| 11 17 |  | 
| 12 18 | 
             
                def init!
         | 
| 13 19 | 
             
                  # Default sentence for real users if text field was visible
         | 
| 14 | 
            -
                  self.sentence_for_humans = 'If you are a human, ignore this field'
         | 
| 20 | 
            +
                  self.sentence_for_humans = -> { I18n.t('invisible_captcha.sentence_for_humans', default: 'If you are a human, ignore this field') }
         | 
| 15 21 |  | 
| 16 22 | 
             
                  # Default error message for validator
         | 
| 17 | 
            -
                  self.error_message = 'You are a robot!'
         | 
| 23 | 
            +
                  self.error_message = -> { I18n.t('invisible_captcha.error_message', default: 'You are a robot!') }
         | 
| 18 24 |  | 
| 19 25 | 
             
                  # Default fake fields for controller based workflow
         | 
| 20 26 | 
             
                  self.honeypots = ['foo_id', 'bar_id', 'baz_id']
         | 
| 21 27 |  | 
| 28 | 
            +
                  # Fastest time (in seconds) to expect a human to submit the form
         | 
| 29 | 
            +
                  self.timestamp_threshold = 4
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  # Timestamp check enabled by default
         | 
| 32 | 
            +
                  self.timestamp_enabled = true
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  # Default error message for validator when form submitted too quickly
         | 
| 35 | 
            +
                  self.timestamp_error_message = -> { I18n.t('invisible_captcha.timestamp_error_message', default: 'Sorry, that was too quick! Please resubmit.') }
         | 
| 36 | 
            +
             | 
| 22 37 | 
             
                  # Make honeypots visibles
         | 
| 23 38 | 
             
                  self.visual_honeypots = false
         | 
| 24 39 | 
             
                end
         | 
| 25 40 |  | 
| 41 | 
            +
                def sentence_for_humans
         | 
| 42 | 
            +
                  call_lambda_or_return(@sentence_for_humans)
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def error_message
         | 
| 46 | 
            +
                  call_lambda_or_return(@error_message)
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                def timestamp_error_message
         | 
| 50 | 
            +
                  call_lambda_or_return(@timestamp_error_message)
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
             | 
| 26 53 | 
             
                def setup
         | 
| 27 54 | 
             
                  yield(self) if block_given?
         | 
| 28 55 | 
             
                end
         | 
| @@ -30,7 +57,13 @@ module InvisibleCaptcha | |
| 30 57 | 
             
                def get_honeypot
         | 
| 31 58 | 
             
                  honeypots.sample
         | 
| 32 59 | 
             
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                private
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                def call_lambda_or_return(obj)
         | 
| 64 | 
            +
                  obj.respond_to?(:call) ? obj.call : obj
         | 
| 65 | 
            +
                end
         | 
| 33 66 | 
             
              end
         | 
| 34 67 | 
             
            end
         | 
| 35 68 |  | 
| 36 | 
            -
            InvisibleCaptcha.init!
         | 
| 69 | 
            +
            InvisibleCaptcha.init!
         | 
    
        data/spec/controllers_spec.rb
    CHANGED
    
    | @@ -3,23 +3,96 @@ require 'spec_helper' | |
| 3 3 | 
             
            describe InvisibleCaptcha::ControllerExt, type: :controller do
         | 
| 4 4 | 
             
              render_views
         | 
| 5 5 |  | 
| 6 | 
            -
              before  | 
| 6 | 
            +
              before do
         | 
| 7 | 
            +
                @controller = TopicsController.new
         | 
| 8 | 
            +
                InvisibleCaptcha.timestamp_threshold = 1
         | 
| 9 | 
            +
                InvisibleCaptcha.timestamp_enabled = true
         | 
| 10 | 
            +
              end
         | 
| 7 11 |  | 
| 8 | 
            -
               | 
| 9 | 
            -
                 | 
| 12 | 
            +
              context 'without invisible_captcha_timestamp in session' do
         | 
| 13 | 
            +
                it 'fails like if it was submitted too fast' do
         | 
| 14 | 
            +
                  request.env['HTTP_REFERER'] = 'http://test.host/topics'
         | 
| 15 | 
            +
                  post :create, topic: { title: 'foo' }
         | 
| 10 16 |  | 
| 11 | 
            -
             | 
| 17 | 
            +
                  expect(response).to redirect_to :back
         | 
| 18 | 
            +
                  expect(flash[:error]).to eq(InvisibleCaptcha.timestamp_error_message)
         | 
| 19 | 
            +
                end
         | 
| 12 20 | 
             
              end
         | 
| 13 21 |  | 
| 14 | 
            -
               | 
| 15 | 
            -
                 | 
| 22 | 
            +
              context 'without invisible_captcha_timestamp in session and timestamp_enabled=false' do
         | 
| 23 | 
            +
                it 'does not fail like if it was submitted too fast' do
         | 
| 24 | 
            +
                  request.env['HTTP_REFERER'] = 'http://test.host/topics'
         | 
| 25 | 
            +
                  InvisibleCaptcha.timestamp_enabled = false
         | 
| 26 | 
            +
                  post :create, topic: { title: 'foo' }
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  expect(flash[:error]).not_to be_present
         | 
| 29 | 
            +
                  expect(response.body).to be_present
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
              end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
              context 'submission timestamp_threshold' do
         | 
| 34 | 
            +
                before do
         | 
| 35 | 
            +
                  session[:invisible_captcha_timestamp] = Time.zone.now.iso8601
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                it 'fails if submission before timestamp_threshold' do
         | 
| 39 | 
            +
                  request.env['HTTP_REFERER'] = 'http://test.host/topics'
         | 
| 40 | 
            +
                  post :create, topic: { title: 'foo' }
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  expect(response).to redirect_to :back
         | 
| 43 | 
            +
                  expect(flash[:error]).to eq(InvisibleCaptcha.timestamp_error_message)
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                it 'allow custom on_timestamp_spam callback' do
         | 
| 47 | 
            +
                  put :update, id: 1, topic: { title: 'bar' }
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  expect(response).to redirect_to(root_path)
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                context 'successful submissions' do
         | 
| 53 | 
            +
                  it 'passes if submission on or after timestamp_threshold' do
         | 
| 54 | 
            +
                    sleep InvisibleCaptcha.timestamp_threshold
         | 
| 16 55 |  | 
| 17 | 
            -
             | 
| 56 | 
            +
                    post :create, topic: { title: 'foo' }
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                    expect(flash[:error]).not_to be_present
         | 
| 59 | 
            +
                    expect(response.body).to be_present
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                  it 'allow to set a custom timestamp_threshold per action' do
         | 
| 63 | 
            +
                    sleep 2 # custom threshold
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                    post :publish, id: 1
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                    expect(flash[:error]).not_to be_present
         | 
| 68 | 
            +
                    expect(response.body).to be_present
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
                end
         | 
| 18 71 | 
             
              end
         | 
| 19 72 |  | 
| 20 | 
            -
               | 
| 21 | 
            -
                 | 
| 73 | 
            +
              context 'honeypot attribute' do
         | 
| 74 | 
            +
                before do
         | 
| 75 | 
            +
                  session[:invisible_captcha_timestamp] = Time.zone.now.iso8601
         | 
| 76 | 
            +
                  # Wait for valid submission
         | 
| 77 | 
            +
                  sleep InvisibleCaptcha.timestamp_threshold
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                it 'fails with spam' do
         | 
| 81 | 
            +
                  post :create, topic: { subtitle: 'foo' }
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                  expect(response.body).to be_blank
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                it 'passes with no spam' do
         | 
| 87 | 
            +
                  post :create, topic: { title: 'foo' }
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                  expect(response.body).to be_present
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                it 'allow custom on_spam callback' do
         | 
| 93 | 
            +
                  put :update, id: 1, topic: { subtitle: 'foo' }
         | 
| 22 94 |  | 
| 23 | 
            -
             | 
| 95 | 
            +
                  expect(response.body).to redirect_to(new_topic_path)
         | 
| 96 | 
            +
                end
         | 
| 24 97 | 
             
              end
         | 
| 25 | 
            -
            end
         | 
| 98 | 
            +
            end
         | 
| @@ -1,6 +1,9 @@ | |
| 1 1 | 
             
            class TopicsController < ApplicationController
         | 
| 2 2 | 
             
              invisible_captcha honeypot: :subtitle, only: :create
         | 
| 3 | 
            -
              invisible_captcha honeypot: :subtitle, only: :update, | 
| 3 | 
            +
              invisible_captcha honeypot: :subtitle, only: :update,
         | 
| 4 | 
            +
                                          on_spam: :custom_callback,
         | 
| 5 | 
            +
                                          on_timestamp_spam: :custom_timestamp_callback
         | 
| 6 | 
            +
              invisible_captcha honeypot: :subtitle, only: :publish, timestamp_threshold: 2
         | 
| 4 7 |  | 
| 5 8 | 
             
              def new
         | 
| 6 9 | 
             
                @topic = Topic.new
         | 
| @@ -19,9 +22,17 @@ class TopicsController < ApplicationController | |
| 19 22 | 
             
              def update
         | 
| 20 23 | 
             
              end
         | 
| 21 24 |  | 
| 25 | 
            +
              def publish
         | 
| 26 | 
            +
                redirect_to new_topic_path
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
             | 
| 22 29 | 
             
              private
         | 
| 23 30 |  | 
| 24 31 | 
             
              def custom_callback
         | 
| 25 32 | 
             
                redirect_to new_topic_path
         | 
| 26 33 | 
             
              end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
              def custom_timestamp_callback
         | 
| 36 | 
            +
                redirect_to root_path
         | 
| 37 | 
            +
              end
         | 
| 27 38 | 
             
            end
         | 
    
        data/spec/dummy/bin/rake
    ADDED