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.
- data/CHANGELOG.rdoc +6 -1
- data/README.rdoc +85 -49
- data/lib/validates_captcha.rb +18 -113
- data/lib/validates_captcha/form_builder.rb +4 -4
- data/lib/validates_captcha/form_helper.rb +13 -25
- data/lib/validates_captcha/image_generator/simple.rb +7 -6
- data/lib/validates_captcha/model_validation.rb +9 -9
- data/lib/validates_captcha/provider/image.rb +244 -0
- data/lib/validates_captcha/provider/question.rb +110 -0
- data/lib/validates_captcha/reversible_encrypter/simple.rb +3 -2
- data/lib/validates_captcha/string_generator/simple.rb +3 -2
- data/lib/validates_captcha/version.rb +1 -1
- data/rails/init.rb +1 -1
- data/test/cases/controller_validation_test.rb +79 -22
- data/test/cases/{image_generator_test.rb → image_generator/simple_test.rb} +11 -10
- data/test/cases/model_validation_test.rb +131 -58
- data/test/cases/provider/image_test.rb +103 -0
- data/test/cases/provider/question_test.rb +41 -0
- data/test/cases/{reversible_encrypter_test.rb → reversible_encrypter/simple_test.rb} +3 -2
- data/test/cases/{string_generator_test.rb → string_generator/simple_test.rb} +1 -0
- data/test/cases/validates_captcha_test.rb +9 -116
- metadata +17 -14
- data/lib/validates_captcha/middleware/simple.rb +0 -108
- data/test/cases/middleware_test.rb +0 -71
@@ -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
|
34
|
+
# def mime_type
|
35
35
|
# 'image/png'
|
36
36
|
# end
|
37
37
|
#
|
38
|
-
# def
|
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
|
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
|
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 :
|
9
|
-
attr_accessor :
|
10
|
-
attr_writer :
|
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
|
46
|
-
return @
|
47
|
-
@
|
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(:
|
57
|
-
errors.add(:
|
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.
|
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
|