acts_as_textcaptcha 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +5 -0
- data/LICENSE +20 -0
- data/README.rdoc +37 -0
- data/Rakefile +48 -0
- data/VERSION +1 -0
- data/config/textcaptcha.yml +36 -0
- data/init.rb +1 -0
- data/lib/acts_as_textcaptcha.rb +96 -0
- data/lib/textcaptcha_helper.rb +6 -0
- data/rails/init.rb +9 -0
- data/spec/acts_as_textcaptcha_spec.rb +183 -0
- data/spec/database.yml +21 -0
- data/spec/schema.rb +18 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +11 -0
- metadata +82 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Matthew Hutchinson
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
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.
|
data/README.rdoc
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
= Act as TextCaptcha
|
2
|
+
|
3
|
+
== About
|
4
|
+
|
5
|
+
== Caveats
|
6
|
+
|
7
|
+
Ongoing issues with this project include;
|
8
|
+
|
9
|
+
* needs tests
|
10
|
+
|
11
|
+
== Setup / Using
|
12
|
+
|
13
|
+
=== Requirements
|
14
|
+
|
15
|
+
What do you need?
|
16
|
+
|
17
|
+
=== Installing
|
18
|
+
|
19
|
+
script/plugin install git://github.com/hiddenloop/acts_as_textcaptcha
|
20
|
+
|
21
|
+
== Using
|
22
|
+
|
23
|
+
== What does the code do?
|
24
|
+
|
25
|
+
== Credits
|
26
|
+
|
27
|
+
Who's who?
|
28
|
+
|
29
|
+
* Authored by "Matthew Hutchinson":http://matthewhutchinson.net
|
30
|
+
|
31
|
+
== Get out clause
|
32
|
+
|
33
|
+
Right now this script is provided without warranty, or support from the author.
|
34
|
+
|
35
|
+
== Creative Commons License
|
36
|
+
|
37
|
+
<a rel="license" href="http://creativecommons.org/licenses/by/2.0/uk/"><img alt="Creative Commons License" style="border-width:0" src="http://i.creativecommons.org/l/by/2.0/uk/80x15.png" /></a>
|
data/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/rdoctask'
|
3
|
+
require 'spec/rake/spectask'
|
4
|
+
require 'spec'
|
5
|
+
|
6
|
+
desc 'Default: run spec tests.'
|
7
|
+
task :default => :spec
|
8
|
+
task :test => :spec
|
9
|
+
|
10
|
+
desc "Run all specs"
|
11
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
12
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
13
|
+
t.spec_opts = ['--options', 'spec/spec.opts']
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "Run all specs with RCov"
|
17
|
+
Spec::Rake::SpecTask.new(:rcov) do |t|
|
18
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
19
|
+
t.rcov = true
|
20
|
+
t.rcov_opts = ['--exclude', 'spec']
|
21
|
+
end
|
22
|
+
|
23
|
+
desc 'Generate documentation for the acts_as_textcaptcha plugin.'
|
24
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
25
|
+
rdoc.rdoc_dir = 'doc'
|
26
|
+
rdoc.title = 'acts_as_textcaptcha'
|
27
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
28
|
+
rdoc.rdoc_files.include('README.rdoc', 'LICENSE')
|
29
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
begin
|
34
|
+
require 'jeweler'
|
35
|
+
Jeweler::Tasks.new do |gemspec|
|
36
|
+
gemspec.name = "acts_as_textcaptcha"
|
37
|
+
gemspec.summary = "Spam protection for your models via logic questions and the excellent textcaptcha.com api"
|
38
|
+
gemspec.description = "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.
|
39
|
+
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.
|
40
|
+
For more reasons on why logic questions are useful, see here; http://textcaptcha.com/why"
|
41
|
+
gemspec.email = "matt@hiddenloop.com"
|
42
|
+
gemspec.homepage = "http://github.com/hiddenloop/acts_as_textcaptcha"
|
43
|
+
gemspec.authors = ["Matthew Hutchinson"]
|
44
|
+
end
|
45
|
+
Jeweler::GemcutterTasks.new
|
46
|
+
rescue LoadError
|
47
|
+
puts "Jeweler not available. Install it with: gem install jeweler"
|
48
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.1.0
|
@@ -0,0 +1,36 @@
|
|
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
|
10
|
+
questions:
|
11
|
+
- question: 'Is ice hot or cold?'
|
12
|
+
answers: 'cold'
|
13
|
+
- question: 'what color is an orange?'
|
14
|
+
answers: 'orange'
|
15
|
+
- question: 'what is two plus 3?'
|
16
|
+
answers: '5,five'
|
17
|
+
- question: 'what is 5 times two?'
|
18
|
+
answers: '10,ten'
|
19
|
+
- question: 'How many colors in the list, green, brown, foot and blue?'
|
20
|
+
answers: '3,three'
|
21
|
+
- question: 'what is Georges name?'
|
22
|
+
answers: 'george'
|
23
|
+
- question: '11 minus 1?'
|
24
|
+
answers: '10,ten'
|
25
|
+
- question: 'is boiling water hot or cold?'
|
26
|
+
answers: 'hot'
|
27
|
+
- question: 'what color is my blue shirt today?'
|
28
|
+
answers: 'blue'
|
29
|
+
- question: 'what is 16 plus 4?'
|
30
|
+
answers: '20,twenty'
|
31
|
+
|
32
|
+
test:
|
33
|
+
*non_production_settings
|
34
|
+
|
35
|
+
production:
|
36
|
+
*non_production_settings
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/rails/init'
|
@@ -0,0 +1,96 @@
|
|
1
|
+
begin
|
2
|
+
require 'xml'
|
3
|
+
require 'yaml'
|
4
|
+
require 'net/http'
|
5
|
+
require 'md5'
|
6
|
+
require 'logger'
|
7
|
+
require 'bcrypt'
|
8
|
+
rescue LoadError
|
9
|
+
end
|
10
|
+
|
11
|
+
module ActsAsTextcaptcha #:nodoc:
|
12
|
+
|
13
|
+
def acts_as_textcaptcha(options = nil)
|
14
|
+
cattr_accessor :textcaptcha_config
|
15
|
+
attr_accessor :spam_answer, :spam_question, :possible_answers
|
16
|
+
|
17
|
+
if options.is_a?(Hash)
|
18
|
+
self.textcaptcha_config = options
|
19
|
+
else
|
20
|
+
begin
|
21
|
+
self.textcaptcha_config = YAML.load(File.read("#{(defined? RAILS_ROOT) ? "#{RAILS_ROOT}" : '.'}/config/textcaptcha.yml"))[((defined? RAILS_ENV) ? RAILS_ENV : 'test')]
|
22
|
+
rescue Errno::ENOENT
|
23
|
+
raise('./config/textcaptcha.yml not found')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
include InstanceMethods
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
module InstanceMethods
|
32
|
+
|
33
|
+
def skip_spam_check?; false end
|
34
|
+
|
35
|
+
def allowed?; true end
|
36
|
+
|
37
|
+
def validate
|
38
|
+
if new_record?
|
39
|
+
if allowed?
|
40
|
+
if possible_answers && !skip_spam_check? && !validate_spam_answer
|
41
|
+
errors.add(:spam_answer, 'is incorrect, try another question instead')
|
42
|
+
return false
|
43
|
+
end
|
44
|
+
else
|
45
|
+
errors.add_to_base("Sorry, #{self.class.name.pluralize.downcase} are currently disabled")
|
46
|
+
return false
|
47
|
+
end
|
48
|
+
end
|
49
|
+
true
|
50
|
+
end
|
51
|
+
|
52
|
+
def validate_spam_answer
|
53
|
+
spam_answer ? possible_answers.include?(encrypt_answer(Digest::MD5.hexdigest(spam_answer.strip.downcase.to_s))) : false
|
54
|
+
end
|
55
|
+
|
56
|
+
def encrypt_answers(answers)
|
57
|
+
answers.map {|answer| encrypt_answer(answer) }
|
58
|
+
end
|
59
|
+
|
60
|
+
def encrypt_answer(answer)
|
61
|
+
return answer unless(textcaptcha_config['bcrypt_salt'])
|
62
|
+
BCrypt::Engine.hash_secret(answer, textcaptcha_config['bcrypt_salt'], (textcaptcha_config['bcrypt_cost'] || 10))
|
63
|
+
end
|
64
|
+
|
65
|
+
def generate_spam_question(use_textcaptcha = true)
|
66
|
+
if use_textcaptcha && textcaptcha_config && textcaptcha_config['api_key']
|
67
|
+
begin
|
68
|
+
resp = Net::HTTP.get(URI.parse('http://textcaptcha.com/api/'+textcaptcha_config['api_key']))
|
69
|
+
if !resp.empty? && xml = XML::Parser.string(resp).parse
|
70
|
+
self.spam_question = xml.find('/captcha/question')[0].inner_xml
|
71
|
+
self.possible_answers = encrypt_answers(xml.find('/captcha/answer').map(&:inner_xml))
|
72
|
+
end
|
73
|
+
return possible_answers if spam_question && !possible_answers.empty?
|
74
|
+
rescue SocketError, Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, Errno::ECONNREFUSED,
|
75
|
+
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, URI::InvalidURIError => e
|
76
|
+
log_textcaptcha("failed to load or parse textcaptcha with key '#{textcaptcha_config['api_key']}'; #{e}")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# fall back to textcaptcha_config questions
|
81
|
+
if textcaptcha_config && textcaptcha_config['questions']
|
82
|
+
log_textcaptcha('falling back to random logic question from config') if textcaptcha_config['api_key']
|
83
|
+
random_question = textcaptcha_config['questions'][rand(textcaptcha_config['questions'].size)]
|
84
|
+
self.spam_question = random_question['question']
|
85
|
+
self.possible_answers = encrypt_answers(random_question['answers'].split(',').map!{|ans| Digest::MD5.hexdigest(ans)})
|
86
|
+
end
|
87
|
+
possible_answers
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
def log_textcaptcha(message)
|
92
|
+
logger ||= Logger.new(STDOUT)
|
93
|
+
logger.info "Textcaptcha >> #{message}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
class Widget < ActiveRecord::Base
|
4
|
+
# uses textcaptcha.yml file for configuration
|
5
|
+
acts_as_textcaptcha
|
6
|
+
end
|
7
|
+
|
8
|
+
class Comment < ActiveRecord::Base
|
9
|
+
# inline options with api_key only
|
10
|
+
acts_as_textcaptcha({'api_key' => '8u5ixtdnq9csc84cok0owswgo'})
|
11
|
+
end
|
12
|
+
|
13
|
+
class Review < ActiveRecord::Base
|
14
|
+
# inline options with all possible options
|
15
|
+
acts_as_textcaptcha({'api_key' => '8u5ixtdnq9csc84cok0owswgo',
|
16
|
+
'bcrypt_salt' => '$2a$10$j0bmycH.SVfD1b5mpEGPpe',
|
17
|
+
'bcrypt_cost' => '3',
|
18
|
+
'questions' => [{'question' => '1+1', 'answers' => '2,two'},
|
19
|
+
{'question' => 'The green hat is what color?', 'answers' => 'green'},
|
20
|
+
{'question' => 'Which is bigger: 67, 14 or 6', 'answers' => '67,sixtyseven,sixty seven,sixty-seven'}]})
|
21
|
+
end
|
22
|
+
|
23
|
+
class Note < ActiveRecord::Base
|
24
|
+
# inline options with user defined questions only (no textcaptcha service)
|
25
|
+
acts_as_textcaptcha('questions' => [{'question' => '1+1', 'answers' => '2,two'}])
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
describe 'ActsAsTextcaptcha' do
|
30
|
+
|
31
|
+
before(:each) do
|
32
|
+
@comment = Comment.new
|
33
|
+
@review = Review.new
|
34
|
+
@note = Note.new
|
35
|
+
end
|
36
|
+
|
37
|
+
describe 'validations' do
|
38
|
+
|
39
|
+
before(:each) do
|
40
|
+
@note.generate_spam_question
|
41
|
+
@note.validate.should be_false
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'should validate spam answer with possible answers' do
|
45
|
+
@note.spam_answer = '2'
|
46
|
+
@note.validate.should be_true
|
47
|
+
|
48
|
+
@note.spam_answer = 'two'
|
49
|
+
@note.validate.should be_true
|
50
|
+
|
51
|
+
@note.spam_answer = 'wrong'
|
52
|
+
@note.validate.should be_false
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should strip whitespace and downcase spam answer' do
|
56
|
+
@note.spam_answer = ' tWo '
|
57
|
+
@note.validate.should be_true
|
58
|
+
|
59
|
+
@note.spam_answer = ' 2 '
|
60
|
+
@note.validate.should be_true
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'should always validate if not a new record' do
|
64
|
+
@note.spam_answer = '2'
|
65
|
+
@note.save!
|
66
|
+
@note.generate_spam_question
|
67
|
+
@note.new_record?.should be_false
|
68
|
+
@note.validate.should be_true
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe 'encryption' do
|
73
|
+
|
74
|
+
it 'should encrypt answers' do
|
75
|
+
@review.encrypt_answers(['123', '456']).should eql(['$2a$10$j0bmycH.SVfD1b5mpEGPperNj9IlIHoieNk38UDQFdtREOmRFKgou',
|
76
|
+
'$2a$10$j0bmycH.SVfD1b5mpEGPpeqf88jqdV6gIgeJLQNjUnufIn8dys1fW'])
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'should encrypt a single answer' do
|
80
|
+
@review.encrypt_answer('123').should eql('$2a$10$j0bmycH.SVfD1b5mpEGPperNj9IlIHoieNk38UDQFdtREOmRFKgou')
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'should not encrypt if no bycrpt-salt set' do
|
84
|
+
@comment.encrypt_answer('123').should eql('123')
|
85
|
+
@comment.encrypt_answers(['123', '456']).should eql(['123', '456'])
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe 'flags' do
|
90
|
+
|
91
|
+
it 'should always be valid if skip_spam_check? is true' do
|
92
|
+
@comment.generate_spam_question
|
93
|
+
@comment.validate.should be_false
|
94
|
+
@comment.stub!(:skip_spam_check?).and_return(true)
|
95
|
+
@comment.validate.should be_true
|
96
|
+
@comment.should be_valid
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'should always fail validation if allowed? is false' do
|
100
|
+
@comment.validate.should be_true
|
101
|
+
@comment.stub!(:allowed?).and_return(false)
|
102
|
+
@comment.validate.should be_false
|
103
|
+
@comment.errors.on(:base).should eql('Sorry, comments are currently disabled')
|
104
|
+
@comment.should_not be_valid
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe 'with inline options hash' do
|
109
|
+
|
110
|
+
it 'should be configurable from inline options' do
|
111
|
+
@comment.textcaptcha_config.should eql({'api_key' => '8u5ixtdnq9csc84cok0owswgo'})
|
112
|
+
@review.textcaptcha_config.should eql({'bcrypt_cost'=>'3', 'questions'=>[{'question'=>'1+1', 'answers'=>'2,two'}, {'question'=>'The green hat is what color?', 'answers'=>'green'}, {'question'=>'Which is bigger: 67, 14 or 6', 'answers'=>'67,sixtyseven,sixty seven,sixty-seven'}], 'bcrypt_salt'=>'$2a$10$j0bmycH.SVfD1b5mpEGPpe', 'api_key'=>'8u5ixtdnq9csc84cok0owswgo'})
|
113
|
+
@note.textcaptcha_config .should eql({'questions'=>[{'question'=>'1+1', 'answers'=>'2,two'}]})
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'should generate spam question from textcaptcha service' do
|
117
|
+
@comment.generate_spam_question
|
118
|
+
@comment.spam_question.should_not be_nil
|
119
|
+
@comment.possible_answers.should_not be_nil
|
120
|
+
@comment.possible_answers.should be_an(Array)
|
121
|
+
@comment.validate.should be_false
|
122
|
+
end
|
123
|
+
|
124
|
+
describe 'and textcaptcha unavailable' do
|
125
|
+
|
126
|
+
before(:each) do
|
127
|
+
Net::HTTP.stub(:get).and_raise(SocketError)
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'should fall back to random user defined question when set' do
|
131
|
+
@review.generate_spam_question
|
132
|
+
@review.spam_question.should_not be_nil
|
133
|
+
@review.possible_answers.should_not be_nil
|
134
|
+
@review.possible_answers.should be_an(Array)
|
135
|
+
@review.validate.should be_false
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'should not generate any spam question/answer if no user defined questions set' do
|
139
|
+
@comment.generate_spam_question
|
140
|
+
@comment.spam_question.should be_nil
|
141
|
+
@comment.possible_answers.should be_nil
|
142
|
+
@comment.validate.should be_true
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
describe 'with textcaptcha yaml config file' do
|
148
|
+
|
149
|
+
before(:each) do
|
150
|
+
@widget = Widget.new
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'should be configurable from config/textcaptcha.yml file' do
|
154
|
+
@widget.textcaptcha_config['api_key'].should eql('8u5ixtdnq9csc84cok0owswgo')
|
155
|
+
@widget.textcaptcha_config['bcrypt_salt'].should eql('$2a$10$j0bmycH.SVfD1b5mpEGPpe')
|
156
|
+
@widget.textcaptcha_config['bcrypt_cost'].should eql(10)
|
157
|
+
@widget.textcaptcha_config['questions'].length.should eql(10)
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'should generate spam question' do
|
161
|
+
@widget.generate_spam_question
|
162
|
+
@widget.spam_question.should_not be_nil
|
163
|
+
@widget.possible_answers.should_not be_nil
|
164
|
+
@widget.possible_answers.should be_an(Array)
|
165
|
+
@widget.validate.should be_false
|
166
|
+
end
|
167
|
+
|
168
|
+
describe 'and textcaptcha unavailable' do
|
169
|
+
|
170
|
+
before(:each) do
|
171
|
+
Net::HTTP.stub(:get).and_raise(SocketError)
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'should fall back to a random user defined question' do
|
175
|
+
@widget.generate_spam_question
|
176
|
+
@widget.spam_question.should_not be_nil
|
177
|
+
@widget.possible_answers.should_not be_nil
|
178
|
+
@widget.possible_answers.should be_an(Array)
|
179
|
+
@widget.validate.should be_false
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
data/spec/database.yml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
sqlite:
|
2
|
+
:adapter: sqlite
|
3
|
+
:database: acts_as_textcaptcha.sqlite.db
|
4
|
+
|
5
|
+
sqlite3:
|
6
|
+
:adapter: sqlite3
|
7
|
+
:database: acts_as_textcaptcha.sqlite3.db
|
8
|
+
|
9
|
+
postgresql:
|
10
|
+
:adapter: postgresql
|
11
|
+
:username: postgres
|
12
|
+
:password: postgres
|
13
|
+
:database: acts_as_textcaptcha_test
|
14
|
+
:min_messages: ERROR
|
15
|
+
|
16
|
+
mysql:
|
17
|
+
:adapter: mysql
|
18
|
+
:host: localhost
|
19
|
+
:username: root
|
20
|
+
:password:
|
21
|
+
:database: acts_as_textcaptcha_test
|
data/spec/schema.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
ActiveRecord::Schema.define(:version => 0) do
|
2
|
+
|
3
|
+
create_table :widgets, :force => true do |t|
|
4
|
+
t.string :name
|
5
|
+
end
|
6
|
+
|
7
|
+
create_table :comments, :force => true do |t|
|
8
|
+
t.string :name
|
9
|
+
end
|
10
|
+
|
11
|
+
create_table :reviews, :force => true do |t|
|
12
|
+
t.string :name
|
13
|
+
end
|
14
|
+
|
15
|
+
create_table :notes, :force => true do |t|
|
16
|
+
t.string :name
|
17
|
+
end
|
18
|
+
end
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'spec'
|
3
|
+
require 'active_record'
|
4
|
+
|
5
|
+
require 'lib/acts_as_textcaptcha'
|
6
|
+
require File.dirname(__FILE__) + '/../init.rb'
|
7
|
+
|
8
|
+
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
|
9
|
+
ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite3'])
|
10
|
+
|
11
|
+
load(File.dirname(__FILE__) + "/schema.rb")
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: acts_as_textcaptcha
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 1
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 1.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Matthew Hutchinson
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-04-19 00:00:00 +01:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: |-
|
22
|
+
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.
|
23
|
+
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.
|
24
|
+
For more reasons on why logic questions are useful, see here; http://textcaptcha.com/why
|
25
|
+
email: matt@hiddenloop.com
|
26
|
+
executables: []
|
27
|
+
|
28
|
+
extensions: []
|
29
|
+
|
30
|
+
extra_rdoc_files:
|
31
|
+
- LICENSE
|
32
|
+
- README.rdoc
|
33
|
+
files:
|
34
|
+
- .gitignore
|
35
|
+
- LICENSE
|
36
|
+
- README.rdoc
|
37
|
+
- Rakefile
|
38
|
+
- VERSION
|
39
|
+
- config/textcaptcha.yml
|
40
|
+
- init.rb
|
41
|
+
- lib/acts_as_textcaptcha.rb
|
42
|
+
- lib/textcaptcha_helper.rb
|
43
|
+
- rails/init.rb
|
44
|
+
- spec/acts_as_textcaptcha_spec.rb
|
45
|
+
- spec/database.yml
|
46
|
+
- spec/schema.rb
|
47
|
+
- spec/spec.opts
|
48
|
+
- spec/spec_helper.rb
|
49
|
+
has_rdoc: true
|
50
|
+
homepage: http://github.com/hiddenloop/acts_as_textcaptcha
|
51
|
+
licenses: []
|
52
|
+
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options:
|
55
|
+
- --charset=UTF-8
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
segments:
|
63
|
+
- 0
|
64
|
+
version: "0"
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
segments:
|
70
|
+
- 0
|
71
|
+
version: "0"
|
72
|
+
requirements: []
|
73
|
+
|
74
|
+
rubyforge_project:
|
75
|
+
rubygems_version: 1.3.6
|
76
|
+
signing_key:
|
77
|
+
specification_version: 3
|
78
|
+
summary: Spam protection for your models via logic questions and the excellent textcaptcha.com api
|
79
|
+
test_files:
|
80
|
+
- spec/acts_as_textcaptcha_spec.rb
|
81
|
+
- spec/schema.rb
|
82
|
+
- spec/spec_helper.rb
|