karsthammer-validates_captcha 0.9.5a

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+