karsthammer-validates_captcha 0.9.5a
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/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
|
+
|