Commit 62bac6f22031a0166608bdd83cc0e1f98d553649
1 parent
7410bc19
Exists in
master
and in
22 other branches
Adding plugin
(ActionItem1732)
Showing
10 changed files
with
266 additions
and
0 deletions
Show diff stats
@@ -0,0 +1,20 @@ | @@ -0,0 +1,20 @@ | ||
1 | +Copyright (c) 2007 [name of plugin creator] | ||
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. |
@@ -0,0 +1,45 @@ | @@ -0,0 +1,45 @@ | ||
1 | +MathCaptcha | ||
2 | +=========== | ||
3 | + | ||
4 | +TODO: Create form helpers | ||
5 | + | ||
6 | +Validates your Model to be saved by a human (or very clever script) by asking | ||
7 | +the user to solve a very basic mathematical equation. | ||
8 | + | ||
9 | +It does not use the session to store the secret, but uses AES from EzCrypto. | ||
10 | + | ||
11 | + | ||
12 | +Requirements | ||
13 | +============ | ||
14 | + * gem ezcrypto | ||
15 | + * users who can calculate | ||
16 | + | ||
17 | +Usage | ||
18 | +===== | ||
19 | + | ||
20 | +In your Model < ActiveRecord::Base | ||
21 | + | ||
22 | + has_captcha | ||
23 | + | ||
24 | + | ||
25 | +In your form view: | ||
26 | + | ||
27 | + <%= @model.captcha.task %> = ? | ||
28 | + <%= form.text_field :captcha_solution %> | ||
29 | + <%= form.hidden_field :captcha_secret %> | ||
30 | + | ||
31 | + | ||
32 | +If you want to skip the validation (in tests/specs), you can | ||
33 | + | ||
34 | + Model.skip_captcha! # or | ||
35 | + @model.skip_captcha! | ||
36 | + | ||
37 | +and turn that off by | ||
38 | + | ||
39 | + Model.dont_skip_captcha! # or | ||
40 | + @model.dont_skip_captcha! | ||
41 | + | ||
42 | + | ||
43 | + | ||
44 | +Copyright (c) 2007 Pat Nakajima, released under the MIT license | ||
45 | +Copyright (c) 2009 Niklas Hofer, released under the MIT license |
@@ -0,0 +1,22 @@ | @@ -0,0 +1,22 @@ | ||
1 | +require 'rake' | ||
2 | +require 'rake/testtask' | ||
3 | +require 'rake/rdoctask' | ||
4 | + | ||
5 | +desc 'Default: run unit tests.' | ||
6 | +task :default => :test | ||
7 | + | ||
8 | +desc 'Test the math_captcha plugin.' | ||
9 | +Rake::TestTask.new(:test) do |t| | ||
10 | + t.libs << 'lib' | ||
11 | + t.pattern = 'test/**/*_test.rb' | ||
12 | + t.verbose = true | ||
13 | +end | ||
14 | + | ||
15 | +desc 'Generate documentation for the math_captcha plugin.' | ||
16 | +Rake::RDocTask.new(:rdoc) do |rdoc| | ||
17 | + rdoc.rdoc_dir = 'rdoc' | ||
18 | + rdoc.title = 'MathCaptcha' | ||
19 | + rdoc.options << '--line-numbers' << '--inline-source' | ||
20 | + rdoc.rdoc_files.include('README') | ||
21 | + rdoc.rdoc_files.include('lib/**/*.rb') | ||
22 | +end |
@@ -0,0 +1 @@ | @@ -0,0 +1 @@ | ||
1 | +# Install hook code here |
vendor/plugins/rails-math-captcha/lib/math_captcha/captcha.rb
0 → 100644
@@ -0,0 +1,68 @@ | @@ -0,0 +1,68 @@ | ||
1 | +require 'rubygems' | ||
2 | +require 'ezcrypto' | ||
3 | +class Captcha | ||
4 | + NUMBERS = (1..9).to_a | ||
5 | + OPERATORS = [:+, :-, :*] | ||
6 | + | ||
7 | + attr_reader :x, :y, :operator | ||
8 | + | ||
9 | + def initialize(x=nil, y=nil, operator=nil) | ||
10 | + @x = x || NUMBERS.sort_by{rand}.first | ||
11 | + @y = y || NUMBERS.sort_by{rand}.first | ||
12 | + @operator = operator || OPERATORS.sort_by{rand}.first | ||
13 | + end | ||
14 | + | ||
15 | + # Only the #to_secret is shared with the client. | ||
16 | + # It can be reused here to create the Captcha again | ||
17 | + def self.from_secret(secret) | ||
18 | + yml = cipher.decrypt64 secret | ||
19 | + args = YAML.load(yml) | ||
20 | + new(args[:x], args[:y], args[:operator]) | ||
21 | + end | ||
22 | + | ||
23 | + def self.cipher | ||
24 | + EzCrypto::Key.with_password key, 'bad_fixed_salt' | ||
25 | + end | ||
26 | + | ||
27 | + def self.key | ||
28 | + 'ultrasecret' | ||
29 | + end | ||
30 | + | ||
31 | + | ||
32 | + def check(answer) | ||
33 | + answer == solution | ||
34 | + end | ||
35 | + | ||
36 | + def task | ||
37 | + "#{@x} #{@operator.to_s} #{@y}" | ||
38 | + end | ||
39 | + def task_with_questionmark | ||
40 | + "#{@x} #{@operator.to_s} #{@y} = ?" | ||
41 | + end | ||
42 | + alias_method :to_s, :task | ||
43 | + | ||
44 | + def solution | ||
45 | + @x.send @operator, @y | ||
46 | + end | ||
47 | + | ||
48 | + def to_secret | ||
49 | + cipher.encrypt64(to_yaml) | ||
50 | + end | ||
51 | + | ||
52 | + def to_yaml | ||
53 | + YAML::dump({ | ||
54 | + :x => x, | ||
55 | + :y => y, | ||
56 | + :operator => operator | ||
57 | + }) | ||
58 | + end | ||
59 | + | ||
60 | + private | ||
61 | + def cipher | ||
62 | + @cipher ||= self.class.cipher | ||
63 | + end | ||
64 | + def reset_cipher | ||
65 | + @cipher = nil | ||
66 | + end | ||
67 | + | ||
68 | +end |
vendor/plugins/rails-math-captcha/lib/math_captcha/has_captcha.rb
0 → 100644
@@ -0,0 +1,54 @@ | @@ -0,0 +1,54 @@ | ||
1 | +module MathCaptcha | ||
2 | + module HasCaptcha | ||
3 | + module InstanceMethods | ||
4 | + def must_solve_captcha | ||
5 | + self.errors.add(:captcha_solution, "wrong answer.") unless self.captcha.check(self.captcha_solution.to_i) | ||
6 | + end | ||
7 | + def skip_captcha! | ||
8 | + self.class.skip_captcha! | ||
9 | + end | ||
10 | + def skip_captcha? | ||
11 | + self.class.skip_captcha? | ||
12 | + end | ||
13 | + def captcha | ||
14 | + @captcha ||= Captcha.new | ||
15 | + end | ||
16 | + def captcha_secret=(secret) | ||
17 | + @captcha = Captcha.from_secret(secret) | ||
18 | + end | ||
19 | + def captcha_secret | ||
20 | + captcha.to_secret | ||
21 | + end | ||
22 | + end | ||
23 | + | ||
24 | + module ClassMethods | ||
25 | + def has_captcha | ||
26 | + include InstanceMethods | ||
27 | + attr_accessor :captcha_solution | ||
28 | + dont_skip_captcha! | ||
29 | + validates_presence_of :captcha_solution, | ||
30 | + :on => :create, :message => "can't be blank", | ||
31 | + :unless => Proc.new {|record| record.skip_captcha? } | ||
32 | + validate_on_create :must_solve_captcha, | ||
33 | + :unless => Proc.new {|record| record.skip_captcha? } | ||
34 | + end | ||
35 | + def skip_captcha! | ||
36 | + @@skip_captcha = true | ||
37 | + end | ||
38 | + def dont_skip_captcha! | ||
39 | + @@skip_captcha = false | ||
40 | + end | ||
41 | + def skip_captcha? | ||
42 | + @@skip_captcha | ||
43 | + end | ||
44 | + def skipping_captcha(&block) | ||
45 | + skipping_before = skip_captcha? | ||
46 | + skip_captcha! | ||
47 | + yield | ||
48 | + dont_skip_captcha! if skipping_before | ||
49 | + end | ||
50 | + end | ||
51 | + end | ||
52 | +end | ||
53 | + | ||
54 | +ActiveRecord::Base.send(:extend, MathCaptcha::HasCaptcha::ClassMethods) |
@@ -0,0 +1,49 @@ | @@ -0,0 +1,49 @@ | ||
1 | +require 'lib/math_captcha/captcha' | ||
2 | +require 'base64' | ||
3 | + | ||
4 | +describe Captcha do | ||
5 | + describe "with a random task" do | ||
6 | + before(:each) do | ||
7 | + @captcha = Captcha.new | ||
8 | + end | ||
9 | + it "should have arguments and an operator" do | ||
10 | + @captcha.x.should_not be_nil | ||
11 | + @captcha.y.should_not be_nil | ||
12 | + @captcha.operator.should_not be_nil | ||
13 | + end | ||
14 | + it "should use numbers bigger than zero" do | ||
15 | + @captcha.x.should > 0 | ||
16 | + @captcha.y.should > 0 | ||
17 | + end | ||
18 | + it "should offer a human readable task" do | ||
19 | + @captcha.task.should =~ /^\d+\s*[\+\-\*]\s*\d+$/ | ||
20 | + end | ||
21 | + it "should have a secret to use in forms" do | ||
22 | + @captcha.to_secret.should_not be_nil | ||
23 | + @captcha.to_secret.should_not be_empty | ||
24 | + end | ||
25 | + | ||
26 | + it "should re-use its cipher" do | ||
27 | + @captcha.send(:cipher).should == @captcha.send(:cipher) | ||
28 | + end | ||
29 | + | ||
30 | + it "should have a base64 encoded secret" do | ||
31 | + lambda { Base64.decode64(@captcha.to_secret).should_not be_nil }.should_not raise_error | ||
32 | + end | ||
33 | + | ||
34 | + describe "re-creating another from secret" do | ||
35 | + before(:each) do | ||
36 | + @secret = @captcha.to_secret | ||
37 | + @new_captcha = Captcha.from_secret(@secret) | ||
38 | + end | ||
39 | + it "should have the same arguments and operator" do | ||
40 | + @new_captcha.x.should == @captcha.x | ||
41 | + @new_captcha.y.should == @captcha.y | ||
42 | + @new_captcha.operator.should == @captcha.operator | ||
43 | + end | ||
44 | + it "should have the same string" do | ||
45 | + @new_captcha.task.should == @captcha.task | ||
46 | + end | ||
47 | + end | ||
48 | + end | ||
49 | +end |