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,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
@@ -0,0 +1,224 @@
1
+ require 'digest/sha1'
2
+ require 'action_view/helpers'
3
+
4
+ module ValidatesCaptcha
5
+ module Provider
6
+ # An image captcha provider that relies on pre-created captcha images.
7
+ #
8
+ # There is a Rake tast for creating the captcha images:
9
+ #
10
+ # rake validates_captcha:create_static_images
11
+ #
12
+ # This will create 3 images in #filesystem_dir. To create a
13
+ # different number of images, provide a COUNT argument:
14
+ #
15
+ # rake validates_captcha:create_static_images COUNT=50
16
+ #
17
+ # This class contains the getters and setters for the backend classes:
18
+ # image generator and string generator. This allows you to replace them
19
+ # with your custom implementations. For more information on how to bring
20
+ # the image provider to use your own implementation instead of the default
21
+ # one, consult the documentation for the specific default class.
22
+ #
23
+ # The default captcha image generator uses ImageMagick's +convert+ command to
24
+ # create the captcha. So a recent and properly configured version of ImageMagick
25
+ # must be installed on the system. The version used while developing was 6.4.5.
26
+ # But you are not bound to ImageMagick. If you want to provide a custom image
27
+ # generator, take a look at the documentation for
28
+ # ValidatesCaptcha::ImageGenerator::Simple on how to create your own.
29
+ class StaticImage
30
+ include ActionView::Helpers
31
+
32
+ SALT = "3f(61&831_fa0712d4a?b58-eb4b8$a2%.36378f".freeze
33
+
34
+ @@string_generator = nil
35
+ @@image_generator = nil
36
+ @@filesystem_dir = nil
37
+ @@web_dir = nil
38
+ @@salt = nil
39
+
40
+ class << self
41
+ # Returns the current captcha string generator. Defaults to an
42
+ # instance of the ValidatesCaptcha::StringGenerator::Simple class.
43
+ def string_generator
44
+ @@string_generator ||= ValidatesCaptcha::StringGenerator::Simple.new
45
+ end
46
+
47
+ # Sets the current captcha string generator. Used to set a
48
+ # custom string generator.
49
+ def string_generator=(generator)
50
+ @@string_generator = generator
51
+ end
52
+
53
+ # Returns the current captcha image generator. Defaults to an
54
+ # instance of the ValidatesCaptcha::ImageGenerator::Simple class.
55
+ def image_generator
56
+ @@image_generator ||= ValidatesCaptcha::ImageGenerator::Simple.new
57
+ end
58
+
59
+ # Sets the current captcha image generator. Used to set a custom
60
+ # image generator.
61
+ def image_generator=(generator)
62
+ @@image_generator = generator
63
+ end
64
+
65
+ # Returns the current captcha image file system directory. Defaults to
66
+ # +RAILS_ROOT/public/images/captchas+.
67
+ def filesystem_dir
68
+ @@filesystem_dir ||= ::File.join(::Rails.public_path, 'images', 'captchas')
69
+ end
70
+
71
+ # Sets the current captcha image file system directory. Used to set a custom
72
+ # image directory.
73
+ def filesystem_dir=(dir)
74
+ @@filesystem_dir = dir
75
+ end
76
+
77
+ # Returns the current captcha image web directory. Defaults to
78
+ # +/images/captchas+.
79
+ def web_dir
80
+ @@web_dir ||= '/images/captchas'
81
+ end
82
+
83
+ # Sets the current captcha image web directory. Used to set a custom
84
+ # image directory.
85
+ def web_dir=(dir)
86
+ @@web_dir = dir
87
+ end
88
+
89
+ # Returns the current salt used for encryption.
90
+ def salt
91
+ @@salt ||= SALT
92
+ end
93
+
94
+ # Sets the current salt used for encryption. Used to set a custom
95
+ # salt.
96
+ def salt=(salt)
97
+ @@salt = salt
98
+ end
99
+
100
+ # Return the encryption of the +code+ using #salt.
101
+ def encrypt(code)
102
+ ::Digest::SHA1.hexdigest "#{salt}--#{code}"
103
+ end
104
+
105
+ # Creates a captcha image in the #filesystem_dir and returns
106
+ # the path to it and the code displayed on the image.
107
+ def create_image
108
+ code = string_generator.generate
109
+ encrypted_code = encrypt(code)
110
+
111
+ image_filename = "#{encrypted_code}#{image_generator.file_extension}"
112
+ image_path = File.join(filesystem_dir, image_filename)
113
+ image_bytes = image_generator.generate(code)
114
+
115
+ File.open image_path, 'w' do |os|
116
+ os.write image_bytes
117
+ end
118
+
119
+ return image_path, code
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 a new challenge in
127
+ # JSON format.
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'] == regenerate_path
132
+ captcha_challenge = generate_challenge
133
+ json = { :captcha_challenge => captcha_challenge, :captcha_image_path => image_path(captcha_challenge) }.to_json
134
+
135
+ [200, { 'Content-Type' => 'application/json' }, [json]]
136
+ else
137
+ [404, { 'Content-Type' => 'text/html' }, ['Not Found']]
138
+ end
139
+ end
140
+
141
+ # Returns an array containing the paths to the available captcha images.
142
+ def images
143
+ @images ||= Dir[File.join(filesystem_dir, "*#{image_file_extension}")]
144
+ end
145
+
146
+ # Returns an array containing the available challenges (encrypted captcha codes).
147
+ def challenges
148
+ @challenges ||= images.map { |path| File.basename(path, image_file_extension) }
149
+ end
150
+
151
+ # Returns a captcha challenge.
152
+ def generate_challenge
153
+ raise("no captcha images found in #{filesystem_dir}") if challenges.empty?
154
+
155
+ challenges[rand(challenges.size)]
156
+ end
157
+
158
+ # Returns true if the captcha was solved using the given +challenge+ and +solution+,
159
+ # otherwise false.
160
+ def solved?(challenge, solution)
161
+ challenge == encrypt(solution)
162
+ end
163
+
164
+ # Returns an image tag with the source set to the url of the captcha image.
165
+ #
166
+ # Internally calls Rails' +image_tag+ helper method, passing the +options+ argument.
167
+ def render_challenge(sanitized_object_name, object, options = {})
168
+ src = image_path(object.captcha_challenge)
169
+
170
+ options[:alt] ||= 'CAPTCHA'
171
+ options[:id] = "#{sanitized_object_name}_captcha_image"
172
+
173
+ image_tag src, options
174
+ end
175
+
176
+ # Returns an anchor tag that makes an AJAX request to fetch a new captcha code and updates
177
+ # the captcha image after the request is complete.
178
+ #
179
+ # Internally calls Rails' +link_to_remote+ helper method, passing the +options+ and
180
+ # +html_options+ arguments. So it relies on the Prototype javascript framework
181
+ # to be available on the web page.
182
+ #
183
+ # The anchor text defaults to 'Regenerate Captcha'. You can set this to a custom value
184
+ # providing a +:text+ key in the +options+ hash.
185
+ def render_regenerate_challenge_link(sanitized_object_name, object, options = {}, html_options = {})
186
+ text = options.delete(:text) || 'Regenerate Captcha'
187
+ 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 = '';"
188
+
189
+ link_to_remote text, options.reverse_merge(:url => regenerate_path, :method => :get, :success => success), html_options
190
+ end
191
+
192
+ private
193
+ def regenerate_path #:nodoc:
194
+ '/captchas/regenerate'
195
+ end
196
+
197
+ def image_file_extension #:nodoc:
198
+ self.class.image_generator.file_extension
199
+ end
200
+
201
+ def image_path(encrypted_code) #:nodoc:
202
+ File.join(web_dir, "#{encrypted_code}#{image_file_extension}")
203
+ end
204
+
205
+ def encrypt(code) #:nodoc:
206
+ self.class.encrypt code
207
+ end
208
+
209
+ def filesystem_dir #:nodoc:
210
+ self.class.filesystem_dir
211
+ end
212
+
213
+ def web_dir #:nodoc:
214
+ self.class.web_dir
215
+ end
216
+
217
+ # This is needed by +link_to_remote+ called in +render_regenerate_link+.
218
+ def protect_against_forgery? #:nodoc:
219
+ false
220
+ end
221
+ end
222
+ end
223
+ end
224
+
@@ -0,0 +1,81 @@
1
+ module ValidatesCaptcha
2
+ module StringGenerator
3
+ # This class is responsible for generating the codes that are displayed
4
+ # on the captcha images. It does so by randomly selecting a number of
5
+ # characters from a predefined alphabet constisting of visually distinguishable
6
+ # letters and digits.
7
+ #
8
+ # The number of characters and the alphabet used when generating strings can
9
+ # be customized. See the #alphabet= and #length= methods for details.
10
+ #
11
+ # You can implement your own string generator by creating a
12
+ # class that conforms to the method definitions of the example below and
13
+ # assign an instance of it to
14
+ # ValidatesCaptcha::Provider::DynamicImage#string_generator=.
15
+ #
16
+ # Example for a custom string generator:
17
+ #
18
+ # class DictionaryGenerator
19
+ # DICTIONARY = ['foo', 'bar', 'baz', ...]
20
+ #
21
+ # def generate
22
+ # return DICTIONARY[rand(DICTIONARY.size)]
23
+ # end
24
+ # end
25
+ #
26
+ # ValidatesCaptcha::Provider::DynamicImage.string_generator = DictionaryGenerator.new
27
+ # ValidatesCaptcha.provider = ValidatesCaptcha::Provider::DynamicImage.new
28
+ #
29
+ # You can also assign it to ValidatesCaptcha::Provider::StaticImage#string_generator=.
30
+ #
31
+ class Simple
32
+ @@alphabet = 'abdefghjkmnqrtABDEFGHJKLMNQRT234678923467892346789'
33
+ @@length = 6
34
+
35
+ class << self
36
+ # Returns a string holding the chars used when randomly generating the text that
37
+ # is displayed on a captcha image. Defaults to a string of visually distinguishable
38
+ # letters and digits.
39
+ def alphabet
40
+ @@alphabet
41
+ end
42
+
43
+ # Sets the string to use as alphabet when randomly generating the text displayed
44
+ # on a captcha image. To increase the probability of appearing in the image, some
45
+ # characters might appear more than once in the string.
46
+ #
47
+ # You can set this to a custom alphabet within a Rails initializer:
48
+ #
49
+ # ValidatesCaptcha::StringGenerator::Simple.alphabet = '01'
50
+ def alphabet=(alphabet)
51
+ alphabet = alphabet.to_s.gsub(/\s/, '')
52
+ raise('alphabet cannot be blank') if alphabet.blank?
53
+
54
+ @@alphabet = alphabet
55
+ end
56
+
57
+ # Returns the length to use when generating captcha codes. Defaults to 6.
58
+ def length
59
+ @@length
60
+ end
61
+
62
+ # Sets the length to use when generating captcha codes.
63
+ #
64
+ # You can set this to a custom length within a Rails initializer:
65
+ #
66
+ # ValidatesCaptcha::StringGenerator::Simple.length = 8
67
+ def length=(length)
68
+ @@length = length.to_i
69
+ end
70
+ end
71
+
72
+ # Randomly generates a string to be used as the code displayed on captcha images.
73
+ def generate
74
+ alphabet_chars = self.class.alphabet.split(//)
75
+ code_chars = []
76
+ self.class.length.times { code_chars << alphabet_chars[rand(alphabet_chars.size)] }
77
+ code_chars.join
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,52 @@
1
+ require 'active_support'
2
+
3
+ module ValidatesCaptcha
4
+ module SymmetricEncryptor
5
+ # This class is responsible for encrypting and decrypting captcha codes.
6
+ # It internally uses ActiveSupport's MessageEncryptor to do the string
7
+ # encryption/decryption.
8
+ #
9
+ # You can implement your own symmetric encryptor by creating a class
10
+ # that conforms to the method definitions of the example below and
11
+ # assign an instance of it to
12
+ # ValidatesCaptcha::Provider::DynamicImage#symmetric_encryptor=.
13
+ #
14
+ # Example for a custom symmetric encryptor:
15
+ #
16
+ # class ReverseString # very insecure and easily cracked
17
+ # def encrypt(code)
18
+ # code.reverse
19
+ # end
20
+ #
21
+ # def decrypt(encrypted_code)
22
+ # encrypted_code.reverse
23
+ # rescue SomeKindOfDecryptionError
24
+ # nil
25
+ # end
26
+ # end
27
+ #
28
+ # ValidatesCaptcha::Provider::DynamicImage.symmetric_encryptor = ReverseString.new
29
+ # ValidatesCaptcha.provider = ValidatesCaptcha::Provider::DynamicImage.new
30
+ #
31
+ # Please note: The #decrypt method should return +nil+ if decryption fails.
32
+ class Simple
33
+ KEY = ::ActiveSupport::SecureRandom.hex(64).freeze
34
+
35
+ def initialize #:nodoc:
36
+ @symmetric_encryptor = ::ActiveSupport::MessageEncryptor.new(KEY)
37
+ end
38
+
39
+ # Encrypts a cleartext string.
40
+ def encrypt(code)
41
+ @symmetric_encryptor.encrypt(code).gsub('+', '%2B').gsub('/', '%2F')
42
+ end
43
+
44
+ # Decrypts an encrypted string.
45
+ def decrypt(encrypted_code)
46
+ @symmetric_encryptor.decrypt encrypted_code.gsub('%2F', '/').gsub('%2B', '+')
47
+ rescue ::ActiveSupport::MessageEncryptor::InvalidMessage
48
+ nil
49
+ end
50
+ end
51
+ end
52
+ end