validates_captcha 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -20,7 +20,7 @@ module ValidatesCaptcha
20
20
  #
21
21
  # You can implement your own (better) image generator by creating a
22
22
  # class that conforms to the method definitions of the example below and
23
- # assign an instance of it to ValidatesCaptcha#image_generator=.
23
+ # assign an instance of it to ValidatesCaptcha::Provider::Image#image_generator=.
24
24
  #
25
25
  # Example for a custom image generator:
26
26
  #
@@ -31,16 +31,17 @@ module ValidatesCaptcha
31
31
  # return string_containing_image_bytes
32
32
  # end
33
33
  #
34
- # def image_mime_type
34
+ # def mime_type
35
35
  # 'image/png'
36
36
  # end
37
37
  #
38
- # def image_file_extension
38
+ # def file_extension
39
39
  # '.png'
40
40
  # end
41
41
  # end
42
42
  #
43
- # ValidatesCaptcha.image_generator = AdvancedImageGenerator.new
43
+ # ValidatesCaptcha::Provider::Image.image_generator = AdvancedImageGenerator.new
44
+ # ValidatesCaptcha.provider = ValidatesCaptcha::Provider::Image.new
44
45
  #
45
46
  class Simple
46
47
  MIME_TYPE = 'image/gif'.freeze
@@ -74,12 +75,12 @@ module ValidatesCaptcha
74
75
  end
75
76
 
76
77
  # Returns the image mime type. This is always 'image/gif'.
77
- def image_mime_type
78
+ def mime_type
78
79
  MIME_TYPE
79
80
  end
80
81
 
81
82
  # Returns the image file extension. This is always '.gif'.
82
- def image_file_extension
83
+ def file_extension
83
84
  FILE_EXTENSION
84
85
  end
85
86
  end
@@ -5,9 +5,9 @@ module ValidatesCaptcha
5
5
  base.send :include, InstanceMethods
6
6
 
7
7
  base.class_eval do
8
- attr_accessible :captcha, :encrypted_captcha
9
- attr_accessor :captcha
10
- attr_writer :encrypted_captcha
8
+ attr_accessible :captcha_challenge, :captcha_solution
9
+ attr_accessor :captcha_solution
10
+ attr_writer :captcha_challenge
11
11
 
12
12
  validate :validate_captcha, :if => :validate_captcha?
13
13
  end
@@ -42,9 +42,9 @@ module ValidatesCaptcha
42
42
  end
43
43
 
44
44
  module InstanceMethods #:nodoc:
45
- def encrypted_captcha #:nodoc:
46
- return @encrypted_captcha unless @encrypted_captcha.blank?
47
- @encrypted_captcha = ValidatesCaptcha.encrypt_captcha_code(ValidatesCaptcha.generate_captcha_code)
45
+ def captcha_challenge #:nodoc:
46
+ return @captcha_challenge unless @captcha_challenge.blank?
47
+ @captcha_challenge = ValidatesCaptcha.provider.generate_challenge
48
48
  end
49
49
 
50
50
  private
@@ -53,12 +53,12 @@ module ValidatesCaptcha
53
53
  end
54
54
 
55
55
  def validate_captcha #:nodoc:
56
- errors.add(:captcha, :blank) and return if captcha.blank?
57
- errors.add(:captcha, :invalid) unless captcha_valid?
56
+ errors.add(:captcha_solution, :blank) and return if captcha_solution.blank?
57
+ errors.add(:captcha_solution, :invalid) unless captcha_valid?
58
58
  end
59
59
 
60
60
  def captcha_valid? #:nodoc:
61
- ValidatesCaptcha.encrypt_captcha_code(captcha) == encrypted_captcha
61
+ ValidatesCaptcha.provider.solved?(captcha_challenge, captcha_solution)
62
62
  end
63
63
  end
64
64
  end
