├── .gitignore ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── gotcha.gemspec ├── gotchas ├── backward_gotcha.rb └── sum_gotcha.rb ├── lib ├── gotcha.rb └── gotcha │ ├── base.rb │ ├── controller_helpers.rb │ ├── form_helpers.rb │ ├── validation_error.rb │ └── version.rb └── spec ├── examples ├── backward_gotcha_spec.rb ├── basic_spec.rb ├── rails_spec.rb └── sumgotcha_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *.sqlite 4 | .bundle 5 | Gemfile.lock 6 | *.gem 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | gemspec 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright © 2011 John Crepezzi 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gotcha 2 | 3 | Adding captchas to some action should be really easy. It shouldn't require a migration, like [Brain Buster](https://github.com/rsanheim/brain_buster) or API calls like [reCAPTCHA](http://www.google.com/recaptcha). Gotcha is an easy way to ask (custom) questions of your users in order for them to perform some action (like submitting a form). 4 | 5 | --- 6 | 7 | ## Installation 8 | 9 | To install Gotcha, just run: 10 | 11 | $ gem install gotcha 12 | 13 | Or put it in your Gemfile 14 | 15 | gem 'gotcha' 16 | 17 | --- 18 | 19 | ## Built-in Gotchas 20 | 21 | There are a few captchas implemented and installed by default: 22 | 23 | * **SumGotcha** - Two random numbers, ask the user for the sum of them 24 | * **BackwardGotcha** - Ask the user to retype a random string backwards (case-insensitive) 25 | 26 | A random Gotcha type will be generated on each call to `gotcha` 27 | 28 | --- 29 | 30 | ## Using Gotcha 31 | 32 | ### In your forms (HAML used for brevity): 33 | 34 | = form_for @thing do |f| 35 | = gotcha_error 36 | = gotcha 37 | = f.submit 38 | 39 | ### In your controller 40 | 41 | def new 42 | @thing = Thing.new 43 | end 44 | 45 | def create 46 | @thing = Thing.new(params[:thing]) 47 | if gotcha_valid? && @thing.save 48 | redirect_to @thing 49 | else 50 | render :new 51 | end 52 | end 53 | 54 | --- 55 | 56 | ## Multiple Gotchas in a page? 57 | 58 | = form_for @thing do |f| 59 | = gotcha_error 60 | - 10.times do 61 | .gotcha= gotcha 62 | = f.submit 63 | 64 | and in your controller 65 | 66 | gotcha_valid?(10) 67 | 68 | --- 69 | 70 | ## Writing your own Gotchas 71 | 72 | To write your own Gotcha, all you have to do is extend `Gotcha::Base` and provide a `question` and an `answer`: 73 | 74 | class MyGotcha < Gotcha::Base 75 | def initialize 76 | @question = 'Who made this?' 77 | @answer = 'john' 78 | end 79 | end 80 | 81 | When writing your own gotchas, you may want the answers to be able to flex a bit, like allowing users to enter things in different cases. In that case, your Gotcha can just override `self.down_transform` with whatever logic you want. It takes a single argument, `text` which is the text of the answer. You are expected to put it in a form that it would match regardless of the cases you want. By default, `self.down_transform` is implemented as: 82 | 83 | def self.down_transform(text) 84 | text = text.is_a?(String) ? text.dup : text.to_s 85 | text.downcase! 86 | text.gsub!(/\s+/, ' ') 87 | text.strip! 88 | text 89 | end 90 | 91 | Meaning by default, space types don't matter - and case is insensitive. 92 | 93 | --- 94 | 95 | ## Installing your gotchas 96 | 97 | In an initializer: 98 | 99 | Gotcha.unregister_all_types # Remove pre-defined 100 | Gotcha.register_type SumGotcha 101 | 102 | --- 103 | 104 | ## Determine if the gotcha was valid 105 | 106 | You have a few ways to determine whether or not a Gotcha was valid. You can use `gotcha_valid?` in views and controllers, or use `validate_gotcha!` (which throws a `Gotcha::ValidationException` if the Gotcha was not valid) in your controllers. 107 | 108 | ## Testing 109 | 110 | In testing, it's sometimes useful to make validation always return true. For this, you can use: `Gotcha.skip_validation = true` 111 | 112 | --- 113 | 114 | ## License 115 | 116 | MIT License. See attached 117 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'spec/rake/spectask' 2 | require File.dirname(__FILE__) + '/lib/gotcha/version' 3 | 4 | task :build do 5 | system "gem build gotcha.gemspec" 6 | end 7 | 8 | task :release => :build do 9 | # tag and push 10 | system "git tag v#{Gotcha::VERSION}" 11 | system "git push origin --tags" 12 | # push the gem 13 | system "gem push gotcha-#{Gotcha::VERSION}.gem" 14 | end 15 | 16 | Spec::Rake::SpecTask.new(:test) do |t| 17 | t.spec_files = FileList['spec/**/*_spec.rb'] 18 | fail_on_error = true # be explicit 19 | end 20 | 21 | Spec::Rake::SpecTask.new(:rcov) do |t| 22 | t.spec_files = FileList['spec/**/*_spec.rb'] 23 | t.rcov = true 24 | fail_on_error = true # be explicit 25 | end 26 | -------------------------------------------------------------------------------- /gotcha.gemspec: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/lib/gotcha/version' 2 | 3 | spec = Gem::Specification.new do |s| 4 | 5 | s.name = 'gotcha' 6 | s.author = 'John Crepezzi' 7 | s.add_development_dependency('rspec') 8 | s.add_development_dependency('actionpack') 9 | s.description = 'A smart captcha library' 10 | s.email = 'john.crepezzi@patch.com' 11 | s.files = Dir['lib/**/*.rb'] + Dir['gotchas/*.rb'] 12 | s.homepage = 'http://seejohnrun.github.com/gotcha/' 13 | s.platform = Gem::Platform::RUBY 14 | s.require_paths = ['lib'] 15 | s.summary = 'A captcha library for auto-generating questions' 16 | s.test_files = Dir.glob('spec/*.rb') 17 | s.version = Gotcha::VERSION 18 | s.rubyforge_project = 'gotcha' 19 | 20 | end 21 | -------------------------------------------------------------------------------- /gotchas/backward_gotcha.rb: -------------------------------------------------------------------------------- 1 | class BackwardGotcha < Gotcha::Base 2 | 3 | MIN_STRING_LENGTH = 8 4 | MAX_STRING_LENGTH = 15 5 | 6 | CHARS = ('a'..'z').to_a + ('0'..'9').to_a 7 | 8 | def initialize 9 | string_length = rand(MAX_STRING_LENGTH - MIN_STRING_LENGTH) + MIN_STRING_LENGTH 10 | string = (0...string_length).collect { CHARS[Kernel.rand(CHARS.length)] }.join 11 | 12 | @question = "What is '#{string}' backwards?" 13 | @answer = string.reverse 14 | end 15 | 16 | end 17 | 18 | Gotcha.register_type BackwardGotcha 19 | -------------------------------------------------------------------------------- /gotchas/sum_gotcha.rb: -------------------------------------------------------------------------------- 1 | class SumGotcha < Gotcha::Base 2 | 3 | DEFAULT_MIN = 0 4 | DEFAULT_MAX = 20 5 | 6 | def initialize 7 | rand1 = self.class.random_number_in_range 8 | rand2 = self.class.random_number_in_range 9 | @question = ["What is the sum of #{rand1} and #{rand2}?", "What is #{rand1} + #{rand2}?"][rand(2)] 10 | @answer = rand1 + rand2 11 | end 12 | 13 | private 14 | 15 | def self.max 16 | @@max ||= DEFAULT_MAX 17 | end 18 | 19 | def self.min 20 | @@min ||= DEFAULT_MIN 21 | end 22 | 23 | def self.random_number_in_range 24 | rand(self.max - self.min) + self.min 25 | end 26 | 27 | end 28 | 29 | Gotcha.register_type SumGotcha 30 | -------------------------------------------------------------------------------- /lib/gotcha.rb: -------------------------------------------------------------------------------- 1 | module Gotcha 2 | 3 | require File.dirname(__FILE__) + '/gotcha/base' 4 | autoload :FormHelpers, File.dirname(__FILE__) + '/gotcha/form_helpers' 5 | autoload :ControllerHelpers, File.dirname(__FILE__) + '/gotcha/controller_helpers' 6 | 7 | # Remove all gotcha types 8 | def self.unregister_all_types 9 | @gotcha_types = [] 10 | end 11 | 12 | # Register a Gotcha type 13 | def self.register_type(type) 14 | @gotcha_types ||= [] 15 | @gotcha_types << type 16 | end 17 | 18 | # Get a random Gotcha from the registered types 19 | def self.random 20 | if !@gotcha_types.nil? && type = random_type 21 | type.new 22 | end 23 | end 24 | 25 | # Whether or not to skip validation 26 | def self.skip_validation? 27 | @skip_validation || false 28 | end 29 | 30 | # Set whether or not to skip validation 31 | def self.skip_validation=(val) 32 | @skip_validation = !!val 33 | end 34 | 35 | def self.random_type 36 | @gotcha_types.respond_to?(:sample) ? @gotcha_types.sample : @gotcha_types[rand(@gotcha_types.size)] 37 | end 38 | 39 | def self.gotcha_types 40 | @gotcha_types ||= [] 41 | end 42 | 43 | end 44 | 45 | ActionView::Base.send(:include, Gotcha::FormHelpers) 46 | ActionController::Base.send(:include, Gotcha::ControllerHelpers) 47 | 48 | Dir.glob(File.dirname(__FILE__) + '/../gotchas/*_gotcha.rb').each { |f| require f } 49 | -------------------------------------------------------------------------------- /lib/gotcha/base.rb: -------------------------------------------------------------------------------- 1 | module Gotcha 2 | 3 | class Base 4 | 5 | attr_reader :question, :answer 6 | 7 | # Determine whether or not an answer is correct 8 | def correct?(str) 9 | str = str.is_a?(String) ? str : str.to_s 10 | str == (@answer.is_a?(String) ? @answer : @answer.to_s) # don't change @answer type 11 | end 12 | 13 | # A default implementation of down_transform - adds the ability to make transforms fuzzy 14 | def self.down_transform(text) 15 | text = text.is_a?(String) ? text.dup : text.to_s 16 | text.downcase! 17 | text.gsub! /\s+/, ' ' 18 | text.strip! 19 | text 20 | end 21 | 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /lib/gotcha/controller_helpers.rb: -------------------------------------------------------------------------------- 1 | module Gotcha 2 | 3 | module ControllerHelpers 4 | 5 | def self.included(base) 6 | base.send(:helper_method, :gotcha_valid?) 7 | end 8 | 9 | # return a true / false as to whether or not *all* of the gotchas on the page validated 10 | def gotcha_valid?(expected = 1) 11 | return true if Gotcha.skip_validation? 12 | @_gotcha_validated ||= determine_gotcha_validity(expected) 13 | end 14 | 15 | # Validate the gotcha, throw an exception if the gotcha does not validate (any on the page) 16 | def validate_gotcha!(expected = 1) 17 | raise Gotcha::ValidationError.new unless gotcha_valid?(expected) 18 | end 19 | 20 | private 21 | 22 | # Go through each response, using the down_transform 23 | # of the original class (as long as it is a subclass of Gotcha::Base) 24 | # and compare the hash to the hash of the value 25 | def determine_gotcha_validity(expected_gotcha_count = 1) 26 | return false unless params[:gotcha_response].kind_of?(Enumerable) 27 | return false unless params[:gotcha_response].count == expected_gotcha_count 28 | params[:gotcha_response].all? do |ident, value| 29 | type, hash = ident.split '-' 30 | return false unless Object.const_defined?(type) 31 | return false unless (klass = Object.const_get(type)) < Gotcha::Base 32 | Digest::MD5.hexdigest(klass.down_transform(value)) == hash 33 | end 34 | end 35 | 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /lib/gotcha/form_helpers.rb: -------------------------------------------------------------------------------- 1 | module Gotcha 2 | 3 | module FormHelpers 4 | 5 | # Propose a gotcha to the user - question and answer hash 6 | def gotcha(options = {}) 7 | options[:label_options] ||= {} 8 | options[:text_field_options] ||= {} 9 | if gotcha = Gotcha.random 10 | field = "gotcha_response[#{gotcha.class.name.to_s}-#{Digest::MD5.hexdigest(gotcha.class.down_transform(gotcha.answer))}]" 11 | (label_tag field, gotcha.question, options[:label_options]) + "\n" + (text_field_tag field, nil, options[:text_field_options]) 12 | else 13 | raise "No Gotchas Installed" 14 | end 15 | end 16 | 17 | # Return the gotcha error if its needed 18 | def gotcha_error 19 | t(:gotcha_validation_failed, :default => 'Failed to validate the Gotcha.') if @_gotcha_validated === false 20 | end 21 | 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /lib/gotcha/validation_error.rb: -------------------------------------------------------------------------------- 1 | class ValidationError < Exception 2 | end 3 | -------------------------------------------------------------------------------- /lib/gotcha/version.rb: -------------------------------------------------------------------------------- 1 | module Gotcha 2 | 3 | VERSION = '0.0.6' 4 | 5 | end 6 | -------------------------------------------------------------------------------- /spec/examples/backward_gotcha_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe BackwardGotcha do 4 | 5 | before(:all) do 6 | Gotcha.unregister_all_types 7 | Gotcha.register_type BackwardGotcha 8 | end 9 | 10 | it 'should be able to ask a question' do 11 | gotcha = Gotcha.random 12 | gotcha.question.should_not be_empty 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /spec/examples/basic_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe Gotcha do 4 | 5 | before :all do 6 | Gotcha.unregister_all_types 7 | end 8 | 9 | before :all do 10 | class NextNumberGotcha < Gotcha::Base 11 | def initialize 12 | @question = '1, 2, 3, ?' 13 | @answer = 4 14 | end 15 | end 16 | Gotcha.register_type(NextNumberGotcha) 17 | end 18 | 19 | it 'should not skip validation by default' do 20 | Gotcha.skip_validation?.should be_false 21 | end 22 | 23 | it 'should be able to be told to skip validation' do 24 | Gotcha.skip_validation = true 25 | Gotcha.skip_validation?.should be true 26 | end 27 | 28 | it 'should be able to select a random type of gotcha' do 29 | gotcha = Gotcha.random 30 | gotcha.should be_a(Gotcha::Base) 31 | end 32 | 33 | it 'should be able to get the question for a gotcha' do 34 | gotcha = Gotcha.random 35 | gotcha.question.should_not be_empty 36 | end 37 | 38 | it 'should be able to check the answer' do 39 | gotcha = Gotcha.random 40 | gotcha.correct?(4).should be_true 41 | end 42 | 43 | it 'should be able to check the answer even if we supply a string' do 44 | gotcha = Gotcha.random 45 | gotcha.correct?('4').should be_true 46 | end 47 | 48 | it 'should be able to verify it got the wrong answer' do 49 | gotcha = Gotcha.random 50 | gotcha.correct?(5).should be_false 51 | end 52 | 53 | end 54 | -------------------------------------------------------------------------------- /spec/examples/rails_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | class SamplesController < ActionController::Base 4 | 5 | # To make testing easier 6 | def flash 7 | @flash ||= {} 8 | end 9 | 10 | end 11 | -------------------------------------------------------------------------------- /spec/examples/sumgotcha_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | require File.dirname(__FILE__) + '/../../gotchas/sum_gotcha' 3 | 4 | describe SumGotcha do 5 | 6 | before(:all) do 7 | Gotcha.unregister_all_types 8 | Gotcha.register_type(SumGotcha) 9 | end 10 | 11 | it 'should be able to pose a question for a sum of two numbers' do 12 | gotcha = Gotcha.random 13 | matches = gotcha.question.match /[^\d]+(\d+)[^\d]+(\d+)[^\d]+/ 14 | 15 | # should get the right answer for this simple one 16 | matches.should_not be_nil 17 | gotcha.correct?($1.to_i + $2.to_i).should be_true 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'action_view' 3 | require 'action_controller' 4 | require File.dirname(__FILE__) + '/../lib/gotcha' 5 | --------------------------------------------------------------------------------