acts_as_textcaptcha 2.2.2 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,8 +1,9 @@
1
- *.*.db
1
+ *.*.db
2
2
  *.db
3
3
  doc
4
4
  *.gem
5
5
  Gemfile.lock
6
6
  pkg/*
7
7
  coverage
8
+ coverage.data
8
9
  .rvmrc
data/Gemfile CHANGED
@@ -1,4 +1,2 @@
1
- source "http://rubygems.org"
2
-
3
- # Specify your gem's dependencies in acts_as_textcaptcha.gemspec
1
+ source 'http://rubygems.org'
4
2
  gemspec
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2010 Matthew Hutchinson
1
+ Copyright (c) 2011 Matthew Hutchinson
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -17,4 +17,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
17
  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
18
  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
19
  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc CHANGED
@@ -1,13 +1,11 @@
1
- = ActAsTextcaptcha
1
+ = ActAsTextcaptcha
2
2
 
3
- 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/]).
3
+ 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)
4
4
 
5
- To get started, {grab an API key for your website}[http://textcaptcha.com/api] and follow along with the instructions below.
6
-
7
- The gem can be configured with your very own logic questions (to fall back on if the textcaptcha service is down) or as a replacement for the service. It also makes use of {bcrypt}[http://bcrypt-ruby.rubyforge.org/] encryption when storing the answers in your session (recommended if you're using the default Rails CookieStore)
5
+ 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.* and 3. If you have any issues {please report them here}[https://github.com/matthutchinson/acts_as_textcaptcha/issues].
8
6
 
9
7
  Text CAPTCHA's logic questions are aimed at a child's age of 7, so can be solved easily by all but the most cognitively impaired users. As they involve human logic, such 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
-
8
+
11
9
  == Demo
12
10
 
13
11
  Here's a {fully working demo on heroku}[http://textcaptcha.heroku.com]!
@@ -16,19 +14,19 @@ Here's a {fully working demo on heroku}[http://textcaptcha.heroku.com]!
16
14
 
17
15
  === Rails 3
18
16
 
19
- Just add the following to your Gemfile, then run `bundle install`;
17
+ Just add the following to your Gemfile, then `bundle install`;
20
18
 
21
19
  gem 'acts_as_textcaptcha'
22
-
20
+
23
21
  === Rails 2.3.*
24
22
 
25
- Add the following to your environment.rb file, then install with `gem install acts_as_textcaptcha`;
23
+ Add this to your environment.rb file, then `gem install acts_as_textcaptcha`;
26
24
 
27
25
  config.gem 'acts_as_textcaptcha'
28
26
 
29
27
  === As a plugin
30
28
 
31
- Or install as a plugin with
29
+ Or install as a plugin with;
32
30
 
33
31
  rails plugin install git@github.com:matthutchinson/acts_as_textcaptcha.git
34
32
  OR
@@ -42,104 +40,90 @@ If you do decide to install this as a Rails plugin, you'll have to manually incl
42
40
 
43
41
  == Setting up
44
42
 
45
- Configure which models are to be spam protected; (this is the most basic way to configure the gem, with an api key only)
43
+ *NOTE:* The procedure for configuring your app *changed significantly* from v2.* to v3.* If you are having problems please carefully check the instructions below.
44
+
45
+ First {grab an API key for your website}[http://textcaptcha.com/api] then open Rails console (or any irb session) and generate a new BCrypt salt by typing;
46
+
47
+ require 'bcrypt';BCrypt::Engine.generate_salt
48
+
49
+ Next configure which models are to be spam protected like so;
46
50
 
47
51
  class Comment < ActiveRecord::Base
48
- acts_as_textcaptcha({'api_key' => 'your_textcaptcha_api_key'})
49
- end
52
+ # (this is the most basic way to configure the gem, with an API key and Salt only)
53
+ acts_as_textcaptcha :api_key => 'PASTE_YOUR_TEXTCAPTCHA_API_KEY_HERE',
54
+ :bcrypt_salt => 'PASTE_YOUR_BCRYPT_SALT_HERE'
55
+ end
50
56
 
51
- Next in your controller *new* and *create* actions you'll want to _spamify_ your model and merge in the answers. Like so;
57
+ Next in your controller's *new* action you'll want to generate the logic question for your model, like so;
52
58
 
53
59
  def new
54
60
  @comment = Comment.new
55
- spamify(@comment)
56
- end
57
-
58
- def create
59
- @comment = Comment.new(params[:comment].merge(:possible_answers => session[:possible_answers]))
60
- if @comment.save
61
- ...
62
- else
63
- spamify(@comment)
64
- render :action => 'new'
65
- end
66
- end
67
-
68
- Finally, in your form/view erb do something like the following;
69
-
70
- <%- if @comment.new_record? -%>
71
- <%- if @comment.validate_spam_answer -%>
72
- <%= f.hidden_field :spam_answer, :value => @comment.spam_answer -%>
73
- <%- else -%>
74
- <%= f.label :spam_answer, @comment.spam_question %>
61
+ @comment.textcaptcha
62
+ end
63
+
64
+ 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;
65
+
66
+ <%= textcaptcha_fields(f) do %>
67
+ <div class="field">
68
+ <%= f.label :spam_answer, @comment.spam_question %><br/>
75
69
  <%= f.text_field :spam_answer, :value => '' %>
76
- <%- end -%>
77
- <%- end -%>
70
+ </div>
71
+ <% end %>
78
72
 
79
- == More Configurations
73
+ *NOTE:* For Rails 2.* this step is slightly different (<%= changes to <%);
80
74
 
81
- You can also configure acts_as_textcaptcha in the following ways.
75
+ <% textcaptcha_fields(f) do %>
76
+ <div class="field">
77
+ <%= f.label :spam_answer, @comment.spam_question %><br/>
78
+ <%= f.text_field :spam_answer, :value => '' %>
79
+ </div>
80
+ <% end %>
81
+
82
+ *NOTE:* If you'd rather NOT use this helper and prefer to write all your own view code, see the html produced from the textcaptcha_fields method in the gem source code.
83
+
84
+ == More Configurations
85
+
86
+ 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.
82
87
 
83
88
  === Hash
84
89
 
85
90
  class Comment < ActiveRecord::Base
86
- acts_as_textcaptcha({'api_key' => 'your_textcaptcha_api_key',
87
- 'bcrypt_salt' => '$2a$10$j0bmycH.SVfD1b5mpEGPpe',
88
- 'bcrypt_cost' => '3',
89
- 'questions' => [{'question' => '1+1', 'answers' => '2,two'},
90
- {'question' => 'The green hat is what color?', 'answers' => 'green'}]})
91
- end
92
-
93
- * *api_key* (from Text CAPTCHA)
94
- * *bcrypt_salt* - used to encrypt valid possible answers in your session (recommended if you are using cookie session storage) NOTE: this must be a valid bcrypt salt; for security PLEASE CHANGE THIS, open irb and enter; require 'bcrypt'; BCrypt::Engine.generate_salt
95
- * *bcrypt_cost* - an optional logarithmic var which determines how computational expensive the bcrypt hash is to calculate (a cost of 4 is twice as much work as a cost of 3 - default is 10)
96
- * *questions* - an array of question and answer hashes (see above) A random question from this array will be asked if the textcaptcha web service fails
91
+ acts_as_textcaptcha :api_key => 'YOUR_TEXTCAPTCHA_API_KEY',
92
+ :bcrypt_salt => 'YOUR_BCRYPT_SALT',
93
+ :bcrypt_cost => '3',
94
+ :questions => [{ 'question' => '1+1', 'answers' => '2,two' },
95
+ { 'question' => 'The green hat is what color?', 'answers' => 'green' }]
96
+ end
97
+
98
+ * *api_key* (get from http://textcaptcha.com/api)
99
+ * *bcrypt_salt* - used to encrypt the valid possible answers
100
+ * *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)
101
+ * *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!
97
102
 
98
103
  === YAML config
99
104
 
100
- All the above options can be expressed in a {textcaptcha.yml}[http://github.com/matthutchinson/acts_as_textcaptcha/raw/master/config/textcaptcha.yml] file. Drop this into your RAILS_ROOT/config folder. The gem comes with a handy rake task to copy over a textcaptcha.yml template to your Rails config directory.
105
+ 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.
101
106
 
102
- rake textcaptcha:generate_config
107
+ rake textcaptcha:config
103
108
 
104
109
  *NOTE:* If you are on Rails 2.3.*, you'll have to add the following to your Rakefile to make this task available;
105
110
 
106
111
  `# load textcaptcha rake tasks
107
112
  Dir["#{Gem.searcher.find('acts_as_textcaptcha').full_gem_path}/lib/tasks/**/*.rake"].each { |ext| load ext } if Gem.searcher.find('acts_as_textcaptcha')`