@@ -0,0 +1,244 @@
1
+ require 'openssl'
2
+ require 'active_support/secure_random'
3
+ require 'action_view/helpers'
4
+
5
+ module ValidatesCaptcha
6
+ # Here is how you can implement your own captcha challenge provider. Create a
7
+ # class that conforms to the public method definitions of the example below
8
+ # and assign an instance of it to ValidatesCaptcha#provider=.
9
+ #
10
+ # Example:
11
+ #
12
+ # require 'action_view/helpers'
13
+ #
14
+ # class ReverseProvider
15
+ # include ActionView::Helpers # for content_tag, link_to_remote
16
+ #
17
+ # def initialize
18
+ # @string_generator = ValidatesCaptcha::StringGenerator::Simple.new
19
+ # end
20
+ #
21
+ # def generate_challenge
22
+ # @string_generator.generate # creates a random string
23
+ # end
24
+ #
25
+ # def solved?(challenge, solution)
26
+ # challenge.reverse == solution
27
+ # end
28
+ #
29
+ # def render_challenge(sanitized_object_name, object, options = {})
30
+ # options[:id] = "#{sanitized_object_name}_captcha_question"
31
+ #
32
+ # content_tag :span, "What's the reverse of '#{object.captcha_challenge}'?", options
33
+ # end
34
+ #
35
+ # def render_regenerate_challenge_link(sanitized_object_name, object, options = {}, html_options = {})
36
+ # text = options.delete(:text) || 'Regenerate'
37
+ # on_success = "var result = request.responseJSON; " \\
38
+ # "$('#{sanitized_object_name}_captcha_question').update(result.question); " \\
39
+ # "$('#{sanitized_object_name}_captcha_challenge').value = result.challenge; " \\
40
+ # "$('#{sanitized_object_name}_captcha_solution').value = '';"
41
+ #
42
+ # link_to_remote text, options.reverse_merge(:url => '/captchas/regenerate', :method => :get, :success => success), html_options
43
+ # end
44
+ #
45
+ # def call(env) # this is executed by Rack
46
+ # if env['PATH_INFO'] == '/captchas/regenerate'
47
+ # challenge = generate_challenge
48
+ # json = { :question => "What's the reverse of '#{challenge}'?", :challenge => challenge }.to_json
49
+ #
50
+ # [200, { 'Content-Type' => 'application/json' }, [json]]
51
+ # else
52
+ # [404, { 'Content-Type' => 'text/html' }, ['Not Found']]
53
+ # end
54
+ # end
55
+ #
56
+ # private
57
+ # # Hack: This is needed by +link_to_remote+ called in +render_regenerate_challenge_link+.
58
+ # def protect_against_forgery?
59
+ # false
60
+ # end
61
+ # end
62
+ #
63
+ # ValidatesCaptcha.provider = ReverseProvider.new
64
+ module Provider
65
+ # An image captcha provider.
66
+ #
67
+ # This class contains the getters and setters for the backend classes:
68
+ # image generator, string generator, and reversible encrypter. This
69
+ # allows you to replace them with your custom implementations. For more
70
+ # information on how to bring the image provider to use your own
71
+ # implementation instead of the default one, consult the documentation
72
+ # for the specific default class.
73
+ #
74
+ # The default captcha image generator uses ImageMagick's +convert+ command to
75
+ # create the captcha. So a recent and properly configured version of ImageMagick
76
+ # must be installed on the system. The version used while developing was 6.4.5.
77
+ # But you are not bound to ImageMagick. If you want to provide a custom image
78
+ # generator, take a look at the documentation for
79
+ # ValidatesCaptcha::ImageGenerator::Simple on how to create your own.
80
+ class Image
81
+ include ActionView::Helpers
82
+
83
+ KEY = ::ActiveSupport::SecureRandom.hex(32).freeze
84
+
85
+ @@string_generator = nil
86
+ @@reversible_encrypter = nil
87
+ @@image_generator = nil
88
+
89
+ class << self
90
+ # Returns the current captcha string generator. Defaults to an
91
+ # instance of the ValidatesCaptcha::StringGenerator::Simple class.
92
+ def string_generator
93
+ @@string_generator ||= ValidatesCaptcha::StringGenerator::Simple.new
94
+ end
95
+
96
+ # Sets the current captcha string generator. Used to set a
97
+ # custom string generator.
98
+ def string_generator=(generator)
99
+ @@string_generator = generator
100
+ end
101
+
102
+ # Returns the current captcha reversible encrypter. Defaults to an
103
+ # instance of the ValidatesCaptcha::ReversibleEncrypter::Simple class.
104
+ def reversible_encrypter
105
+ @@reversible_encrypter ||= ValidatesCaptcha::ReversibleEncrypter::Simple.new
106
+ end
107
+
108
+ # Sets the current captcha reversible encrypter. Used to set a
109
+ # custom reversible encrypter.
110
+ def reversible_encrypter=(encrypter)
111
+ @@reversible_encrypter = encrypter
112
+ end
113
+
114
+ # Returns the current captcha image generator. Defaults to an
115
+ # instance of the ValidatesCaptcha::ImageGenerator::Simple class.
116
+ def image_generator
117
+ @@image_generator ||= ValidatesCaptcha::ImageGenerator::Simple.new
118
+ end
119
+
120
+ # Sets the current captcha image generator. Used to set a custom
121
+ # image generator.
122
+ def image_generator=(generator)
123
+ @@image_generator = generator
124
+ end
125
+ end
126
+
127
+ # This method is the one called by Rack.
128
+ #
129
+ # It returns HTTP status 404 if the path is not recognized. If the path is
130
+ # recognized, it returns HTTP status 200 and delivers the image if it could
131
+ # successfully decrypt the captcha code, otherwise HTTP status 422.
132
+ #
133
+ # Please take a look at the source code if you want to learn more.
134
+ def call(env)
135
+ if env['PATH_INFO'] =~ /^\/captchas\/([^\.]+)/
136
+ if $1 == 'regenerate'
137
+ captcha_challenge = generate_challenge
138
+ json = { :captcha_challenge => captcha_challenge, :captcha_image_path => image_path(captcha_challenge) }.to_json
139
+
140
+ [200, { 'Content-Type' => 'application/json' }, [json]]
141
+ else
142
+ decrypted_code = decrypt($1)
143
+
144
+ if decrypted_code.nil?
145
+ [422, { 'Content-Type' => 'text/html' }, ['Unprocessable Entity']]
146
+ else
147
+ image_data = generate_image(decrypted_code)
148
+
149
+ response_headers = {
150
+ 'Content-Length' => image_data.bytesize.to_s,
151
+ 'Content-Type' => image_mime_type,
152
+ 'Content-Disposition' => 'inline',
153
+ 'Content-Transfer-Encoding' => 'binary',
154
+ 'Cache-Control' => 'private'
155
+ }
156
+
157
+ [200, response_headers, [image_data]]
158
+ end
159
+ end
160
+ else
161
+ [404, { 'Content-Type' => 'text/html' }, ['Not Found']]
162
+ end
163
+ end
164
+
165
+ # Returns a captcha challenge.
166
+ def generate_challenge
167
+ encrypt(generate_code)
168
+ end
169
+
170
+ # Returns true if the captcha was solved using the given +challenge+ and +solution+,
171
+ # otherwise false.
172
+ def solved?(challenge, solution)
173
+ challenge == encrypt(solution)
174
+ end
175
+
176
+ # Returns an image tag with the source set to the url of the captcha image.
177
+ #
178
+ # Internally calls Rails' +image_tag+ helper method, passing the +options+ argument.
179
+ def render_challenge(sanitized_object_name, object, options = {})
180
+ src = image_path(object.captcha_challenge)
181
+
182
+ options[:alt] ||= 'CAPTCHA'
183
+ options[:id] = "#{sanitized_object_name}_captcha_image"
184
+
185
+ image_tag src, options
186
+ end
187
+
188
+ # Returns an anchor tag that makes an AJAX request to fetch a new captcha code and updates
189
+ # the captcha image after the request is complete.
190
+ #
191
+ # Internally calls Rails' +link_to_remote+ helper method, passing the +options+ and
192
+ # +html_options+ arguments. So it relies on the Prototype javascript framework
193
+ # to be available on the web page.
194
+ #
195
+ # The anchor text defaults to 'Regenerate Captcha'. You can set this to a custom value
196
+ # providing a +:text+ key in the +options+ hash.
197
+ def render_regenerate_challenge_link(sanitized_object_name, object, options = {}, html_options = {})
198
+ text = options.delete(:text) || 'Regenerate Captcha'
199
+ 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 = '';"
200
+
201
+ link_to_remote text, options.reverse_merge(:url => regenerate_path, :method => :get, :success => success), html_options
202
+ end
203
+
204
+ private
205
+ def generate_image(code) #:nodoc:
206
+ self.class.image_generator.generate code
207
+ end
208
+
209
+ def image_mime_type #:nodoc:
210
+ self.class.image_generator.mime_type
211
+ end
212
+
213
+ def image_file_extension #:nodoc:
214
+ self.class.image_generator.file_extension
215
+ end
216
+
217
+ def encrypt(code) #:nodoc:
218
+ self.class.reversible_encrypter.encrypt code
219
+ end
220
+
221
+ def decrypt(encrypted_code) #:nodoc:
222
+ self.class.reversible_encrypter.decrypt encrypted_code
223
+ end
224
+
225
+ def generate_code #:nodoc:
226
+ self.class.string_generator.generate
227
+ end
228
+
229
+ def image_path(encrypted_code) #:nodoc:
230
+ "/captchas/#{encrypted_code}#{image_file_extension}"
231
+ end
232
+
233
+ def regenerate_path #:nodoc:
234
+ '/captchas/regenerate'
235
+ end
236
+
237
+ # This is needed by +link_to_remote+ called in +render_regenerate_link+.
238
+ def protect_against_forgery? #:nodoc:
239
+ false
240
+ end
241
+ end
242
+ end
243
+ end
244
+
@@ -0,0 +1,110 @@
1
+ require 'action_view/helpers'
2
+
3
+ module ValidatesCaptcha
4
+ module Provider
5
+ # A question/answer captcha provider.
6
+ class Question
7
+ include ActionView::Helpers
8
+
9
+ DEFAULT_QUESTIONS_AND_ANSWERS = {
10
+ "What's the capital of France?" => "Paris",
11
+ "What's the capital of Germany?" => "Berlin",
12
+ "What's the opposite of good?" => ["bad", "evil"],
13
+ "What's the opposite of love?" => "hate",
14
+ "What's the sum of 2 and 3?" => ["5", "five"],
15
+ "What's the product of 3 and 4?" => ["12", "twelve"],
16
+ "Thumb, tooth or hand: which is part of the head?" => "tooth",
17
+ "Bread, ham or milk: which is something to drink?" => "milk",
18
+ "What day is today, if yesterday was Friday?" => "Saturday",
19
+ "What day is today, if tomorrow is Tuesday?" => "Monday",
20
+ "What is the 2nd letter of the the third word in this question?" => "h",
21
+ "What color is the sky on a sunny day?" => "blue" }.freeze
22
+
23
+ @@questions_and_answers = DEFAULT_QUESTIONS_AND_ANSWERS
24
+
25
+ class << self
26
+ # Returns the current captcha questions/answers hash. Defaults to
27
+ # DEFAULT_QUESTIONS_AND_ANSWERS.
28
+ def questions_and_answers
29
+ @@questions_and_answers
30
+ end
31
+
32
+ # Sets the current captcha questions/answers hash. Used to set a
33
+ # custom questions/answers hash.
34
+ def questions_and_answers=(qna)
35
+ @@questions_and_answers = qna
36
+ end
37
+ end
38
+
39
+ # This method is the one called by Rack.
40
+ #
41
+ # It returns HTTP status 404 if the path is not recognized. If the path is
42
+ # recognized, it returns HTTP status 200 and delivers a new challenge in
43
+ # JSON format.
44
+ #
45
+ # Please take a look at the source code if you want to learn more.
46
+ def call(env)
47
+ if env['PATH_INFO'] == regenerate_path
48
+ captcha_challenge = generate_challenge
49
+ json = { :captcha_challenge => captcha_challenge }.to_json
50
+
51
+ [200, { 'Content-Type' => 'application/json' }, [json]]
52
+ else
53
+ [404, { 'Content-Type' => 'text/html' }, ['Not Found']]
54
+ end
55
+ end
56
+
57
+ # Returns a captcha question.
58
+ def generate_challenge
59
+ self.class.questions_and_answers.keys[rand(self.class.questions_and_answers.keys.size)]
60
+ end
61
+
62
+ # Returns true if the captcha was solved using the given +question+ and +answer+,
63
+ # otherwise false.
64
+ def solved?(question, answer)
65
+ return false unless self.class.questions_and_answers.key?(question)
66
+ answers = Array.wrap(self.class.questions_and_answers[question]).map(&:downcase)
67
+ answers.include?(answer.downcase)
68
+ end
69
+
70
+ # Returns a span tag with the inner HTML set to the captcha question.
71
+ #
72
+ # Internally calls Rails' +content_tag+ helper method, passing the +options+ argument.
73
+ def render_challenge(sanitized_object_name, object, options = {})
74
+ options[:id] = "#{sanitized_object_name}_captcha_question"
75
+
76
+ content_tag :span, object.captcha_challenge, options
77
+ end
78
+
79
+ # Returns an anchor tag that makes an AJAX request to fetch a new question and updates
80
+ # the captcha challenge after the request is complete.
81
+ #
82
+ # Internally calls Rails' +link_to_remote+ helper method, passing the +options+ and
83
+ # +html_options+ arguments. So it relies on the Prototype javascript framework
84
+ # to be available on the web page.
85
+ #
86
+ # The anchor text defaults to 'New question'. You can set this to a custom value
87
+ # providing a +:text+ key in the +options+ hash.
88
+ def render_regenerate_challenge_link(sanitized_object_name, object, options = {}, html_options = {})
89
+ text = options.delete(:text) || 'New question'
90
+ success = "var result = request.responseJSON; $('#{sanitized_object_name}_captcha_question').update(result.captcha_challenge); $('#{sanitized_object_name}_captcha_challenge').value = result.captcha_challenge; $('#{sanitized_object_name}_captcha_solution').value = '';"
91
+
92
+ link_to_remote text, options.reverse_merge(:url => regenerate_path, :method => :get, :success => success), html_options
93
+ end
94
+
95
+ private
96
+ def regenerate_path #:nodoc:
97
+ '/captchas/regenerate'
98
+ end
99
+
100
+ # This is needed by +link_to_remote+ called in +render_regenerate_link+.
101
+ def protect_against_forgery? #:nodoc:
102
+ false
103
+ end
104
+
105
+ def solve(challenge) #:nodoc:
106
+ Array.wrap(self.class.questions_and_answers[challenge]).first
107
+ end
108
+ end
109
+ end
110
+ end
@@ -8,7 +8,7 @@ module ValidatesCaptcha
8
8
  #
9
9
  # You can implement your own reversible encrypter by creating a class
10
10
  # that conforms to the method definitions of the example below and
11
- # assign an instance of it to ValidatesCaptcha#reversible_encrypter=.
11
+ # assign an instance of it to ValidatesCaptcha::Provider::Image#reversible_encrypter=.
12
12
  #
13
13
  # Example for a custom encrypter/decrypter:
14
14
  #
@@ -24,7 +24,8 @@ module ValidatesCaptcha
24
24
  # end
25
25
  # end
26
26
  #
27
- # ValidatesCaptcha.reversible_encrypter = ReverseString.new
27
+ # ValidatesCaptcha::Provider::Image.reversible_encrypter = ReverseString.new
28
+ # ValidatesCaptcha.provider = ValidatesCaptcha::Provider::Image.new
28
29
  #
29
30
  # Please note: The #decrypt method should return +nil+ if decryption fails.
30
31
  class Simple