text_captcha 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +89 -0
- data/Rakefile +48 -0
- data/lib/text_captcha.rb +33 -0
- data/lib/text_captcha/config.rb +13 -0
- data/lib/text_captcha/validation.rb +124 -0
- data/lib/text_captcha/version.rb +8 -0
- data/locales/en.yml +12 -0
- data/locales/pt.yml +13 -0
- data/test/config_test.rb +0 -0
- data/test/test_helper.rb +15 -0
- data/test/text_captcha_test.rb +93 -0
- metadata +79 -0
data/README.rdoc
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
= TextCaptcha
|
2
|
+
|
3
|
+
A captcha verification for Rails apps, directly integrated into ActiveRecord.
|
4
|
+
It uses questions & answers instead of those ugly images.
|
5
|
+
|
6
|
+
== Installation
|
7
|
+
|
8
|
+
Just run
|
9
|
+
|
10
|
+
gem install text_captcha
|
11
|
+
|
12
|
+
== Usage
|
13
|
+
|
14
|
+
Just add the validation to your model.
|
15
|
+
|
16
|
+
class Comment < ActiveRecord::Base
|
17
|
+
validates_captcha
|
18
|
+
end
|
19
|
+
|
20
|
+
By default the <tt>{:on => :create}</tt> option will be used. You can provide
|
21
|
+
any other option you want.
|
22
|
+
|
23
|
+
class Comment < ActiveRecord::Base
|
24
|
+
validates_captcha :if => :new_record?
|
25
|
+
end
|
26
|
+
|
27
|
+
@comment = Comment.new
|
28
|
+
|
29
|
+
@comment.challenge
|
30
|
+
#=> The colour of a red T-shirt is?
|
31
|
+
|
32
|
+
@comment.challenge_answer = "red"
|
33
|
+
@comment.valid?
|
34
|
+
#=> true
|
35
|
+
|
36
|
+
Note that you can answer the question without worrying about uppercase/lowercase.
|
37
|
+
All strings are normalized before the comparison. So "ReD", "RED" or "red" will
|
38
|
+
pass the validation.
|
39
|
+
|
40
|
+
You can use TextCaptcha with a non-ActiveRecord class. You just need to
|
41
|
+
include the <tt>TextCaptcha::Validation</tt> module.
|
42
|
+
|
43
|
+
class Comment
|
44
|
+
include ActiveModel::Validations
|
45
|
+
include TextCaptcha::Validation
|
46
|
+
|
47
|
+
validates_captcha
|
48
|
+
end
|
49
|
+
|
50
|
+
@comment = Comment.new
|
51
|
+
@comment.valid?
|
52
|
+
#=> false
|
53
|
+
|
54
|
+
@comment.errors[:challenge_answer]
|
55
|
+
#=> ["You need to answer the anti-spam question."]
|
56
|
+
|
57
|
+
You can disable TextCaptcha without having to remove all validations.
|
58
|
+
This is specially good when running tests.
|
59
|
+
|
60
|
+
TextCaptcha.enabled = false
|
61
|
+
|
62
|
+
class Comment
|
63
|
+
include ActiveModel::Validations
|
64
|
+
include TextCaptcha::Validation
|
65
|
+
|
66
|
+
validates_captcha
|
67
|
+
end
|
68
|
+
|
69
|
+
@comment = Comment.new
|
70
|
+
@comment.valid?
|
71
|
+
#=> true
|
72
|
+
|
73
|
+
== To-Do
|
74
|
+
|
75
|
+
* Add more questions.
|
76
|
+
|
77
|
+
== License
|
78
|
+
|
79
|
+
(The MIT License)
|
80
|
+
|
81
|
+
Copyright © 2010:
|
82
|
+
|
83
|
+
* Nando Vieira - http://nandovieira.com.br
|
84
|
+
|
85
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ‘Software’), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
86
|
+
|
87
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
88
|
+
|
89
|
+
THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require "rcov/rcovtask"
|
2
|
+
require "rake/testtask"
|
3
|
+
require "rake/rdoctask"
|
4
|
+
require "lib/text_captcha/version"
|
5
|
+
|
6
|
+
Rcov::RcovTask.new do |t|
|
7
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
8
|
+
t.rcov_opts = ["--sort coverage", "--exclude .gem"]
|
9
|
+
|
10
|
+
t.output_dir = "coverage"
|
11
|
+
t.libs << "test"
|
12
|
+
t.verbose = true
|
13
|
+
end
|
14
|
+
|
15
|
+
Rake::TestTask.new do |t|
|
16
|
+
t.libs << "lib"
|
17
|
+
t.libs << "test"
|
18
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
19
|
+
t.verbose = true
|
20
|
+
t.ruby_opts = %w[-rubygems]
|
21
|
+
end
|
22
|
+
|
23
|
+
Rake::RDocTask.new do |rdoc|
|
24
|
+
rdoc.main = "README.rdoc"
|
25
|
+
rdoc.rdoc_dir = "doc"
|
26
|
+
rdoc.title = "TextCaptcha API"
|
27
|
+
rdoc.options += %w[ --line-numbers --inline-source --charset utf-8 ]
|
28
|
+
rdoc.rdoc_files.include("README.rdoc")
|
29
|
+
rdoc.rdoc_files.include("lib/**/*.rb")
|
30
|
+
end
|
31
|
+
|
32
|
+
begin
|
33
|
+
require "jeweler"
|
34
|
+
|
35
|
+
Jeweler::Tasks.new do |gem|
|
36
|
+
gem.name = "text_captcha"
|
37
|
+
gem.email = "fnando.vieira@gmail.com"
|
38
|
+
gem.homepage = "http://github.com/fnando/text_captcha"
|
39
|
+
gem.authors = ["Nando Vieira"]
|
40
|
+
gem.version = TextCaptcha::Version::STRING
|
41
|
+
gem.summary = "Simple captcha based on plain text questions."
|
42
|
+
gem.files = FileList["README.rdoc", "{lib,test,locales}/**/*", "Rakefile"]
|
43
|
+
end
|
44
|
+
|
45
|
+
Jeweler::GemcutterTasks.new
|
46
|
+
rescue LoadError => e
|
47
|
+
puts "You need to install jeweler to build this gem."
|
48
|
+
end
|
data/lib/text_captcha.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require "active_record"
|
2
|
+
require "active_support/all"
|
3
|
+
|
4
|
+
module TextCaptcha
|
5
|
+
autoload :Validation, "text_captcha/validation"
|
6
|
+
autoload :Version, "text_captcha/version"
|
7
|
+
|
8
|
+
class << self
|
9
|
+
# You can disable TextCaptcha without having to remove all validations.
|
10
|
+
# This is specially good when running tests.
|
11
|
+
#
|
12
|
+
# TextCaptcha.enabled = false
|
13
|
+
#
|
14
|
+
# class Comment
|
15
|
+
# include ActiveModel::Validations
|
16
|
+
# include TextCaptcha::Validation
|
17
|
+
#
|
18
|
+
# validates_captcha
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# @comment = Comment.new
|
22
|
+
# @comment.valid?
|
23
|
+
# #=> true
|
24
|
+
#
|
25
|
+
attr_accessor :enabled
|
26
|
+
end
|
27
|
+
|
28
|
+
# TextCaptcha is enabled by default
|
29
|
+
self.enabled = true
|
30
|
+
|
31
|
+
I18n.load_path += Dir[File.dirname(__FILE__) + "/../locales/**/*.yml"]
|
32
|
+
::ActiveRecord::Base.send :include, TextCaptcha::Validation
|
33
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module TextCaptcha
|
2
|
+
module Validation
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval do
|
5
|
+
include InstanceMethods
|
6
|
+
extend ClassMethods
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
# Validate if answer for a captcha question is correct.
|
12
|
+
# If there's no valid user, an error will be attached to
|
13
|
+
# <tt>:challenge_answer</tt> attribute.
|
14
|
+
#
|
15
|
+
# class Comment < ActiveRecord::Base
|
16
|
+
# validates_captcha
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# By default the <tt>{:on => :create}</tt> options will be used. You can provide
|
20
|
+
# any other option you want.
|
21
|
+
#
|
22
|
+
# class Comment < ActiveRecord::Base
|
23
|
+
# validates_captcha :if => :new_record?
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# @comment = Comment.new
|
27
|
+
#
|
28
|
+
# @comment.challenge
|
29
|
+
# #=> The colour of a red T-shirt is?
|
30
|
+
#
|
31
|
+
# @comment.challenge_answer = "red"
|
32
|
+
# @comment.valid?
|
33
|
+
# #=> true
|
34
|
+
#
|
35
|
+
# Note that you can answer the question without worrying about uppercase/lowercase.
|
36
|
+
# All strings are normalized before the comparison. So "ReD", "RED" or "red" will
|
37
|
+
# pass the validation.
|
38
|
+
#
|
39
|
+
# You can use TextCaptcha with a non-ActiveRecord class. You just need to
|
40
|
+
# include the <tt>TextCaptcha::Validation</tt> module.
|
41
|
+
#
|
42
|
+
# class Comment
|
43
|
+
# include ActiveModel::Validations
|
44
|
+
# include TextCaptcha::Validation
|
45
|
+
#
|
46
|
+
# validates_captcha
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# @comment = Comment.new
|
50
|
+
# @comment.valid?
|
51
|
+
# #=> false
|
52
|
+
#
|
53
|
+
# @comment.errors[:challenge_answer]
|
54
|
+
# #=> ["You need to answer the anti-spam question."]
|
55
|
+
#
|
56
|
+
#
|
57
|
+
def validates_captcha(options = {})
|
58
|
+
attr_accessor :challenge_answer
|
59
|
+
attr_writer :challenge_id
|
60
|
+
|
61
|
+
# Only add default options if class descends from ActiveRecord.
|
62
|
+
# Otherwise, validations won't run because regular classes don't
|
63
|
+
# have save new record status.
|
64
|
+
if self.ancestors.include?(::ActiveRecord::Base)
|
65
|
+
options.reverse_merge!(:on => :create)
|
66
|
+
end
|
67
|
+
|
68
|
+
validate :check_challenge_answer, options
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
module InstanceMethods
|
73
|
+
# Return an array with the current question and its answers.
|
74
|
+
def current_challenge
|
75
|
+
all_challenges[challenge_id.to_i]
|
76
|
+
end
|
77
|
+
|
78
|
+
# Return the question string.
|
79
|
+
def challenge
|
80
|
+
current_challenge.first
|
81
|
+
end
|
82
|
+
|
83
|
+
# Return all accepted answers.
|
84
|
+
def challenge_answers
|
85
|
+
current_challenge.last
|
86
|
+
end
|
87
|
+
|
88
|
+
# Return the question id. If none is assigned it chooses on
|
89
|
+
# randomly.
|
90
|
+
def challenge_id
|
91
|
+
@challenge_id ||= Kernel.rand(all_challenges.count)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Return all questions.
|
95
|
+
def all_challenges
|
96
|
+
@all_challenges ||= I18n.t("text_captcha.challenges")
|
97
|
+
end
|
98
|
+
|
99
|
+
# Detect if the answer is correct. Will also return true if <tt>TextCaptcha.enabled</tt>
|
100
|
+
# is set to <tt>false</tt>.
|
101
|
+
def valid_challenge_answer?
|
102
|
+
return false unless current_challenge
|
103
|
+
return true unless TextCaptcha.enabled
|
104
|
+
|
105
|
+
answers = challenge_answers.collect {|a| to_captcha(a)}
|
106
|
+
!challenge_answer.blank? && answers.include?(to_captcha(challenge_answer))
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
# Check if the answer is correct. Add an error to
|
111
|
+
# <tt><:challenge_answer/tt> attribute otherwise.
|
112
|
+
def check_challenge_answer
|
113
|
+
unless valid_challenge_answer?
|
114
|
+
errors.add(:challenge_answer, I18n.t("text_captcha.error_message"))
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Normalize the strings for comparison.
|
119
|
+
def to_captcha(str)
|
120
|
+
str.to_s.squish.downcase
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
data/locales/en.yml
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
en:
|
2
|
+
text_captcha:
|
3
|
+
error_message: "You need to answer the anti-spam question."
|
4
|
+
challenges:
|
5
|
+
- - "The colour of a red T-shirt is?"
|
6
|
+
- ["red"]
|
7
|
+
- - "What is two plus two?"
|
8
|
+
- ["4", "four"]
|
9
|
+
- - "Green, tracksuit or elephant: the colour is?"
|
10
|
+
- ["green"]
|
11
|
+
- - "The colour of a blue shark is?"
|
12
|
+
- ["blue"]
|
data/locales/pt.yml
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
pt: &PT
|
2
|
+
text_captcha:
|
3
|
+
error_message: "Você precisa responder a pergunta anti-spam corretamente."
|
4
|
+
challenges:
|
5
|
+
- - Quanto é 2 + 2?
|
6
|
+
- ["4", "quatro"]
|
7
|
+
- - Que dia vem depois de sábado?
|
8
|
+
- ["domingo"]
|
9
|
+
- - Quantas horas tem 1 dia?
|
10
|
+
- ["24"]
|
11
|
+
|
12
|
+
pt-BR:
|
13
|
+
<<: *PT
|
data/test/config_test.rb
ADDED
File without changes
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
gem "test-unit"
|
3
|
+
require "test/unit"
|
4
|
+
require "mocha"
|
5
|
+
|
6
|
+
require "text_captcha"
|
7
|
+
|
8
|
+
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
|
9
|
+
ActiveRecord::Schema.define(:version => 0) do
|
10
|
+
create_table :users do
|
11
|
+
end
|
12
|
+
|
13
|
+
create_table :accounts do
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class User < ActiveRecord::Base
|
4
|
+
validates_captcha
|
5
|
+
end
|
6
|
+
|
7
|
+
class Account < ActiveRecord::Base
|
8
|
+
validates_captcha :on => :save
|
9
|
+
end
|
10
|
+
|
11
|
+
class Comment
|
12
|
+
include ActiveModel::Validations
|
13
|
+
include TextCaptcha::Validation
|
14
|
+
|
15
|
+
validates_captcha
|
16
|
+
end
|
17
|
+
|
18
|
+
class TextCaptchaTest < Test::Unit::TestCase
|
19
|
+
def setup
|
20
|
+
I18n.locale = :en
|
21
|
+
TextCaptcha.enabled = true
|
22
|
+
@user = User.new
|
23
|
+
@account = Account.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_enabled_option
|
27
|
+
assert_respond_to TextCaptcha, :enabled
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_skip_captcha_validation_when_is_disabled
|
31
|
+
TextCaptcha.enabled = false
|
32
|
+
assert @user.valid?
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_return_all_challenges
|
36
|
+
assert_equal I18n.t("text_captcha.challenges"), @user.all_challenges
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_randomly_choose_challenge_id
|
40
|
+
assert_kind_of Numeric, @user.challenge_id
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_keep_previously_selected_challenge
|
44
|
+
id = @user.challenge_id
|
45
|
+
assert_equal id, @user.challenge_id
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_select_a_new_challenge
|
49
|
+
Kernel.expects(:rand).twice.with(@user.all_challenges.count).returns(100, 101)
|
50
|
+
|
51
|
+
assert_equal 100, @user.challenge_id
|
52
|
+
@user.challenge_id = nil
|
53
|
+
assert_equal 101, @user.challenge_id
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_return_challenge
|
57
|
+
assert_not_nil @user.challenge
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_require_captcha_answer
|
61
|
+
assert !@user.valid?
|
62
|
+
assert_equal I18n.t("text_captcha.error_message"), @user.errors[:challenge_answer].to_s
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_accept_valid_answer
|
66
|
+
@user.challenge_answer = @user.challenge_answers.first
|
67
|
+
assert @user.valid?
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_respect_on_create_option
|
71
|
+
@user.challenge_answer = @user.challenge_answers.first
|
72
|
+
assert @user.save
|
73
|
+
assert @user.valid?
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_respect_on_save_option
|
77
|
+
@account.challenge_answer = @account.challenge_answers.first
|
78
|
+
assert @account.save
|
79
|
+
|
80
|
+
@account.challenge_answer = nil
|
81
|
+
@account.challenge_id = nil
|
82
|
+
|
83
|
+
assert !@account.valid?
|
84
|
+
assert_equal I18n.t("text_captcha.error_message"), @account.errors[:challenge_answer].to_s
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_extend_any_object
|
88
|
+
@comment = Comment.new
|
89
|
+
assert_not_nil @comment.challenge
|
90
|
+
assert !@comment.valid?
|
91
|
+
assert_equal I18n.t("text_captcha.error_message"), @comment.errors[:challenge_answer].to_s
|
92
|
+
end
|
93
|
+
end
|
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: text_captcha
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 25
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 1
|
10
|
+
version: 0.1.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Nando Vieira
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-07-17 00:00:00 -03:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description:
|
23
|
+
email: fnando.vieira@gmail.com
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files:
|
29
|
+
- README.rdoc
|
30
|
+
files:
|
31
|
+
- README.rdoc
|
32
|
+
- Rakefile
|
33
|
+
- lib/text_captcha.rb
|
34
|
+
- lib/text_captcha/config.rb
|
35
|
+
- lib/text_captcha/validation.rb
|
36
|
+
- lib/text_captcha/version.rb
|
37
|
+
- locales/en.yml
|
38
|
+
- locales/pt.yml
|
39
|
+
- test/config_test.rb
|
40
|
+
- test/test_helper.rb
|
41
|
+
- test/text_captcha_test.rb
|
42
|
+
has_rdoc: true
|
43
|
+
homepage: http://github.com/fnando/text_captcha
|
44
|
+
licenses: []
|
45
|
+
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options:
|
48
|
+
- --charset=UTF-8
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
hash: 3
|
57
|
+
segments:
|
58
|
+
- 0
|
59
|
+
version: "0"
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
hash: 3
|
66
|
+
segments:
|
67
|
+
- 0
|
68
|
+
version: "0"
|
69
|
+
requirements: []
|
70
|
+
|
71
|
+
rubyforge_project:
|
72
|
+
rubygems_version: 1.3.7
|
73
|
+
signing_key:
|
74
|
+
specification_version: 3
|
75
|
+
summary: Simple captcha based on plain text questions.
|
76
|
+
test_files:
|
77
|
+
- test/config_test.rb
|
78
|
+
- test/test_helper.rb
|
79
|
+
- test/text_captcha_test.rb
|