acts_as_textcaptcha 3.0.11 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 98ad0396a537ab06e540f42880d1fcd24993f59c
4
+ data.tar.gz: 57ee900760402ed0cc2e22c609efe2d550b841c4
5
+ SHA512:
6
+ metadata.gz: aa692657b0f23e1fc0410cfa5c8f996c6465148b09fa0f4ab17d47f1f1008b2db5fe3c42f5f2a777d729645589f6e5883d9dd25c7f03a49396e5de79ec20b1a4
7
+ data.tar.gz: 2e53ec3dc94736c4ccaeba1fa5670b6e79d0c74e8e191d25b8bf7e0643493293999253d05aa60b73176b8e44fc20d3f9e7ef901fe934ed9a7f274e83f079978d
@@ -0,0 +1 @@
1
+ service_name: travis-ci
@@ -1,7 +1,10 @@
1
+ before_install:
2
+ - gem update --system 2.1.11
3
+ - gem --version
1
4
  rvm:
2
5
  - 1.8.7
3
6
  - 1.9.2
4
7
  - 1.9.3
5
8
  - 2.0.0
9
+ - 2.1.0
6
10
  - ree
7
- - rbx
@@ -1,16 +1,16 @@
1
- = ActAsTextcaptcha
1
+ == ActAsTextcaptcha
2
2
 