108
113
 
114
+ === Using _without_ the Text CAPTCHA web service
109
115
 
110
- === Using _Without_ the Text CAPTCHA web service
111
-
112
- It *also* is possible to configure to use only your own user defined logic questions. To do so, just ommit the api_key and set at least 1 logic question in your options.
113
-
114
-
115
- == What does the code do?
116
-
117
- The gem contains two parts, a module for your ActiveRecord models, and a tiny helper method (spamify).
118
-
119
- A call to spamify(@model) 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 is assigned to the model, and an array of possible answers are encrypted in the session.
120
-
121
- validate_spam_answer() is called on @model.validate() and checks that the @model.spam_answer matches one of those possible answers in the session. This validation is _only_ carried out on new records, i.e. never on edit, only on create. User's attempted spam answers are not case-sensitive and have trailing/leading white-space removed.
122
-
123
- {BCrypt}[http://bcrypt-ruby.rubyforge.org] encryption is used to securely store the possible answers in your session. You must specify a valid bcrypt-salt and (computational) cost in your options. Without these options possible answers will be MD5-hashed only.
124
-
125
- allowed?() and perform_spam_check?() are utility methods (that can be overridden in your model) They basically act as flags allowing you to control creation of new records, or whether the spam check should be carried out at all.
126
-
127
- If an error occurs in loading or parsing the web service XML, ActsAsTextcaptcha will fall back to choose a random logic question defined in your options. Additionally, if you'd prefer _not_ to use the service at all, you can omit the api_key from your options entirely.
128
-
129
- 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.
130
-
131
- For more details on the code please check the {documentation}[http://rdoc.info/projects/matthutchinson/acts_as_textcaptcha].
116
+ To use only your own logic questions simply ommit the api_key from your config and define at least 1 logic question/answer.
132
117
 
133
118
  == Translations
134
119
 
135
- Recent versions of the gem use standard Rails, I18n translation for the 2 possible error messages generated by acts_as_textcaptcha, with a fall-back to a default English error message. Unfortunately in some versions of Rails 2.3.* the translation fall-back fails. For this case (and when translating) the translations should be provided in your locale file with the following hierarchy (assuming your model is Comment);
120
+ 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);
136
121
 
137
122
  en:
138
123
  activerecord:
139
124
  errors:
140
125
  models:
141
126
  comment:
142
- disabled: "Sorry, adding a %{model} is currently disabled"
143
127
  attributes:
144
128
  spam_answer:
145
129
  incorrect_answer: "is incorrect, try another question instead"
@@ -148,48 +132,62 @@ Recent versions of the gem use standard Rails, I18n translation for the 2 possib
148
132
  comment:
149
133
  spam_answer: "Spam answer"
150
134
 
151
- It should be noted that currently the textcaptcha API service only offers logic questions in English.
135
+ *NOTE:* currently the Text CAPTCHA API web service only offers logic questions in English.
152
136
 
153
137
  == Testing and docs
154
138
 
155
- In development you can run the specs and rdoc tasks like so;
139
+ In development you can run the tests and rdoc tasks like so;
156
140
 
157
- * rake spec (run the rspec2 tests)
141
+ * rake test (all tests)
142
+ * rake test:coverage (all tests with code coverage reporting)
158
143
  * rake rdoc (generate docs)
159
144
 
145
+ == What does the code do?
146
+
147
+ The gem contains two parts, a module for your ActiveRecord models, and a single view helper method.
148
+
149
+ 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.
150
+
151
+ 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.
152
+
153
+ perform_textcaptcha? is a simple utility method (that can be overridden in your model) It allows you to define whether the spam check should be carried out at all. For example, to turn off spam checking for logged in users.
154
+
155
+ 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.
156
+
157
+ 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]
158
+
160
159
  == Requirements
