karsthammer-validates_captcha 0.9.5a
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +22 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +320 -0
- data/Rakefile +102 -0
- data/lib/validates_captcha.rb +76 -0
- data/lib/validates_captcha/controller_validation.rb +63 -0
- data/lib/validates_captcha/form_builder.rb +16 -0
- data/lib/validates_captcha/form_helper.rb +39 -0
- data/lib/validates_captcha/image_generator/simple.rb +94 -0
- data/lib/validates_captcha/model_validation.rb +65 -0
- data/lib/validates_captcha/provider/dynamic_image.rb +240 -0
- data/lib/validates_captcha/provider/question.rb +110 -0
- data/lib/validates_captcha/provider/static_image.rb +224 -0
- data/lib/validates_captcha/string_generator/simple.rb +81 -0
- data/lib/validates_captcha/symmetric_encryptor/simple.rb +52 -0
- data/lib/validates_captcha/test_case.rb +12 -0
- data/lib/validates_captcha/version.rb +9 -0
- data/rails/init.rb +29 -0
- data/tasks/static_image_tasks.rake +33 -0
- data/test/cases/controller_validation_test.rb +222 -0
- data/test/cases/image_generator/simple_test.rb +34 -0
- data/test/cases/model_validation_test.rb +258 -0
- data/test/cases/provider/dynamic_image_test.rb +103 -0
- data/test/cases/provider/question_test.rb +41 -0
- data/test/cases/provider/static_image_test.rb +148 -0
- data/test/cases/string_generator/simple_test.rb +115 -0
- data/test/cases/symmetric_encryptor/simple_test.rb +28 -0
- data/test/cases/validates_captcha_test.rb +28 -0
- data/test/test_helper.rb +26 -0
- metadata +120 -0
@@ -0,0 +1,76 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2009 Martin Andert
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
|
25
|
+
# This module contains the getter and setter for the captcha provider.
|
26
|
+
# This allows you to replace it with your custom implementation. For more
|
27
|
+
# information on how to bring Validates Captcha to use your own
|
28
|
+
# implementation instead of the default one, consult the documentation
|
29
|
+
# for the default provider.
|
30
|
+
module ValidatesCaptcha
|
31
|
+
autoload :ModelValidation, 'validates_captcha/model_validation'
|
32
|
+
autoload :ControllerValidation, 'validates_captcha/controller_validation'
|
33
|
+
autoload :FormHelper, 'validates_captcha/form_helper'
|
34
|
+
autoload :FormBuilder, 'validates_captcha/form_builder'
|
35
|
+
autoload :TestCase, 'validates_captcha/test_case'
|
36
|
+
autoload :VERSION, 'validates_captcha/version'
|
37
|
+
|
38
|
+
module Provider
|
39
|
+
autoload :Question, 'validates_captcha/provider/question'
|
40
|
+
autoload :DynamicImage, 'validates_captcha/provider/dynamic_image'
|
41
|
+
autoload :StaticImage, 'validates_captcha/provider/static_image'
|
42
|
+
end
|
43
|
+
|
44
|
+
module StringGenerator
|
45
|
+
autoload :Simple, 'validates_captcha/string_generator/simple'
|
46
|
+
end
|
47
|
+
|
48
|
+
module SymmetricEncryptor
|
49
|
+
autoload :Simple, 'validates_captcha/symmetric_encryptor/simple'
|
50
|
+
end
|
51
|
+
|
52
|
+
module ImageGenerator
|
53
|
+
autoload :Simple, 'validates_captcha/image_generator/simple'
|
54
|
+
end
|
55
|
+
|
56
|
+
@@provider = nil
|
57
|
+
|
58
|
+
class << self
|
59
|
+
# Returns Validates Captcha's current version number.
|
60
|
+
def version
|
61
|
+
ValidatesCaptcha::VERSION::STRING
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns the current captcha challenge provider. Defaults to an instance of
|
65
|
+
# the ValidatesCaptcha::Provider::Question class.
|
66
|
+
def provider
|
67
|
+
@@provider ||= Provider::Question.new
|
68
|
+
end
|
69
|
+
|
70
|
+
# Sets the current captcha challenge provider. Used to set a custom provider.
|
71
|
+
def provider=(provider)
|
72
|
+
@@provider = provider
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module ValidatesCaptcha
|
2
|
+
module ControllerValidation
|
3
|
+
def self.included(base) #:nodoc:
|
4
|
+
base.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
# This module extends ActionController::Base with methods for captcha
|
8
|
+
# verification.
|
9
|
+
module ClassMethods
|
10
|
+
# This method is the one Validates Captcha got its name from. It
|
11
|
+
# internally calls #validates_captcha_of with the name of the controller
|
12
|
+
# as first argument and passing the conditions hash.
|
13
|
+
#
|
14
|
+
# Usage Example:
|
15
|
+
#
|
16
|
+
# class UsersController < ApplicationController
|
17
|
+
# # Whenever a User gets saved, validate the captcha.
|
18
|
+
# validates_captcha
|
19
|
+
#
|
20
|
+
# def create
|
21
|
+
# # ... user creation code ...
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# # ... more actions ...
|
25
|
+
# end
|
26
|
+
def validates_captcha(conditions = {})
|
27
|
+
validates_captcha_of controller_name, conditions
|
28
|
+
end
|
29
|
+
|
30
|
+
# Activates captcha validation for the specified model.
|
31
|
+
#
|
32
|
+
# The +model+ argument can be a Class, a string, or a symbol.
|
33
|
+
#
|
34
|
+
# This method internally creates an around filter, passing the
|
35
|
+
# +conditions+ argument to it. So you can (de)activate captcha
|
36
|
+
# validation for specific actions.
|
37
|
+
#
|
38
|
+
# Usage examples:
|
39
|
+
#
|
40
|
+
# class UsersController < ApplicationController
|
41
|
+
# validates_captcha_of User
|
42
|
+
# validates_captcha_of :users, :only => [:create, :update]
|
43
|
+
# validates_captcha_of 'user', :except => :persist
|
44
|
+
#
|
45
|
+
# # ... actions go here ...
|
46
|
+
# end
|
47
|
+
def validates_captcha_of(model, conditions = {})
|
48
|
+
model = model.is_a?(Class) ? model : model.to_s.classify.constantize
|
49
|
+
without_formats = Array.wrap(conditions.delete(:without)).map(&:to_sym)
|
50
|
+
|
51
|
+
around_filter(conditions) do |controller, action|
|
52
|
+
if without_formats.include?(controller.request.format.to_sym)
|
53
|
+
action.call
|
54
|
+
else
|
55
|
+
model.with_captcha_validation do
|
56
|
+
action.call
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ValidatesCaptcha
|
2
|
+
module FormBuilder #:nodoc:
|
3
|
+
def captcha_challenge(options = {}) #:nodoc:
|
4
|
+
@template.captcha_challenge @object_name, options.merge(:object => @object)
|
5
|
+
end
|
6
|
+
|
7
|
+
def captcha_field(options = {}) #:nodoc:
|
8
|
+
@template.captcha_field @object_name, options.merge(:object => @object)
|
9
|
+
end
|
10
|
+
|
11
|
+
def regenerate_captcha_challenge_link(options = {}, html_options = {}) #:nodoc:
|
12
|
+
@template.regenerate_captcha_challenge_link @object_name, options.merge(:object => @object), html_options
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module ValidatesCaptcha
|
2
|
+
module FormHelper
|
3
|
+
# Returns the captcha challenge.
|
4
|
+
#
|
5
|
+
# Internally calls the +render_challenge+ method of ValidatesCaptcha#provider.
|
6
|
+
def captcha_challenge(object_name, options = {})
|
7
|
+
options.symbolize_keys!
|
8
|
+
|
9
|
+
object = options.delete(:object)
|
10
|
+
sanitized_object_name = object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
|
11
|
+
|
12
|
+
ValidatesCaptcha.provider.render_challenge sanitized_object_name, object, options
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns an input tag of the "text" type tailored for entering the captcha solution.
|
16
|
+
#
|
17
|
+
# Internally calls Rails' #text_field helper method, passing the +object_name+ and
|
18
|
+
# +options+ arguments.
|
19
|
+
def captcha_field(object_name, options = {})
|
20
|
+
options.delete(:id)
|
21
|
+
|
22
|
+
hidden_field(object_name, :captcha_challenge, options) + text_field(object_name, :captcha_solution, options)
|
23
|
+
end
|
24
|
+
|
25
|
+
# By default, returns an anchor tag that makes an AJAX request to fetch a new captcha challenge and updates
|
26
|
+
# the current challenge after the request is complete.
|
27
|
+
#
|
28
|
+
# Internally calls +render_regenerate_challenge_link+ method of ValidatesCaptcha#provider.
|
29
|
+
def regenerate_captcha_challenge_link(object_name, options = {}, html_options = {})
|
30
|
+
options.symbolize_keys!
|
31
|
+
|
32
|
+
object = options.delete(:object)
|
33
|
+
sanitized_object_name = object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
|
34
|
+
|
35
|
+
ValidatesCaptcha.provider.render_regenerate_challenge_link sanitized_object_name, object, options, html_options
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module ValidatesCaptcha
|
2
|
+
module ImageGenerator
|
3
|
+
# This class is responsible for creating the captcha image. It internally
|
4
|
+
# uses ImageMagick's +convert+ command to generate the image bytes. So
|
5
|
+
# ImageMagick must be installed on the system for it to work properly.
|
6
|
+
#
|
7
|
+
# In order to deliver the captcha image to the user's browser,
|
8
|
+
# Validate Captcha's Rack middleware calls the methods of this class
|
9
|
+
# to create the image, to retrieve its mime type, and to construct the
|
10
|
+
# path to it.
|
11
|
+
#
|
12
|
+
# The image generation process is no rocket science. The chars are just
|
13
|
+
# laid out next to each other with varying vertical positions, font sizes,
|
14
|
+
# and weights. Then a slight rotation is performed and some randomly
|
15
|
+
# positioned lines are rendered on the canvas.
|
16
|
+
#
|
17
|
+
# Sure, the created captcha can easily be cracked by intelligent
|
18
|
+
# bots. As the name of the class suggests, it's rather a starting point
|
19
|
+
# for your own implementations.
|
20
|
+
#
|
21
|
+
# You can implement your own (better) image generator by creating a
|
22
|
+
# class that conforms to the method definitions of the example below.
|
23
|
+
#
|
24
|
+
# Example for a custom image generator:
|
25
|
+
#
|
26
|
+
# class AdvancedImageGenerator
|
27
|
+
# def generate(captcha_text)
|
28
|
+
# # ... do your magic here ...
|
29
|
+
#
|
30
|
+
# return string_containing_image_bytes
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# def mime_type
|
34
|
+
# 'image/png'
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# def file_extension
|
38
|
+
# '.png'
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# Then assign an instance of it to ValidatesCaptcha::Provider::DynamicImage#image_generator=.
|
43
|
+
#
|
44
|
+
# ValidatesCaptcha::Provider::DynamicImage.image_generator = AdvancedImageGenerator.new
|
45
|
+
# ValidatesCaptcha.provider = ValidatesCaptcha::Provider::DynamicImage.new
|
46
|
+
#
|
47
|
+
# Or to ValidatesCaptcha::Provider::StaticImage#image_generator=.
|
48
|
+
#
|
49
|
+
# ValidatesCaptcha::Provider::StaticImage.image_generator = AdvancedImageGenerator.new
|
50
|
+
# ValidatesCaptcha.provider = ValidatesCaptcha::Provider::StaticImage.new
|
51
|
+
#
|
52
|
+
class Simple
|
53
|
+
MIME_TYPE = 'image/gif'.freeze
|
54
|
+
FILE_EXTENSION = '.gif'.freeze
|
55
|
+
|
56
|
+
# Returns a string containing the image bytes of the captcha.
|
57
|
+
# As the only argument, the cleartext captcha text must be passed.
|
58
|
+
def generate(captcha_code)
|
59
|
+
image_width = captcha_code.length * 20 + 10
|
60
|
+
|
61
|
+
cmd = []
|
62
|
+
cmd << "convert -size #{image_width}x40 xc:grey84 -background grey84 -fill black "
|
63
|
+
|
64
|
+
captcha_code.split(//).each_with_index do |char, i|
|
65
|
+
cmd << " -pointsize #{rand(8) + 15} "
|
66
|
+
cmd << " -weight #{rand(2) == 0 ? '4' : '8'}00 "
|
67
|
+
cmd << " -draw 'text #{5 + 20 * i},#{rand(10) + 20} \"#{char}\"' "
|
68
|
+
end
|
69
|
+
|
70
|
+
cmd << " -rotate #{rand(2) == 0 ? '-' : ''}5 -fill grey40 "
|
71
|
+
|
72
|
+
captcha_code.size.times do
|
73
|
+
cmd << " -draw 'line #{rand(image_width)},0 #{rand(image_width)},60' "
|
74
|
+
end
|
75
|
+
|
76
|
+
cmd << " gif:-"
|
77
|
+
|
78
|
+
image_magick_command = cmd.join
|
79
|
+
|
80
|
+
`#{image_magick_command}`
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns the image mime type. This is always 'image/gif'.
|
84
|
+
def mime_type
|
85
|
+
MIME_TYPE
|
86
|
+
end
|
87
|
+
|
88
|
+
# Returns the image file extension. This is always '.gif'.
|
89
|
+
def file_extension
|
90
|
+
FILE_EXTENSION
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module ValidatesCaptcha
|
2
|
+
module ModelValidation
|
3
|
+
def self.included(base) #:nodoc:
|
4
|
+
base.extend ClassMethods
|
5
|
+
base.send :include, InstanceMethods
|
6
|
+
|
7
|
+
base.class_eval do
|
8
|
+
attr_accessor :captcha_solution
|
9
|
+
attr_writer :captcha_challenge
|
10
|
+
|
11
|
+
validate :validate_captcha, :if => :validate_captcha?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
# Activates captcha validation on entering the block and deactivates
|
17
|
+
# captcha validation on leaving the block.
|
18
|
+
#
|
19
|
+
# Example:
|
20
|
+
#
|
21
|
+
# User.with_captcha_validation do
|
22
|
+
# @user = User.new(...)
|
23
|
+
# @user.save
|
24
|
+
# end
|
25
|
+
def with_captcha_validation(&block)
|
26
|
+
self.validate_captcha = true
|
27
|
+
result = yield
|
28
|
+
self.validate_captcha = false
|
29
|
+
result
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns +true+ if captcha validation is activated, otherwise +false+.
|
33
|
+
def validate_captcha? #:nodoc:
|
34
|
+
@validate_captcha == true
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
def validate_captcha=(value) #:nodoc:
|
39
|
+
@validate_captcha = value
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module InstanceMethods #:nodoc:
|
44
|
+
def captcha_challenge #:nodoc:
|
45
|
+
return @captcha_challenge unless @captcha_challenge.blank?
|
46
|
+
@captcha_challenge = ValidatesCaptcha.provider.generate_challenge
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
def validate_captcha? #:nodoc:
|
51
|
+
self.class.validate_captcha?
|
52
|
+
end
|
53
|
+
|
54
|
+
def validate_captcha #:nodoc:
|
55
|
+
errors.add(:captcha_solution, :blank) and return if captcha_solution.blank?
|
56
|
+
errors.add(:captcha_solution, :invalid) unless captcha_valid?
|
57
|
+
end
|
58
|
+
|
59
|
+
def captcha_valid? #:nodoc:
|
60
|
+
ValidatesCaptcha.provider.solved?(captcha_challenge, captcha_solution)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
@@ -0,0 +1,240 @@
|
|
1
|
+
require 'action_view/helpers'
|
2
|
+
|
3
|
+
module ValidatesCaptcha
|
4
|
+
# Here is how you can implement your own captcha challenge provider. Create a
|
5
|
+
# class that conforms to the public method definitions of the example below
|
6
|
+
# and assign an instance of it to ValidatesCaptcha#provider=.
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
#
|
10
|
+
# require 'action_view/helpers'
|
11
|
+
#
|
12
|
+
# class ReverseProvider
|
13
|
+
# include ActionView::Helpers # for content_tag, link_to_remote
|
14
|
+
#
|
15
|
+
# def initialize
|
16
|
+
# @string_generator = ValidatesCaptcha::StringGenerator::Simple.new
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# def generate_challenge
|
20
|
+
# @string_generator.generate # creates a random string
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# def solved?(challenge, solution)
|
24
|
+
# challenge.reverse == solution
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# def render_challenge(sanitized_object_name, object, options = {})
|
28
|
+
# options[:id] = "#{sanitized_object_name}_captcha_question"
|
29
|
+
#
|
30
|
+
# content_tag :span, "What's the reverse of '#{object.captcha_challenge}'?", options
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# def render_regenerate_challenge_link(sanitized_object_name, object, options = {}, html_options = {})
|
34
|
+
# text = options.delete(:text) || 'Regenerate'
|
35
|
+
# on_success = "var result = request.responseJSON; " \\
|
36
|
+
# "$('#{sanitized_object_name}_captcha_question').update(result.question); " \\
|
37
|
+
# "$('#{sanitized_object_name}_captcha_challenge').value = result.challenge; " \\
|
38
|
+
# "$('#{sanitized_object_name}_captcha_solution').value = '';"
|
39
|
+
#
|
40
|
+
# link_to_remote text, options.reverse_merge(:url => '/captchas/regenerate', :method => :get, :success => success), html_options
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# def call(env) # this is executed by Rack
|
44
|
+
# if env['PATH_INFO'] == '/captchas/regenerate'
|
45
|
+
# challenge = generate_challenge
|
46
|
+
# json = { :question => "What's the reverse of '#{challenge}'?", :challenge => challenge }.to_json
|
47
|
+
#
|
48
|
+
# [200, { 'Content-Type' => 'application/json' }, [json]]
|
49
|
+
# else
|
50
|
+
# [404, { 'Content-Type' => 'text/html' }, ['Not Found']]
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# private
|
55
|
+
# # Hack: This is needed by +link_to_remote+ called in +render_regenerate_challenge_link+.
|
56
|
+
# def protect_against_forgery?
|
57
|
+
# false
|
58
|
+
# end
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# ValidatesCaptcha.provider = ReverseProvider.new
|
62
|
+
module Provider
|
63
|
+
# An image captcha provider.
|
64
|
+
#
|
65
|
+
# This class contains the getters and setters for the backend classes:
|
66
|
+
# image generator, string generator, and symmetric encryptor. This
|
67
|
+
# allows you to replace them with your custom implementations. For more
|
68
|
+
# information on how to bring the image provider to use your own
|
69
|
+
# implementation instead of the default one, consult the documentation
|
70
|
+
# for the specific default class.
|
71
|
+
#
|
72
|
+
# The default captcha image generator uses ImageMagick's +convert+ command to
|
73
|
+
# create the captcha. So a recent and properly configured version of ImageMagick
|
74
|
+
# must be installed on the system. The version used while developing was 6.4.5.
|
75
|
+
# But you are not bound to ImageMagick. If you want to provide a custom image
|
76
|
+
# generator, take a look at the documentation for
|
77
|
+
# ValidatesCaptcha::ImageGenerator::Simple on how to create your own.
|
78
|
+
class DynamicImage
|
79
|
+
include ActionView::Helpers
|
80
|
+
|
81
|
+
@@string_generator = nil
|
82
|
+
@@symmetric_encryptor = nil
|
83
|
+
@@image_generator = nil
|
84
|
+
|
85
|
+
class << self
|
86
|
+
# Returns the current captcha string generator. Defaults to an
|
87
|
+
# instance of the ValidatesCaptcha::StringGenerator::Simple class.
|
88
|
+
def string_generator
|
89
|
+
@@string_generator ||= ValidatesCaptcha::StringGenerator::Simple.new
|
90
|
+
end
|
91
|
+
|
92
|
+
# Sets the current captcha string generator. Used to set a
|
93
|
+
# custom string generator.
|
94
|
+
def string_generator=(generator)
|
95
|
+
@@string_generator = generator
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns the current captcha symmetric encryptor. Defaults to an
|
99
|
+
# instance of the ValidatesCaptcha::SymmetricEncryptor::Simple class.
|
100
|
+
def symmetric_encryptor
|
101
|
+
@@symmetric_encryptor ||= ValidatesCaptcha::SymmetricEncryptor::Simple.new
|
102
|
+
end
|
103
|
+
|
104
|
+
# Sets the current captcha symmetric encryptor. Used to set a
|
105
|
+
# custom symmetric encryptor.
|
106
|
+
def symmetric_encryptor=(encryptor)
|
107
|
+
@@symmetric_encryptor = encryptor
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns the current captcha image generator. Defaults to an
|
111
|
+
# instance of the ValidatesCaptcha::ImageGenerator::Simple class.
|
112
|
+
def image_generator
|
113
|
+
@@image_generator ||= ValidatesCaptcha::ImageGenerator::Simple.new
|
114
|
+
end
|
115
|
+
|
116
|
+
# Sets the current captcha image generator. Used to set a custom
|
117
|
+
# image generator.
|
118
|
+
def image_generator=(generator)
|
119
|
+
@@image_generator = generator
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# This method is the one called by Rack.
|
124
|
+
#
|
125
|
+
# It returns HTTP status 404 if the path is not recognized. If the path is
|
126
|
+
# recognized, it returns HTTP status 200 and delivers the image if it could
|
127
|
+
# successfully decrypt the captcha code, otherwise HTTP status 422.
|
128
|
+
#
|
129
|
+
# Please take a look at the source code if you want to learn more.
|
130
|
+
def call(env)
|
131
|
+
if env['PATH_INFO'] =~ /^\/captchas\/([^\.]+)/
|
132
|
+
if $1 == 'regenerate'
|
133
|
+
captcha_challenge = generate_challenge
|
134
|
+
json = { :captcha_challenge => captcha_challenge, :captcha_image_path => image_path(captcha_challenge) }.to_json
|
135
|
+
|
136
|
+
[200, { 'Content-Type' => 'application/json' }, [json]]
|
137
|
+
else
|
138
|
+
decrypted_code = decrypt($1)
|
139
|
+
|
140
|
+
if decrypted_code.nil?
|
141
|
+
[422, { 'Content-Type' => 'text/html' }, ['Unprocessable Entity']]
|
142
|
+
else
|
143
|
+
image_data = generate_image(decrypted_code)
|
144
|
+
|
145
|
+
response_headers = {
|
146
|
+
'Content-Length' => image_data.length.to_s,
|
147
|
+
'Content-Type' => image_mime_type,
|
148
|
+
'Content-Disposition' => 'inline',
|
149
|
+
'Content-Transfer-Encoding' => 'binary',
|
150
|
+
'Cache-Control' => 'private'
|
151
|
+
}
|
152
|
+
|
153
|
+
[200, response_headers, [image_data]]
|
154
|
+
end
|
155
|
+
end
|
156
|
+
else
|
157
|
+
[404, { 'Content-Type' => 'text/html' }, ['Not Found']]
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Returns a captcha challenge.
|
162
|
+
def generate_challenge
|
163
|
+
encrypt(generate_code)
|
164
|
+
end
|
165
|
+
|
166
|
+
# Returns true if the captcha was solved using the given +challenge+ and +solution+,
|
167
|
+
# otherwise false.
|
168
|
+
def solved?(challenge, solution)
|
169
|
+
decrypt(challenge) == solution
|
170
|
+
end
|
171
|
+
|
172
|
+
# Returns an image tag with the source set to the url of the captcha image.
|
173
|
+
#
|
174
|
+
# Internally calls Rails' +image_tag+ helper method, passing the +options+ argument.
|
175
|
+
def render_challenge(sanitized_object_name, object, options = {})
|
176
|
+
src = image_path(object.captcha_challenge)
|
177
|
+
|
178
|
+
options[:alt] ||= 'CAPTCHA'
|
179
|
+
options[:id] = "#{sanitized_object_name}_captcha_image"
|
180
|
+
|
181
|
+
image_tag src, options
|
182
|
+
end
|
183
|
+
|
184
|
+
# Returns an anchor tag that makes an AJAX request to fetch a new captcha code and updates
|
185
|
+
# the captcha image after the request is complete.
|
186
|
+
#
|
187
|
+
# Internally calls Rails' +link_to_remote+ helper method, passing the +options+ and
|
188
|
+
# +html_options+ arguments. So it relies on the Prototype javascript framework
|
189
|
+
# to be available on the web page.
|
190
|
+
#
|
191
|
+
# The anchor text defaults to 'Regenerate Captcha'. You can set this to a custom value
|
192
|
+
# providing a +:text+ key in the +options+ hash.
|
193
|
+
def render_regenerate_challenge_link(sanitized_object_name, object, options = {}, html_options = {})
|
194
|
+
text = options.delete(:text) || 'Regenerate Captcha'
|
195
|
+
success = "var result = request.responseJSON; $('#{sanitized_object_name}_captcha_image').src = result.captcha_image_path; $('#{sanitized_object_name}_captcha_challenge').value = result.captcha_challenge; $('#{sanitized_object_name}_captcha_solution').value = '';"
|
196
|
+
|
197
|
+
link_to_remote text, options.reverse_merge(:url => regenerate_path, :method => :get, :success => success), html_options
|
198
|
+
end
|
199
|
+
|
200
|
+
private
|
201
|
+
def generate_image(code) #:nodoc:
|
202
|
+
self.class.image_generator.generate code
|
203
|
+
end
|
204
|
+
|
205
|
+
def image_mime_type #:nodoc:
|
206
|
+
self.class.image_generator.mime_type
|
207
|
+
end
|
208
|
+
|
209
|
+
def image_file_extension #:nodoc:
|
210
|
+
self.class.image_generator.file_extension
|
211
|
+
end
|
212
|
+
|
213
|
+
def encrypt(code) #:nodoc:
|
214
|
+
self.class.symmetric_encryptor.encrypt code
|
215
|
+
end
|
216
|
+
|
217
|
+
def decrypt(encrypted_code) #:nodoc:
|
218
|
+
self.class.symmetric_encryptor.decrypt encrypted_code
|
219
|
+
end
|
220
|
+
|
221
|
+
def generate_code #:nodoc:
|
222
|
+
self.class.string_generator.generate
|
223
|
+
end
|
224
|
+
|
225
|
+
def image_path(encrypted_code) #:nodoc:
|
226
|
+
"/captchas/#{encrypted_code}#{image_file_extension}"
|
227
|
+
end
|
228
|
+
|
229
|
+
def regenerate_path #:nodoc:
|
230
|
+
'/captchas/regenerate'
|
231
|
+
end
|
232
|
+
|
233
|
+
# This is needed by +link_to_remote+ called in +render_regenerate_link+.
|
234
|
+
def protect_against_forgery? #:nodoc:
|
235
|
+
false
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|