3
- {<img src="https://secure.travis-ci.org/matthutchinson/acts_as_textcaptcha.png" alt="Travis Build Status" align="absmiddle"/>}[https://travis-ci.org/matthutchinson/acts_as_textcaptcha] {<img src="https://codeclimate.com/badge.png" align="absmiddle" alt="Code Climate Stats" />}[https://codeclimate.com/github/matthutchinson/acts_as_textcaptcha]
3
+ {<img src="https://secure.travis-ci.org/matthutchinson/acts_as_textcaptcha.png" alt="Travis Build Status" align="absmiddle"/>}[https://travis-ci.org/matthutchinson/acts_as_textcaptcha] {<img src="https://coveralls.io/repos/matthutchinson/acts_as_textcaptcha/badge.png" alt="Coverage Status" align="absmiddle" />}[https://coveralls.io/r/matthutchinson/acts_as_textcaptcha] {<img src="https://codeclimate.com/badge.png" align="absmiddle" alt="Code Climate Stats" />}[https://codeclimate.com/github/matthutchinson/acts_as_textcaptcha]
4
4
 
5
- ActsAsTextcaptcha provides spam protection for your Rails models using logic questions from the excellent {Text CAPTCHA}[http://textcaptcha.com/] web service (by {Rob Tuley}[http://openknot.com/me/] of {Openknot}[http://openknot.com/]). It is also possible to configure your own questions and answers instead of using this API (or as a fall back in the unlikely event of the web service being unavailable)
5
+ ActsAsTextcaptcha provides spam protection for your Rails models using logic questions from the excellent {Text CAPTCHA}[http://textcaptcha.com/] web service (by {Rob Tuley}[http://openknot.com/me/] of {Openknot}[http://openknot.com/]). It is also possible to configure your own text captcha questions (instead, or as a fall back in the event of any remote API issues).
6
6
 
7
- The gem makes use of {bcrypt}[http://bcrypt-ruby.rubyforge.org/] encryption when comparing a guess with the correct answer(s). This gem is actively maintained, has good test coverage and is compatible with Rails 2.3.*, Rails 3 and 4. If you have any issues {please report them here}[https://github.com/matthutchinson/acts_as_textcaptcha/issues].
7
+ This gem is actively maintained, has good test coverage and is compatible with Rails >= 2.3.8 (including Rails 3 and 4). If you have any issues {please report them here}[https://github.com/matthutchinson/acts_as_textcaptcha/issues/new].
8
8
 
9
- Text CAPTCHA's logic questions are aimed at a child's age of 7, so they can be solved easily by even the most cognitively impaired users. As they involve human logic, questions cannot be solved by a robot. There are both advantages and disadvantages for using logic questions over image based captchas, {find out more at Text CAPTCHA}[http://textcaptcha.com/why].
9
+ Logic questions from the web service are aimed at a child's age of 7, so they can be solved easily by even the most cognitively impaired users. As they involve human logic, questions cannot be solved by a robot. There are both advantages and disadvantages for using logic questions over image based captchas, {find out more at Text CAPTCHA}[http://textcaptcha.com/why].
10
10
 
11
11
  == Demo
12
12
 
13
- Here's a {fully working demo on heroku}[http://textcaptcha.heroku.com]!
13
+ Try a {working demo here}[http://textcaptcha.heroku.com]!
14
14
 
15
15
  == Installing
16
16
 
@@ -26,15 +26,9 @@ Add this to your environment.rb file, then `gem install acts_as_textcaptcha`;
26
26
 
27
27
  config.gem 'acts_as_textcaptcha'
28
28
 
29
- === Or as a plugin
29
+ === Or as a plugin (pre Rails 4)
30
30
 
31
- If you do decide to install this as a Rails plugin, you'll have to manually include the bcrypt-ruby gem in your Gemfile or environment config. Like so;
32
-
33
- gem 'bcrypt-ruby', :require => 'bcrypt'
34
- OR
35
- config.gem 'bcrypt-ruby', :lib => 'bcrypt'
36
-
37
- Then install the plugin with rails or script/rails as follows;
31
+ Install with rails or script/rails as follows;
38
32
 
39
33
  rails plugin install git@github.com:matthutchinson/acts_as_textcaptcha.git
40
34
  OR
@@ -42,93 +36,101 @@ Then install the plugin with rails or script/rails as follows;
42
36
 
43
37
  == Setting up
44
38
 
45
- *NOTE:* The procedure for configuring your app *changed significantly* from v2.* to v3.* If you are having problems please carefully check the instructions below.
39
+ *NOTE:* The steps to configure your app changed with the v4.0.* release. If you are having problems please carefully check the steps or {upgrade instructions}[https://github.com/matthutchinson/acts_as_textcaptcha#upgrading-from-3010] below.
46
40
 
47
- First {grab an API key for your website}[http://textcaptcha.com/api] then open you Rails console (or any irb session) and generate a new BCrypt salt by typing;
48
-
49
- require 'bcrypt';BCrypt::Engine.generate_salt
50
-
51
- Next add the following code to models you'd like to spam protect;
41
+ First {grab an API key for your website}[http://textcaptcha.com/api]. Next add the following code to any models you would like to protect;
52
42
 
53
43
  class Comment < ActiveRecord::Base
54
- # (this is the most basic way to configure the gem, with an API key and Salt only)
55
- acts_as_textcaptcha :api_key => 'PASTE_YOUR_TEXTCAPTCHA_API_KEY_HERE',
56
- :bcrypt_salt => 'PASTE_YOUR_BCRYPT_SALT_HERE'
44
+ # (this is the simplest way to configure the gem, with an API key only)
45
+ acts_as_textcaptcha :api_key => 'PASTE_YOUR_TEXTCAPTCHA_API_KEY_HERE'
57
46
  end
58
47
 
59
- Next in your controller's *new* action you'll want to generate the logic question for your model, like so;
48
+ Next in your controller's *new* action you'll want to generate and assign the logic question for the new record, like so;
60
49
 
61
50
  def new
62
51
  @comment = Comment.new
63
52
  @comment.textcaptcha
64
53
  end
65
54
 
66
- Finally, in your form view add the spam question and answer fields using the textcaptcha_fields helper. You are free to arrange the HTML within this helper as you like;
55
+ Finally, in your form view add the textcaptcha question and answer fields using the textcaptcha_fields helper. Feel free to arrange the HTML within this block as you like;
67
56
 
57
+ # for Rails 2.3.* change <%= to <% on the next line
68
58
  <%= textcaptcha_fields(f) do %>
69
59
  <div class="field">
70
- <%= f.label :spam_answer, @comment.spam_question %><br/>
71
- <%= f.text_field :spam_answer, :value => '' %>
60
+ <%= f.label :textcaptcha_answer, @comment.textcaptcha_question %><br/>
61
+ <%= f.text_field :textcaptcha_answer, :value => '' %>
72
62
  </div>
73
63
  <% end %>
74
64
 
75
- *NOTE:* For Rails 2.* this step is slightly different (<%= changes to <%);
65
+ *NOTE:* If you'd rather NOT use this helper and would prefer to write your own form view code, see the html produced from the textcaptcha_fields method in the {source code}[https://github.com/matthutchinson/acts_as_textcaptcha/blob/master/lib/acts_as_textcaptcha/textcaptcha_helper.rb].
76
66
 
77
- <% textcaptcha_fields(f) do %>
78
- <div class="field">
79
- <%= f.label :spam_answer, @comment.spam_question %><br/>
80
- <%= f.text_field :spam_answer, :value => '' %>
81
- </div>
82
- <% end %>
67
+ === Upgrading from 3.0.10
83
68
 
84
- *NOTE:* If you'd rather NOT use this helper and prefer to write your own view code, see the html produced from the textcaptcha_fields method in the source code.
69
+ Due to a relatively {large hole}[https://github.com/matthutchinson/acts_as_textcaptcha/issues/15] that could allow bots to by-pass textcaptcha's, it was nessecary to re-engineer the gem for the v4.0.* release. Thanks to {Jeffrey Lim}[https://github.com/jf] for spotting this and raising the issue. Upgrading is straightforward;
85
70
 
86
- === Toggling Textcaptcha
71
+ * Rename `spam_question` to `textcaptcha_question`
72
+ * Rename `spam_answer` to `textcaptcha_answer`
73
+ * Any {strong parameter}[http://weblog.rubyonrails.org/2012/3/21/strong-parameters/] calls should include the `textcaptcha_answer` and `textcaptcha_key` fields;
87
74
 
88
- You can toggle textcaptcha on/off for your models by overriding the `perform_textcaptcha?` method. If you overridde it to return false, no questions will be fetched from
89
- the web service and textcaptcha validation is not performed. Additionally the `textcaptcha_fields` form helper will render nothing. This is useful for writing your own
90
- logic to disable spam protection for logged in users etc.
75
+ params.require(:comment).permit(:textcaptcha_answer, :textcaptcha_key, ... )
91
76
 
92
- For flexibility you can also use a `skip_textcaptcha` attribute (protected from mass-assignment) to skip the textcaptcha validation step only. This is helpful when you
93
- need to bypass spam protection after a question has been generated and `perform_textcaptcha?` is true.
94
77
 
95
- == More Configurations
78
+ === Toggling Textcaptcha
79
+
80
+ You can toggle textcaptcha on/off for your models by overriding the `perform_textcaptcha?` method. If it returns false, no questions will be fetched from
81
+ the web service and textcaptcha validation is disabled.
96
82
 
97
- The gem can be configured for models individually (as shown above) or with a config/textcaptcha.yml file for the whole app. A config must have a valid bcrypt_salt, and an api_key and/or an array of questions defined.
83
+ This is useful for writing your own custom logic for toggling spam protection on/off e.g. for logged in users. By default the `perform_textcaptcha?` method {checks if the form object is a new (unsaved) record}[https://github.com/matthutchinson/acts_as_textcaptcha/blob/master/lib/acts_as_textcaptcha/textcaptcha.rb#L54].
98
84
 
99
- === Hash
85
+ So out of the box, spam protection is only enabled for creating new records (not updating). Here is a typical example showing how to overwrite the `perform_textcaptcha?` method, while maintaining the new record check.
100
86
 
101
87
  class Comment < ActiveRecord::Base
102
- acts_as_textcaptcha :api_key => 'YOUR_TEXTCAPTCHA_API_KEY',
103
- :bcrypt_salt => 'YOUR_BCRYPT_SALT',
104
- :bcrypt_cost => '3',
105
- :questions => [{ 'question' => '1+1', 'answers' => '2,two' },
106
- { 'question' => 'The green hat is what color?', 'answers' => 'green' }]
88
+ acts_as_textcaptcha :api_key => 'YOUR_TEXTCAPTCHA_API_KEY'
89
+
90
+ def perform_textcaptcha?
91
+ super && user.admin?
92
+ end
107
93
  end
108
94
 
109
- * *api_key* (get from http://textcaptcha.com/api)
110
- * *bcrypt_salt* - used to encrypt the valid possible answers
111
- * *bcrypt_cost* - an optional logarithmic number which determines how computationally expensive the bcrypt hash will be (a cost of 4 is twice as much work as a cost of 3 - the default is 10)
112
- * *questions* - an array of question and answer hashes (see above) A random question from this array will be asked if the web service fails OR if no API key has been set. Multiple answers to the same question are comma separated (e.g. 2,two) so don't use commas in your answers!
95
+ == More configuration options
96
+
97
+ You can configure acts_as_textcaptcha with the following options;
98
+
99
+ * *api_key* - get a free key from http://textcaptcha.com/api
100
+ * *questions* (_optional, array of question and answer hashes (see below) A random question from this array will be asked if the web service fails OR if no API key has been set. Multiple answers to the same question are comma separated (e.g. 2,two). So do not use commas in your answers!
101
+ * *cache_expiry_minutes* (_optional, minutes for answers to persist in the cache (default 10 minutes), see {below for details}[https://github.com/matthutchinson/acts_as_textcaptcha#what-does-the-code-do]
102
+ * *http_read_timeout* (_optional, Net::HTTP option, seconds to wait for one block to be read from the remote API
103
+ * *http_open_timeout* (_optional, Net::HTTP option, seconds to wait for the connection to open to the remote API
104
+
105
+ For example;
106
+
107
+ class Comment < ActiveRecord::Base
108
+ acts_as_textcaptcha :api_key => 'YOUR_TEXTCAPTCHA_API_KEY',
109
+ :http_read_timeout => 60,
110
+ :http_read_timeout => 10,
111
+ :cache_expiry_minutes => 10,
112
+ :questions => [{ 'question' => '1+1', 'answers' => '2,two' },
113
+ { 'question' => 'The green hat is what color?', 'answers' => 'green' }]
114
+ end
113
115
 
114
116
  === YAML config
115
117
 
116
- The gem comes with a handy rake task to copy over a {textcaptcha.yml}[http://github.com/matthutchinson/acts_as_textcaptcha/raw/master/config/textcaptcha.yml] template to your Rails config directory. It will also generate a random BCrypt Salt when you first run it.
118
+ The gem can be configured for models individually (as shown above) or with a config/textcaptcha.yml file. The config file must have an api_key defined and optional array of questions. Options definied inline in model classes take preference over the configuration in textcaptcha.yml. The gem comes with a handy rake task to copy over a {textcaptcha.yml}[http://github.com/matthutchinson/acts_as_textcaptcha/raw/master/config/textcaptcha.yml] template to your config directory;
117
119
 
118
120
  rake textcaptcha:config
119
121
 
120
- *NOTE:* If you are on Rails 2.3.*, you'll have to add the following to your Rakefile to make this task available;
122
+ *NOTE:* For Rails 2.3.*, you'll have to add the following to your Rakefile to reveal this task;
121
123
 
122
124
  # load textcaptcha rake tasks
123
125
  Dir["#{Gem.searcher.find('acts_as_textcaptcha').full_gem_path}/lib/tasks/**/*.rake"].each { |ext| load ext } if Gem.searcher.find('acts_as_textcaptcha')
124
126
 
125
- === Confguring _without_ the Text CAPTCHA web service
127
+ === Confguring _without_ the TextCAPTCHA web service
126
128
 
127
- To use only your own logic questions simply ommit the api_key from your config and define at least 1 logic question/answer.
129
+ To use only your own logic questions, simply ommit the api_key from the configuration and define at least 1 logic question and answer (see above).
128
130
 
129
131
  == Translations
130
132
 
131
- Recent versions of the gem use standard Rails I18n translation for the error message generated by acts_as_textcaptcha, with a fall-back to English. Unfortunately in some versions of Rails 2.3.* this translation fall-back fails. For this case (and when translating) setup your own locale file with this hierarchy (assuming your model is Comment);
133
+ The gem uses the standard Rails I18n translation approach for error messages, with a fall-back to English. Unfortunately in some versions of Rails 2.3.* this translation fall-back fails. For this case (and when translating) setup your own locale file with this hierarchy (assuming your model is Comment);
132
134
 
133
135
  en:
134
136
  activerecord:
@@ -136,66 +138,74 @@ Recent versions of the gem use standard Rails I18n translation for the error mes
136
138
  models:
137
139
  comment:
138
140
  attributes:
139
- spam_answer:
140
- incorrect_answer: "is incorrect, try another question instead"
141
+ textcaptcha_answer:
142
+ incorrect: "is incorrect, try another question instead"
143
+ expired: "was not submitted quickly enough, try another question instead"
141
144
  activemodel:
142
145
  attributes:
143
146
  comment:
144
- spam_answer: "Spam answer"
147
+ textcaptcha_answer: "Textcaptcha answer"
145
148
 
146
149
  *NOTE:* currently the Text CAPTCHA API web service only offers logic questions in English.
147
150
 
148
151
  == Without Rails or ActiveRecord
149
152
 
150
- Although this gem has been built with Rails in mind, is entirely possible to use it without ActiveRecord, or Rails. For an example, take a look at the {Contact}[https://github.com/matthutchinson/acts_as_textcaptcha/blob/master/test/test_models.rb#L44] model used in the test suite {here}[https://github.com/matthutchinson/acts_as_textcaptcha/blob/master/test/test_models.rb#L44].
153
+ Although this gem has been built with Rails in mind, is entirely possible to use it without ActiveRecord, or Rails. As an example, take a look at the {Contact}[https://github.com/matthutchinson/acts_as_textcaptcha/blob/master/test/test_models.rb#L44] model used in the test suite {here}[https://github.com/matthutchinson/acts_as_textcaptcha/blob/master/test/test_models.rb#L44].
154
+
155
+ Please note that the built-in {TextcaptchaCache}[https://github.com/matthutchinson/acts_as_textcaptcha/blob/master/lib/acts_as_textcaptcha/textcaptcha_cache.rb] class directly wraps the {Rails.cache}[http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html]. An alternative TextcaptchaCache implementation will be nessecary if Rails.cache is not available.
151
156
 
152
157
  == Testing and docs
153
158
 
154
159
  In development you can run the tests and rdoc tasks like so;
155
160
 
156
161
  * rake test (all tests)
157
- * rake test:coverage (all tests with code coverage reporting)
162
+ * rake test:coverage (all tests with code coverage)
158
163
  * rake rdoc (generate docs)
159
164
 
160
165
  == What does the code do?
161
166
 
162
- The gem contains two parts, a module for your ActiveRecord models, and a single view helper method.
167
+ The gem contains two parts, a module for your ActiveRecord models, and a single view helper method. The ActiveRecord module makes use of two futher classes, {TextcaptchaApi}[https://github.com/matthutchinson/acts_as_textcaptcha/blob/master/lib/acts_as_textcaptcha/textcaptcha_api.rb] and {TextcaptchaCache}[https://github.com/matthutchinson/acts_as_textcaptcha/blob/master/lib/acts_as_textcaptcha/textcaptcha_cache.rb].
168
+
169
+ A call to @model.textcaptcha in your controller will query the Text CAPTCHA web service. A GET request is made with Net::HTTP and parsed using the default Rails ActiveSupport::XMLMini backend. A textcaptcha_question and a random cache key is assigned to the record. An array of possible answers is stored in the TextcaptchaCache with this random key. The cached answers have (by default) a 10 minute TTL in your cache. If your forms take more than 10 minutes to be completed you can adjust this value setting the `cache_expiry_minutes` option. Internally TextcaptchaCache wraps {Rails.cache}[http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html] and all cache keys are namespaced.
163
170
 
164
- A call to @model.textcaptcha in your controller will query the Text CAPTCHA web service. A GET request is made with Net::HTTP and parsed using the default Rails ActiveSupport::XMLMini backend (or the standard XML::Parser in older versions of Rails). A spam_question and an encrypted array of possible answers are assigned to the model.
171
+ On saving, validate_textcaptcha is called on @model.validate checking that the @model.textcaptcha_answer matches one of the possible answers (retrieved from the cache). By default, this validation is _only_ carried out on new records, i.e. never on edit, only on create. All attempted answers are case-insensitive and have trailing/leading white-space removed.
165
172
 
166
- validate_textcaptcha is called on @model.validate and checks that the @model.spam_answer matches one of the possible answers (decrypted). This validation is _only_ carried out on new records, i.e. never on edit, only on create. All attempted spam answers are case-insensitive and have trailing/leading white-space removed.
173
+ Regardless of a correct, or incorrect answer the possible answers are cleared from the cache and a new random key is generated and assigned. An incorrect answer will cause a new question to be prompted. After one correct answer, the answer and a mutating key are sent on further form requests, and no question is presented in the form.
167
174
 
168
- If an error or timeout occurs in loading or parsing the API, ActsAsTextcaptcha will fall back to choose a random logic question defined in your options. If the web service fails or no API key is specified AND no alternate questions are configured, the @model will not require spam checking and will pass as valid.
175
+ If an error or timeout occurs during API fetching, ActsAsTextcaptcha will fall back to choose a random logic question defined in your options (see above). If the web service fails or no API key is specified AND no alternate questions are configured, the @model will not require textcaptcha checking and will pass as valid.
169
176
 
170
- For more details on the code please check the {documentation}[http://rdoc.info/projects/matthutchinson/acts_as_textcaptcha]. Tests are written with {MiniTest}[https://rubygems.org/gems/minitest] and code coverage is provided by {SimpleCov}[https://github.com/colszowka/simplecov]
177
+ For more details on the code please check the {documentation}[http://rdoc.info/projects/matthutchinson/acts_as_textcaptcha]. Tests are written with {MiniTest}[https://rubygems.org/gems/minitest]. Pull requests and bug reports are welcome.
171
178
 
172
179
  == Requirements
173
180
 
174
181
  What do you need?
175
182
 
176
- * {Rails}[http://github.com/rails/rails] >= 2.3.2 (including Rails 3 and 4)
177
- * {Ruby}[http://ruby-lang.org/] >= 1.8.7 (also tested with REE, RBX, 1.9.2, 1.9.3, 2.0.0)
178
- * {bcrypt-ruby}[http://bcrypt-ruby.rubyforge.org/] gem (to securely encrypt spam answers)
183
+ * {Rails}[http://github.com/rails/rails] >= 2.3.8 (including Rails 3 and 4)
184
+ * {Rails.cache}[http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html] - some basic cache configuration is nessecary
185
+ * {Ruby}[http://ruby-lang.org/] >= 1.8.7 (also tested with REE, 1.9.2, 1.9.3, 2.0.0, 2.1.0)
179
186
  * {Text CAPTCHA API key}[http://textcaptcha.com/register] (_optional_, since you can define your own logic questions)
180
187
  * {MiniTest}[https://rubygems.org/gems/minitest] (_optional_ if you want to run the tests, built into Ruby 1.9)
181
188
  * {SimpleCov}[https://rubygems.org/gems/simplecov] (_optional_ if you want to run the tests with code coverage reporting)
182
189
 
190
+ *Note*: The built-in {TextcaptchaCache}[https://github.com/matthutchinson/acts_as_textcaptcha/blob/master/lib/acts_as_textcaptcha/textcaptcha_cache.rb] class directly wraps the {Rails.cache}[http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html] object.
191
+
183
192
  == Links
184
193
 
185
- * {Documentation}[http://rdoc.info/projects/matthutchinson/acts_as_textcaptcha]
186
- * {Travis CI Testing}[http://travis-ci.org/#!/matthutchinson/acts_as_textcaptcha]
187
194
  * {Demo}[http://textcaptcha.heroku.com]
188
- * {Code}[http://github.com/matthutchinson/acts_as_textcaptcha]
195
+ * {Travis CI}[http://travis-ci.org/#!/matthutchinson/acts_as_textcaptcha]
196
+ * {Test Coverage}[https://coveralls.io/r/matthutchinson/acts_as_textcaptcha]
197
+ * {Code Climate}[https://codeclimate.com/github/matthutchinson/acts_as_textcaptcha]
198
+ * {RDoc}[http://rdoc.info/projects/matthutchinson/acts_as_textcaptcha]
189
199
  * {Wiki}[http://wiki.github.com/matthutchinson/acts_as_textcaptcha/]
190
- * {Bug Tracker}[http://github.com/matthutchinson/acts_as_textcaptcha/issues]
200
+ * {Issues}[http://github.com/matthutchinson/acts_as_textcaptcha/issues]
201
+ * {Report a bug}[http://github.com/matthutchinson/acts_as_textcaptcha/issues/new]
191
202
  * {Gem}[http://rubygems.org/gems/acts_as_textcaptcha]
203
+ * {GitHub}[http://github.com/matthutchinson/acts_as_textcaptcha]
192
204
 
193
205
  == Who's who?
194
206
 
195
207
  * {ActsAsTextcaptcha}[http://github.com/matthutchinson/acts_as_textcaptcha] and {little robot drawing}[http://www.flickr.com/photos/hiddenloop/4541195635/] by {Matthew Hutchinson}[http://matthewhutchinson.net]
196
208
  * {Text CAPTCHA}[http://textcaptcha.com] API and service by {Rob Tuley}[http://openknot.com/me/] at {Openknot}[http://openknot.com]
197
- * {bcrypt-ruby}[http://bcrypt-ruby.rubyforge.org/] Gem by {Coda Hale}[http://codahale.com]
198
-
199
209
 
200
210
  == Gem Signing
201
211
 
@@ -211,7 +221,6 @@ After doing this, you should be able to install the gem with;
211
221
 
212
222
  == Todo
213
223
 
214
- * Achieve 100% test coverage, currently at 93%
215
224
  * Remove support for Rails 2.3.* in a future gem release (clean code and tests for this)
216
225
 
217
226
  == Usage
@@ -23,8 +23,8 @@ Gem::Specification.new do |s|
23
23
  s.test_files = `git ls-files -- {test}/*`.split("\n")
24
24
  s.require_paths = ["lib"]
25
25
 
26
- s.add_dependency('bcrypt-ruby', '~> 3.0.1')
27
-
26
+ # lock mime-types to pre 2.0 since we want to support 1.8.7/REE in tests
27
+ s.add_development_dependency('mime-types', '~> 1.25.1')
28
28
  s.add_development_dependency('rails')
29
29
  s.add_development_dependency('bundler')
30
30
  s.add_development_dependency('minitest')
@@ -33,4 +33,6 @@ Gem::Specification.new do |s|
33
33
  s.add_development_dependency('sqlite3')
34
34
  s.add_development_dependency('fakeweb')
35
35
  s.add_development_dependency('strong_parameters')
36
+ s.add_development_dependency('coveralls')
37
+ s.add_development_dependency('pry')
36
38
  end
@@ -1,9 +1,9 @@
1
1
  development: &common_settings
2
2
  api_key: PASTE_YOUR_TEXTCAPCHA_API_KEY_HERE # grab one from http://textcaptcha.com/api
3
- bcrypt_salt: RAKE_GENERATED_SALT_PLACEHOLDER # must be a valid bcrypt salt, we've generated this one for you randomly
4
- # generate another with; require 'bcrypt'; BCrypt::Engine.generate_salt
5
- bcrypt_cost: 10 # optional (default is 10) must be > 4 (a larger number means slower, but better encryption)
6
- # see http://bcrypt-ruby.rubyforge.org for more information on bcrypt
3
+ http_read_timeout: 60 # Optional seconds to wait for one block to be read from http://textcaptcha.com/api
4
+ # http_open_timeout: 10 # Optional seconds to wait for the connection to open
5
+ # cache_expiry_minutes: 10 # Optional minutes for captcha answers to persist in the cache (default 10 minutes)
6
+
7
7
  questions:
8
8
  - question: 'Is ice hot or cold?'
9
9
  answers: 'cold'
@@ -28,9 +28,7 @@ development: &common_settings
28
28
 
29
29
  test:
30
30
  <<: *common_settings
31
- api_key: 6eh1co0j12mi2ogcoggkkok4o # for gem test purposes only
32
- bcrypt_salt: $2a$10$qhSefD6gKtmq6M0AzXk4CO # for gem test purposes only
33
- bcrypt_cost: 1
31
+ api_key: 6eh1co0j12mi2ogcoggkkok4o
34
32
 
35
33
  production:
36
34
  <<: *common_settings
@@ -1,17 +1,8 @@
1
1
  require 'yaml'
2
2
  require 'net/http'
3
3
  require 'digest/md5'
4
-
5
- # compatiblity when XmlMini is not available
6
- require 'xml' unless defined?(ActiveSupport::XmlMini)
7
- require 'rexml/document'
8
-
9
- # if using as a plugin in /vendor/plugins
10
- begin
11
- require 'bcrypt'
12
- rescue LoadError => e
13
- raise "ActsAsTextcaptcha >> please gem install bcrypt-ruby and add `gem \"bcrypt-ruby\"` to your Gemfile (or environment config) #{e}"
14
- end
4
+ require 'acts_as_textcaptcha/textcaptcha_cache'
5
+ require 'acts_as_textcaptcha/textcaptcha_api'
15
6
 
16
7
  module ActsAsTextcaptcha
17
8
 
@@ -27,23 +18,19 @@ module ActsAsTextcaptcha
27
18
 
28
19
  module Textcaptcha #:nodoc:
29
20
 
30
- # raised if an empty response is ever returned from textcaptcha.com web service
31
- class BadResponse < StandardError; end;
32
-
33
21
  def acts_as_textcaptcha(options = nil)
34
22
  cattr_accessor :textcaptcha_config
35
- attr_accessor :spam_question, :spam_answers, :spam_answer, :skip_textcaptcha
23
+ attr_accessor :textcaptcha_question, :textcaptcha_answer, :textcaptcha_key
36
24
 
37
25
  if respond_to?(:accessible_attributes)
38
26
  if accessible_attributes.nil? && respond_to?(:attr_protected)
39
- attr_protected :spam_question
40
- attr_protected :skip_textcaptcha
27
+ attr_protected :textcaptcha_question
41
28
  elsif respond_to?(:attr_accessible)
42
- attr_accessible :spam_answer, :spam_answers
29
+ attr_accessible :textcaptcha_answer, :textcaptcha_key
43
30
  end
44
31
  end
45
32
 
46
- validate :validate_textcaptcha
33
+ validate :validate_textcaptcha, :if => :perform_textcaptcha?
47
34
 
48
35
  if options.is_a?(Hash)
49
36
  self.textcaptcha_config = options.symbolize_keys!
@@ -61,51 +48,30 @@ module ActsAsTextcaptcha
61
48
 
62
49
  module InstanceMethods
63
50
 
64
- # override this method to toggle textcaptcha spam checking altogether, default is on (true)
51
+ # override this method to toggle textcaptcha checking
52
+ # by default this will only allow new records to be
53
+ # protected with textcaptchas
65
54
  def perform_textcaptcha?
66
- true
55
+ !respond_to?('new_record?') || new_record?
67
56
  end
68
57
 
69
- # generate textcaptcha question and encrypt possible spam_answers
58
+ # generate and assign textcaptcha
70
59
  def textcaptcha
71
- show_deprecation_warning
72
-
73
- return if !perform_textcaptcha? || validate_spam_answer
74
- self.spam_answer = nil
60
+ if perform_textcaptcha? && textcaptcha_config
61
+ question = answers = nil
75
62
 
76
- if textcaptcha_config
77
- unless BCrypt::Engine.valid_salt?(textcaptcha_config[:bcrypt_salt])
78
- raise BCrypt::Errors::InvalidSalt.new "you must specify a valid BCrypt Salt in your acts_as_textcaptcha options, get a salt from irb/console with\nrequire 'bcrypt';BCrypt::Engine.generate_salt\n\n(Please check Gem README for more details)\n"
79
- end
63
+ # get textcaptcha from api
80
64
  if textcaptcha_config[:api_key]
81
- begin
82
- uri_parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI # URI.parse is deprecated in 1.9.2
83
- response = Net::HTTP.get(uri_parser.parse("http://textcaptcha.com/api/#{textcaptcha_config[:api_key]}"))
84
- if response.empty?
85
- raise Textcaptcha::BadResponse
86
- else
87
- parse_textcaptcha_xml(response)
88
- end
89
- return
90
- rescue SocketError, Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, Errno::ECONNREFUSED, Errno::ETIMEDOUT,
91
- Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, URI::InvalidURIError,
92
- REXML::ParseException, Textcaptcha::BadResponse
93
- # rescue from these errors and continue
94
- end
65
+ question, answers = TextcaptchaApi.fetch(textcaptcha_config[:api_key], textcaptcha_config)
95
66
  end
96
67
 
97
- # fall back to textcaptcha_config questions if they are configured correctly
98
- if textcaptcha_config[:questions]
99
- random_question = textcaptcha_config[:questions][rand(textcaptcha_config[:questions].size)].symbolize_keys!
100
- if random_question[:question] && random_question[:answers]
101
- self.spam_question = random_question[:question]
102
- self.spam_answers = encrypt_answers(random_question[:answers].split(',').map!{ |answer| md5_answer(answer) })
103
- end
68
+ # fall back to config based textcaptcha
69
+ unless question && answers
70
+ question, answers = textcaptcha_config_questions
104
71
  end
105
72
 
106
- unless self.spam_question && self.spam_answers
107
- self.spam_question = 'ActsAsTextcaptcha >> no API key (or questions) set and/or the textcaptcha service is currently unavailable (answer ok to bypass)'
108
- self.spam_answers = 'ok'
73
+ if question && answers
74
+ assign_textcaptcha(question, answers)
109
75
  end
110
76
  end
111
77
  end
@@ -113,58 +79,72 @@ module ActsAsTextcaptcha
113
79
 
114
80
  private
115
81
 
116
- def show_deprecation_warning
117
- warning_message = "
118
- [warning] ** ActsAsTextcaptcha - gem versions prior to v4.0.0 are VUNERABLE TO SPAM CHECK BY-PASSING, please upgrade gem to v4.0.0 (or higher) **
119
- [warning] ** ActsAsTextcaptcha - see here for details: https://github.com/matthutchinson/acts_as_textcaptcha/issues/15 **\n"
120
- Rails.logger.warn warning_message
121
- rescue
122
- puts warning_message
82
+ def textcaptcha_config_questions
83
+ if textcaptcha_config[:questions]
84
+ random_question = textcaptcha_config[:questions][rand(textcaptcha_config[:questions].size)].symbolize_keys!
85
+ [random_question[:question], (random_question[:answers] || '').split(',').map!{ |answer| safe_md5(answer) }]
86
+ end
123
87
  end
124
88
 
125
- def parse_textcaptcha_xml(xml)
126
- if defined?(ActiveSupport::XmlMini)
127
- parsed_xml = ActiveSupport::XmlMini.parse(xml)['captcha']
128
- self.spam_question = parsed_xml['question']['__content__']
129
- if parsed_xml['answer'].is_a?(Array)
130
- self.spam_answers = encrypt_answers(parsed_xml['answer'].collect { |a| a['__content__'] })
89
+
90
+ # check textcaptcha, if incorrect, regenerate a new textcaptcha
91
+ def validate_textcaptcha
92
+ valid_answers = textcaptcha_cache.read(textcaptcha_key) || []
93
+ reset_textcaptcha
94
+ if valid_answers.include?(safe_md5(textcaptcha_answer))
95
+ # answer was valid, mutate the key again
96
+ self.textcaptcha_key = textcaptcha_random_key
97
+ textcaptcha_cache.write(textcaptcha_key, valid_answers, textcaptcha_cache_options)
98
+ true
99
+ else
100
+ if valid_answers.empty?
101
+ # took too long to answer
102
+ errors.add(:textcaptcha_answer, :expired, :message => 'was not submitted quickly enough, try another question instead')
131
103
  else
132
- self.spam_answers = encrypt_answers([parsed_xml['answer']['__content__']])
104
+ # incorrect answer
105
+ errors.add(:textcaptcha_answer, :incorrect, :message => 'is incorrect, try another question instead')
133
106
  end
134
- else
135
- parsed_xml = XML::Parser.string(xml).parse
136
- self.spam_question = parsed_xml.find('/captcha/question')[0].inner_xml
137
- self.spam_answers = encrypt_answers(parsed_xml.find('/captcha/answer').map(&:inner_xml))
107
+ textcaptcha
108
+ false
138
109
  end
139
110
  end
140
111
 
141
- def validate_spam_answer
142
- (spam_answer && spam_answers) ? spam_answers.split('-').include?(encrypt_answer(md5_answer(spam_answer))) : false
112
+ def reset_textcaptcha
113
+ if textcaptcha_key
114
+ textcaptcha_cache.delete(textcaptcha_key)
115
+ self.textcaptcha_key = nil
116
+ end
143
117
  end
144
118
 
145
- def validate_textcaptcha
146
- # only spam check on new/unsaved records (ie. no spam check on updates/edits)
147
- if !respond_to?('new_record?') || new_record?
148
- if !skip_textcaptcha && perform_textcaptcha? && !validate_spam_answer
149
- errors.add(:spam_answer, :incorrect_answer, :message => "is incorrect, try another question instead")
150
- # regenerate question
151
- textcaptcha
152
- return false
153
- end
154
- end
155
- true
119
+ def assign_textcaptcha(question, answers)
120
+ self.textcaptcha_question = question
121
+ self.textcaptcha_key = textcaptcha_random_key
122
+ textcaptcha_cache.write(textcaptcha_key, answers, textcaptcha_cache_options)
123
+ end
124
+
125
+ # strip whitespace pass through mb_chars (a multibyte
126
+ # safe proxy for string methods) then downcase
127
+ def safe_md5(answer)
128
+ Digest::MD5.hexdigest(answer.to_s.strip.mb_chars.downcase)
156
129
  end
157
130
 
158
- def encrypt_answers(answers)
159
- answers.map { |answer| encrypt_answer(answer) }.join('-')
131
+ # a random cache key, time based and random
132
+ def textcaptcha_random_key
133
+ safe_md5(Time.now.to_i + rand(1_000_000))
160
134
  end
161
135
 
162
- def encrypt_answer(answer)
163
- BCrypt::Engine.hash_secret(answer, textcaptcha_config[:bcrypt_salt], (textcaptcha_config[:bcrypt_cost].to_i || 10))
136
+ def textcaptcha_cache_options
137
+ if textcaptcha_config[:cache_expiry_minutes]
138
+ { :expires_in => textcaptcha_config[:cache_expiry_minutes].to_f.minutes }
139
+ else
140
+ {}
141
+ end
164
142
  end
165
143
 
166
- def md5_answer(answer)
167
- Digest::MD5.hexdigest(answer.to_s.strip.mb_chars.downcase)
144
+ # cache is used to persist textcaptcha questions and answers
145
+ # between requests
146
+ def textcaptcha_cache
147
+ @@textcaptcha_cache ||= TextcaptchaCache.new
168
148
  end
169
149
  end
170
150
  end