161
160
 
162
161
  What do you need?
163
162
 
164
163
  * {Rails}[http://github.com/rails/rails] >= 2.3.2 (including Rails 3)
165
- * {Ruby}[http://ruby-lang.org/] >= 1.8.7 (also tested with REE and Ruby 1.9.2)
166
- * {bcrypt-ruby}[http://bcrypt-ruby.rubyforge.org/] gem (to securely encrypt the spam answers in your session)
167
- * {Text CAPTCHA api key}[http://textcaptcha.com/register] (_optional_, since you can define your own logic questions, see below for details)
168
- * {Rspec2}[http://rspec.info/] (_optional_ if you want to run the tests, requires >= rspec2)
164
+ * {Ruby}[http://ruby-lang.org/] >= 1.8.7 (also tested with REE and Ruby 1.9.2)
165
+ * {bcrypt-ruby}[http://bcrypt-ruby.rubyforge.org/] gem (to securely encrypt spam answers)
166
+ * {Text CAPTCHA API key}[http://textcaptcha.com/register] (_optional_, since you can define your own logic questions, see below for details)
167
+ * {MiniTest}[https://rubygems.org/gems/minitest] (_optional_ if you want to run the tests, built into Ruby 1.9)
169
168
 
170
169
  == Links
171
-
170
+
172
171
  * {Documentation}[http://rdoc.info/projects/matthutchinson/acts_as_textcaptcha]
173
172
  * {Demo}[http://textcaptcha.heroku.com]
174
173
  * {Code}[http://github.com/matthutchinson/acts_as_textcaptcha]
175
174
  * {Wiki}[http://wiki.github.com/matthutchinson/acts_as_textcaptcha/]
176
175
  * {Bug Tracker}[http://github.com/matthutchinson/acts_as_textcaptcha/issues]
177
- * {Gem}[http://rubygems.org/gems/acts_as_textcaptcha]
178
- * {Code Metrics}[http://getcaliper.com/caliper/project?repo=http%3A%2F%2Frubygems.org%2Fgems%2Facts_as_textcaptcha] (flay, reek, churn etc.)
176
+ * {Gem}[http://rubygems.org/gems/acts_as_textcaptcha]
179
177
 
180
178
  == Who's who?
181
179
 
182
180
  * {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]
183
- * {Text CAPTCHA}[http://textcaptcha.com] api and service by {Rob Tuley}[http://openknot.com/me/] at {Openknot}[http://openknot.com]
181
+ * {Text CAPTCHA}[http://textcaptcha.com] API and service by {Rob Tuley}[http://openknot.com/me/] at {Openknot}[http://openknot.com]
184
182
  * {bcrypt-ruby}[http://bcrypt-ruby.rubyforge.org/] Gem by {Coda Hale}[http://codahale.com]
185
183
 
186
184
  == Todo
187
185
 
188
- * Fix issue with multiple forms open in the same session
186
+ * Achieve 100% test coverage, currently at 93%
189
187
 
190
188
  == Usage
191
189
 
192
- This code is currently used in a number of production websites and apps. It was originally extracted from code developed for {Bugle}[http://bugleblogs.com]
190
+ This gem is used in a number of production websites and apps. It was originally extracted from code developed for {Bugle}[http://bugleblogs.com]
193
191
 
194
192
  * {matthewhutchinson.net}[http://matthewhutchinson.net]
195
193
  * {pmFAQtory.com}[http://pmfaqtory.com]
data/Rakefile CHANGED
@@ -1,19 +1,29 @@
1
1
  gem 'rdoc'
2
2
 
3
- require 'bundler'
4
- require 'rdoc/task'
5
- require 'rspec/core/rake_task'
6
- require 'rcov/rcovtask'
3
+ # default rake
4
+ task :default => [:test]
7
5
 
8
6
  # bundler tasks
7
+ require 'bundler'
9
8
  Bundler::GemHelper.install_tasks
10
9
 
11
- # rspec tasks
12
- RSpec::Core::RakeTask.new(:spec) do |t|
13
- t.rspec_opts = "--color --format=doc"
10
+ # run all tests
11
+ require 'rake/testtask'
12
+ Rake::TestTask.new do |t|
13
+ t.pattern = "test/*_test.rb"
14
+ end
15
+
16
+ # code coverage
17
+ namespace :test do
18
+ desc "Run all tests and generate a code coverage report (simplecov)"
19
+ task :coverage do
20
+ ENV['COVERAGE'] = 'true'
21
+ Rake::Task['test'].execute
22
+ end
14
23
  end
15
24
 
16
25
  # rdoc tasks
26
+ require 'rdoc/task'
17
27
  RDoc::Task.new do |rd|
18
28
  rd.main = "README.rdoc"
19
29
  rd.title = 'acts_as_textcaptcha'
@@ -9,25 +9,24 @@ Gem::Specification.new do |s|
9
9
  s.authors = ["Matthew Hutchinson"]
10
10
  s.email = ["matt@hiddenloop.com"]
11
11
  s.homepage = "http://github.com/matthutchinson/acts_as_textcaptcha"
12
- s.summary = %q{Spam protection for your models via logic questions and the excellent textcaptcha.com api}
13
- s.description = %q{Spam protection for your ActiveRecord models using logic questions and the excellent textcaptcha api. See textcaptcha.com for more details and to get your api key.
14
- The logic questions are aimed at a child's age of 7, so can be solved easily by all but the most cognitively impaired users. As they involve human logic, such questions cannot be solved by a robot.
15
- For more reasons on why logic questions are useful, see here; http://textcaptcha.com/why}
12
+ s.summary = %q{Spam protection for your models via logic questions and the textcaptcha.com API}
13
+ s.description = %q{Simple question/answer based spam protection for your Rails models.
14
+ You can define your own logic questions and/or fetch questions from the textcaptcha.com API.
15
+ The questions involve human logic and are tough for spam bots to crack.
16
+ For more reasons on why logic questions are a good idea visit; http://textcaptcha.com/why}
16
17
 
17
18
  s.extra_rdoc_files = ['README.rdoc', 'LICENSE']
18
19
 
19
20
  s.files = `git ls-files`.split("\n")
20
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
+ s.test_files = `git ls-files -- {test}/*`.split("\n")
22
22
  s.require_paths = ["lib"]
23
23
 
24
- s.add_dependency('bcrypt-ruby', '~> 2.1.2')
24
+ s.add_dependency('bcrypt-ruby', '~> 2.1.4')
25
25
 
26
26
  s.add_development_dependency('rails')
27
- s.add_development_dependency('activerecord')
28
27
  s.add_development_dependency('bundler')
29
- s.add_development_dependency('rspec')
30
- s.add_development_dependency('rcov')
28
+ s.add_development_dependency('simplecov')
31
29
  s.add_development_dependency('rdoc')
32
30
  s.add_development_dependency('sqlite3')
31
+ s.add_development_dependency('fakeweb')
33
32
  end
@@ -1,12 +1,9 @@
1
- development: &non_production_settings
2
- api_key: 8u5ixtdnq9csc84cok0owswgo
3
- # bcrypt can be used to encrypt valid possible answers in your session; http://bcrypt-ruby.rubyforge.org/
4
- # (recommended if you are using cookie session storage)
5
- # NOTE: bcrypt_salt must be a valid bcrypt salt; for security PLEASE CHANGE THIS, open irb and enter; require 'bcrypt'; BCrypt::Engine.generate_salt
6
- bcrypt_salt: $2a$10$j0bmycH.SVfD1b5mpEGPpe
7
- # an optional logarithmic var which determines how computational expensive the hash is to calculate (a cost of 4 is twice as much work as a cost of 3)
8
- bcrypt_cost: 10 # default is 10, must be > 4 (large number means slower encryption)
9
- # if you'd rather NOT use bcrypt, just remove these two settings, bcrypt_salt and bcrypt_cost, valid possible answers will be MD5 digested in your session
1
+ development: &common_settings
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
10
7
  questions:
11
8
  - question: 'Is ice hot or cold?'
12
9
  answers: 'cold'
@@ -30,7 +27,10 @@ development: &non_production_settings
30
27
  answers: '20,twenty'
31
28
 
32
29
  test:
33
- *non_production_settings
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
34
34
 
35
35
  production:
36
- *non_production_settings
36
+ <<: *common_settings
data/init.rb CHANGED
@@ -1,2 +1,2 @@
1
- # init.rb, not included in gemspec files, but required to work as a rails plugin
2
- require './vendor/plugins/acts_as_textcaptcha/lib/acts_as_textcaptcha'
1
+ # this file is required to allow the gem to work as a rails plugin
2
+ require './vendor/plugins/acts_as_textcaptcha/lib/acts_as_textcaptcha'
@@ -1,3 +1,3 @@
1
1
  require 'acts_as_textcaptcha/textcaptcha'
2
2
  require 'acts_as_textcaptcha/textcaptcha_helper'
3
- require "acts_as_textcaptcha/framework/rails#{Rails::VERSION::MAJOR}" if defined?(Rails)
3
+ require "acts_as_textcaptcha/framework/rails#{Rails::VERSION::MAJOR < 3 ? 2 : nil}" if defined?(Rails)
@@ -2,6 +2,6 @@ ActiveSupport.on_load(:active_record) do
2
2
  extend ActsAsTextcaptcha::Textcaptcha
3
3
  end
4
4
 
5
- ActiveSupport.on_load(:action_controller) do
5
+ ActiveSupport.on_load(:action_view) do
6
6
  include ActsAsTextcaptcha::TextcaptchaHelper
7
- end
7
+ end
@@ -1,2 +1,2 @@
1
1
  ActiveRecord::Base.extend ActsAsTextcaptcha::Textcaptcha
2
- ActionController::Base.send(:include, ActsAsTextcaptcha::TextcaptchaHelper)
2
+ ActionView::Base.send(:include, ActsAsTextcaptcha::TextcaptchaHelper)
@@ -1,22 +1,21 @@
1
1
  require 'yaml'
2
2
  require 'net/http'
3
3
  require 'digest/md5'
4
- require 'logger'
5
4
 
6
- # compatiblity with < Rails 3.0.0
5
+ # compatiblity when XmlMini is not available
7
6
  require 'xml' unless defined?(ActiveSupport::XmlMini)
8
7
 
9
8
  # if using as a plugin in /vendor/plugins
10
9
  begin
11
10
  require 'bcrypt'
12
11
  rescue LoadError => e
13
- puts "ActsAsTextcaptcha - please gem install bcrypt-ruby and add `gem \"bcrypt-ruby\"` to your Gemfile (or environment config)"
14
- raise e
12
+ raise "ActsAsTextcaptcha >> please gem install bcrypt-ruby and add `gem \"bcrypt-ruby\"` to your Gemfile (or environment config) #{e}"
15
13
  end
16
14
 
17
15
  module ActsAsTextcaptcha
18
16
 
19
- if Rails.version =~ /^3\./
17
+ # dont use Railtie if Rails < 3
18
+ unless Rails::VERSION::MAJOR < 3
20
19
  class Railtie < ::Rails::Railtie
21
20
  rake_tasks do
22
21
  load "tasks/textcaptcha.rake"
@@ -24,20 +23,26 @@ module ActsAsTextcaptcha
24
23
  end
25
24
  end
26
25
 
26
+
27
27
  module Textcaptcha #:nodoc:
28
28
 
29
+ # raised if an empty response is ever returned from textcaptcha.com web service
30
+ class BadResponse < StandardError; end;
31
+
29
32
  def acts_as_textcaptcha(options = nil)
30
33
  cattr_accessor :textcaptcha_config
31
- attr_accessor :spam_answer, :spam_question, :possible_answers
32
- validate :validate_textcaptcha
34
+ attr_accessor :spam_question, :spam_answers, :spam_answer
35
+ attr_protected :spam_question if respond_to?(:accessible_attributes) && accessible_attributes.nil?
36
+
37
+ validate :validate_textcaptcha
33
38
 
34
39
  if options.is_a?(Hash)
35
40
  self.textcaptcha_config = options.symbolize_keys!
36
41
  else
37
42
  begin
38
43
  self.textcaptcha_config = YAML.load(File.read("#{Rails.root ? Rails.root.to_s : '.'}/config/textcaptcha.yml"))[Rails.env].symbolize_keys!
39
- rescue Errno::ENOENT
40
- raise('./config/textcaptcha.yml not found')
44
+ rescue
45
+ raise 'could not find any textcaptcha options, in config/textcaptcha.yml or model - run rake textcaptcha:config to generate a template config file'
41
46
  end
42
47
  end
43
48
 
@@ -47,82 +52,95 @@ module ActsAsTextcaptcha
47
52
 
48
53
  module InstanceMethods
49
54
 
50
- # override this method to toggle spam checking, default is on (true)
51
- def perform_spam_check?; true end
55
+ # override this method to toggle textcaptcha spam checking, default is on (true)
56
+ def perform_textcaptcha?
57
+ true
58
+ end
52
59
 
53
- # override this method to toggle allowing the model to be created, default is on (true)
54
- # if returning false model.validate will always be false with errors on base
55
- def allowed?; true end
60
+ # generate textcaptcha question and encrypt possible spam_answers
61
+ def textcaptcha
62
+ return if !perform_textcaptcha? || validate_spam_answer
63
+ self.spam_answer = nil
56
64
 
57
- def validate_textcaptcha
58
- # if not new_record? we dont spam check on existing records (ie. no spam check on updates/edits)
59
- if !respond_to?('new_record?') || new_record?
60
- if allowed?
61
- if possible_answers && perform_spam_check? && !validate_spam_answer
62
- errors.add(:spam_answer, :incorrect_answer, :message => "is incorrect, try another question instead")
63
- return false
65
+ if textcaptcha_config
66
+ unless BCrypt::Engine.valid_salt?(textcaptcha_config[:bcrypt_salt])
67
+ 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"
68
+ end
69
+ if textcaptcha_config[:api_key]
70
+ begin
71
+ uri_parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI # URI.parse is deprecated in 1.9.2
72
+ response = Net::HTTP.get(uri_parser.parse("http://textcaptcha.com/api/#{textcaptcha_config[:api_key]}"))
73
+ if response.empty?
74
+ raise Textcaptcha::BadResponse
75
+ else
76
+ parse_textcaptcha_xml(response)
77
+ end
78
+ return
79
+ rescue SocketError, Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, Errno::ECONNREFUSED, Errno::ETIMEDOUT,
80
+ Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, URI::InvalidURIError,
81
+ REXML::ParseException, Textcaptcha::BadResponse
82
+ # rescue from these errors and continue
64
83
  end
84
+ end
85
+
86
+ # fall back to textcaptcha_config questions
87
+ if textcaptcha_config[:questions]
88
+ random_question = textcaptcha_config[:questions][rand(textcaptcha_config[:questions].size)].symbolize_keys!
89
+ self.spam_question = random_question[:question]
90
+ self.spam_answers = encrypt_answers(random_question[:answers].split(',').map!{ |answer| md5_answer(answer) })
65
91
  else
66
- errors.add(:base, :disabled, :message => "Sorry, adding a %{model} is currently disabled")
67
- return false
92
+ self.spam_question = 'ActsAsTextcaptcha >> no API key (or questions) set and/or the textcaptcha service is currently unavailable (answer ok to bypass)'
93
+ self.spam_answers = 'ok'
68
94
  end
69
95
  end
70
- true
71
96
  end
72
97
 
73
- def validate_spam_answer
74
- (spam_answer && possible_answers) ? possible_answers.include?(encrypt_answer(Digest::MD5.hexdigest(spam_answer.strip.downcase.to_s))) : false
75
- end
76
98
 
77
- def encrypt_answers(answers)
78
- answers.map {|answer| encrypt_answer(answer) }
99
+ private
100
+
101
+ def parse_textcaptcha_xml(xml)
102
+ if defined?(ActiveSupport::XmlMini)
103
+ parsed_xml = ActiveSupport::XmlMini.parse(xml)['captcha']
104
+ self.spam_question = parsed_xml['question']['__content__']
105
+ if parsed_xml['answer'].is_a?(Array)
106
+ self.spam_answers = encrypt_answers(parsed_xml['answer'].collect { |a| a['__content__'] })
107
+ else
108
+ self.spam_answers = encrypt_answers([parsed_xml['answer']['__content__']])
109
+ end
110
+ else
111
+ parsed_xml = XML::Parser.string(xml).parse
112
+ self.spam_question = parsed_xml.find('/captcha/question')[0].inner_xml
113
+ self.spam_answers = encrypt_answers(parsed_xml.find('/captcha/answer').map(&:inner_xml))
114
+ end
79
115
  end
80
116
 
81
- def encrypt_answer(answer)
82
- return answer unless(textcaptcha_config[:bcrypt_salt])
83
- BCrypt::Engine.hash_secret(answer, textcaptcha_config[:bcrypt_salt], (textcaptcha_config[:bcrypt_cost].to_i || 10))
117
+ def validate_spam_answer
118
+ (spam_answer && spam_answers) ? spam_answers.split('-').include?(encrypt_answer(md5_answer(spam_answer))) : false
84
119
  end
85
120
 
86
- def generate_spam_question(use_textcaptcha = true)
87
- if use_textcaptcha && textcaptcha_config && textcaptcha_config[:api_key]
88
- begin
89
- resp = Net::HTTP.get(URI.parse('http://textcaptcha.com/api/'+textcaptcha_config[:api_key]))
90
- return [] if resp.empty?
91
-
92
- if defined?(ActiveSupport::XmlMini)
93
- parsed_xml = ActiveSupport::XmlMini.parse(resp)['captcha']
94
- self.spam_question = parsed_xml['question']['__content__']
95
- if parsed_xml['answer'].is_a?(Array)
96
- self.possible_answers = encrypt_answers(parsed_xml['answer'].collect {|a| a['__content__']})
97
- else
98
- self.possible_answers = encrypt_answers([parsed_xml['answer']['__content__']])
99
- end
100
- else
101
- parsed_xml = XML::Parser.string(resp).parse
102
- self.spam_question = parsed_xml.find('/captcha/question')[0].inner_xml
103
- self.possible_answers = encrypt_answers(parsed_xml.find('/captcha/answer').map(&:inner_xml))
104
- end
105
- return possible_answers if spam_question && !possible_answers.empty?
106
- rescue SocketError, Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, Errno::ECONNREFUSED,
107
- Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, URI::InvalidURIError => e
108
- log_textcaptcha("failed to load or parse textcaptcha with key '#{textcaptcha_config[:api_key]}'; #{e}")
121
+ def validate_textcaptcha
122
+ # only spam check on new/unsaved records (ie. no spam check on updates/edits)
123
+ if !respond_to?('new_record?') || new_record?
124
+ if perform_textcaptcha? && !validate_spam_answer
125
+ errors.add(:spam_answer, :incorrect_answer, :message => "is incorrect, try another question instead")
126
+ # regenerate question
127
+ textcaptcha
128
+ return false
109
129
  end
110
130
  end
131
+ true
132
+ end
111
133
 
112
- # fall back to textcaptcha_config questions
113
- if textcaptcha_config && textcaptcha_config[:questions]
114
- log_textcaptcha('falling back to random logic question from config') if textcaptcha_config[:api_key]
115
- random_question = textcaptcha_config[:questions][rand(textcaptcha_config[:questions].size)].symbolize_keys!
116
- self.spam_question = random_question[:question]
117
- self.possible_answers = encrypt_answers(random_question[:answers].split(',').map!{|ans| Digest::MD5.hexdigest(ans)})
118
- end
119
- possible_answers
134
+ def encrypt_answers(answers)
135
+ answers.map { |answer| encrypt_answer(answer) }.join('-')
120
136
  end
121
137
 
122
- private
123
- def log_textcaptcha(message)
124
- logger ||= Logger.new(STDOUT)
125
- logger.info "Textcaptcha >> #{message}"
138
+ def encrypt_answer(answer)
139
+ BCrypt::Engine.hash_secret(answer, textcaptcha_config[:bcrypt_salt], (textcaptcha_config[:bcrypt_cost].to_i || 10))
140
+ end
141
+
142
+ def md5_answer(answer)
143
+ Digest::MD5.hexdigest(answer.to_s.strip.downcase)
126
144
  end
127
145
  end
128
146
  end