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 +2 -1
- data/Gemfile +1 -3
- data/LICENSE +2 -2
- data/README.rdoc +86 -88
- data/Rakefile +17 -7
- data/acts_as_textcaptcha.gemspec +9 -10
- data/config/textcaptcha.yml +11 -11
- data/init.rb +2 -2
- data/lib/acts_as_textcaptcha.rb +1 -1
- data/lib/acts_as_textcaptcha/framework/{rails3.rb → rails.rb} +2 -2
- data/lib/acts_as_textcaptcha/framework/rails2.rb +1 -1
- data/lib/acts_as_textcaptcha/textcaptcha.rb +85 -67
- data/lib/acts_as_textcaptcha/textcaptcha_helper.rb +20 -4
- data/lib/acts_as_textcaptcha/version.rb +2 -2
- data/lib/tasks/textcaptcha.rake +14 -11
- data/{spec → test}/schema.rb +1 -1
- data/test/test_helper.rb +27 -0
- data/test/test_models.rb +42 -0
- data/test/textcaptcha_helper_test.rb +50 -0
- data/test/textcaptcha_test.rb +188 -0
- metadata +34 -52
- data/spec/acts_as_textcaptcha_spec.rb +0 -227
- data/spec/database.yml +0 -21
- data/spec/spec.opts +0 -2
- data/spec/spec_helper.rb +0 -8
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/LICENSE
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
Copyright (c)
|
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
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
49
|
-
|
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*
|
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
|
-
|
56
|
-
end
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
77
|
-
|
70
|
+
</div>
|
71
|
+
<% end %>
|
78
72
|
|
79
|
-
|
73
|
+
*NOTE:* For Rails 2.* this step is slightly different (<%= changes to <%);
|
80
74
|
|
81
|
-
|
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
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
end
|
92
|
-
|
93
|
-
* *api_key* (from
|
94
|
-
* *bcrypt_salt* - used to encrypt valid possible answers
|
95
|
-
* *bcrypt_cost* - an optional logarithmic
|
96
|
-
* *questions* - an array of question and answer hashes (see above)
|
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
|
-
|
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:
|
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
|
-
|
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
|
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
|
-
|
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
|
139
|
+
In development you can run the tests and rdoc tasks like so;
|
156
140
|
|
157
|
-
* rake
|
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
|
167
|
-
* {Text CAPTCHA
|
168
|
-
* {
|
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]
|
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
|
-
*
|
186
|
+
* Achieve 100% test coverage, currently at 93%
|
189
187
|
|
190
188
|
== Usage
|
191
189
|
|
192
|
-
This
|
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
|
-
|
4
|
-
|
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
|
-
#
|
12
|
-
|
13
|
-
|
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'
|
data/acts_as_textcaptcha.gemspec
CHANGED
@@ -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
|
13
|
-
s.description = %q{
|
14
|
-
|
15
|
-
|
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
|
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.
|
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('
|
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
|
data/config/textcaptcha.yml
CHANGED
@@ -1,12 +1,9 @@
|
|
1
|
-
development: &
|
2
|
-
api_key:
|
3
|
-
#
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
-
*
|
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
|
-
*
|
36
|
+
<<: *common_settings
|
data/init.rb
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
#
|
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'
|
data/lib/acts_as_textcaptcha.rb
CHANGED
@@ -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)
|
@@ -1,2 +1,2 @@
|
|
1
1
|
ActiveRecord::Base.extend ActsAsTextcaptcha::Textcaptcha
|
2
|
-
|
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
|
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
|
-
|
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
|
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 :
|
32
|
-
|
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
|
40
|
-
raise
|
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
|
55
|
+
# override this method to toggle textcaptcha spam checking, default is on (true)
|
56
|
+
def perform_textcaptcha?
|
57
|
+
true
|
58
|
+
end
|
52
59
|
|
53
|
-
#
|
54
|
-
|
55
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
67
|
-
|
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
|
-
|
78
|
-
|
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
|
82
|
-
|
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
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
113
|
-
|
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
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
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
|