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,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