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.
@@ -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
+