validates_captcha 0.9.2 → 0.9.3

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