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.
- 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
data/CHANGELOG.rdoc
CHANGED
data/README.rdoc
CHANGED
@@ -1,12 +1,15 @@
|
|
1
1
|
= Validates Captcha
|
2
2
|
|
3
|
-
|
3
|
+
A captcha verification approach for Rails apps, directly integrated into
|
4
4
|
ActiveRecord's validation mechanism and providing helpers for ActionController
|
5
5
|
and ActionView.
|
6
6
|
|
7
7
|
RDoc documentation (including this README as start page) can be found at
|
8
8
|
http://m4n.github.com/validates_captcha
|
9
9
|
|
10
|
+
Validates Captcha uses question/answer captchas by default. But you can
|
11
|
+
also use the built-in image captcha provider -- or implement your own.
|
12
|
+
|
10
13
|
|
11
14
|
|
12
15
|
== Basic Usage
|
@@ -15,7 +18,7 @@ Validates Captcha extends ActiveRecord, ActionController and ActionView with
|
|
15
18
|
helper methods that make it a snap to integrate captcha verification in your
|
16
19
|
Rails application.
|
17
20
|
|
18
|
-
Step #1
|
21
|
+
<b>Step #1:</b> Extend the form of your view with the necessary captcha display
|
19
22
|
and input logic.
|
20
23
|
|
21
24
|
# app/views/comments/new.html.erb
|
@@ -32,8 +35,8 @@ and input logic.
|
|
32
35
|
<!-- now something new: -->
|
33
36
|
<p>
|
34
37
|
<%= f.label :captcha %><br />
|
35
|
-
<%= f.
|
36
|
-
<%= f.captcha_field %>
|
38
|
+
<%= f.captcha_challenge # displays the question or image %>
|
39
|
+
<%= f.captcha_field # displays the input field %>
|
37
40
|
</p>
|
38
41
|
|
39
42
|
<p>
|
@@ -41,7 +44,7 @@ and input logic.
|
|
41
44
|
</p>
|
42
45
|
<% end %>
|
43
46
|
|
44
|
-
Step #2
|
47
|
+
<b>Step #2:</b> Tell the controller that you want to validate
|
45
48
|
captchas.
|
46
49
|
|
47
50
|
class CommentsController < ApplicationController
|
@@ -57,11 +60,11 @@ captchas.
|
|
57
60
|
This activates captcha validation in every action of the controller
|
58
61
|
whenever an instance of class +Comment+ is saved.
|
59
62
|
|
60
|
-
Step #3
|
63
|
+
<b>Step #3:</b> There's no step three!
|
61
64
|
|
62
65
|
To summarize: Put the following in your view.
|
63
66
|
|
64
|
-
<%= f.
|
67
|
+
<%= f.captcha_challenge %>
|
65
68
|
<%= f.captcha_field %>
|
66
69
|
|
67
70
|
And what you see below in the corresponding controller.
|
@@ -91,48 +94,39 @@ You can customize the validated class using the +validates_captcha_of+ method.
|
|
91
94
|
end
|
92
95
|
|
93
96
|
Two kinds of errors are added to the model if captcha validation fails:
|
94
|
-
+:blank+ if no captcha
|
95
|
-
is submitted but does not
|
96
|
-
|
97
|
-
|
97
|
+
+:blank+ if no captcha solution is submitted and +:invalid+ if a captcha
|
98
|
+
solution is submitted but does not solve the captcha's challenge. You can
|
99
|
+
localize the error messages for the captcha as you usually do for the
|
100
|
+
other attributes.
|
98
101
|
|
99
102
|
models:
|
100
103
|
comment:
|
101
104
|
attributes:
|
102
|
-
|
105
|
+
captcha_solution:
|
103
106
|
blank: 'must not be empty'
|
104
107
|
invalid: 'does not match the code displayed on the image'
|
105
108
|
|
106
|
-
What if the captcha's text is unreadable
|
107
|
-
|
109
|
+
What if the image captcha's text is unreadable or a user does not know the
|
110
|
+
correct answer to the captcha question? There's also a form helper method
|
111
|
+
for captcha regeneration available. You can call it like this.
|
108
112
|
|
109
113
|
<p>
|
110
|
-
|
114
|
+
Don't know the answer? <%= f.regenerate_captcha_challenge_link %>
|
111
115
|
</p>
|
112
116
|
|
113
|
-
This generates an anchor tag that, when clicked, generates a new
|
114
|
-
|
115
|
-
new
|
117
|
+
This generates an anchor tag that, when clicked, generates a new captcha
|
118
|
+
challenge and updates the display. It makes an AJAX request to fetch a
|
119
|
+
new challenge and updates the question/image after the request is complete.
|
116
120
|
|
117
|
-
+
|
118
|
-
method. So it relies on the Prototype javascript framework to be
|
119
|
-
on the page.
|
121
|
+
+regenerate_captcha_challenge_link+ internally calls Rails' +link_to_remote+
|
122
|
+
helper method. So it relies on the Prototype javascript framework to be
|
123
|
+
available on the page.
|
120
124
|
|
121
|
-
The anchor's text defaults to '
|
125
|
+
The anchor's text defaults to 'New question' (question challenge) and
|
126
|
+
'Regenerate Captcha' (image challenge) respectively. You can set this to
|
122
127
|
a custom value by providing a +:text+ key in the options hash.
|
123
128
|
|
124
|
-
<%= f.
|
125
|
-
|
126
|
-
By default, captchas have a length of 6 characters and the text displayed
|
127
|
-
on the captcha image is created by randomly selecting characters from a
|
128
|
-
predefined alphabet constisting of visually distinguishable letters and digits.
|
129
|
-
|
130
|
-
The number of characters and the alphabet used when generating strings can
|
131
|
-
be customized. Just put the following in a Rails initializer and adjust the
|
132
|
-
values to your needs.
|
133
|
-
|
134
|
-
ValidatesCaptcha::StringGenerator::Simple.alphabet = '01'
|
135
|
-
ValidatesCaptcha::StringGenerator::Simple.length = 8
|
129
|
+
<%= f.regenerate_captcha_challenge_link :text => 'Another captcha, please' %>
|
136
130
|
|
137
131
|
Apart from controllers, you can activate captcha validation for a model
|
138
132
|
using the class level +with_captcha_validation+ method added to
|
@@ -146,28 +140,67 @@ ActiveRecord::Base.
|
|
146
140
|
This activates captcha validation on entering the block and deactivates it
|
147
141
|
on leaving the block.
|
148
142
|
|
149
|
-
Two new attribute
|
150
|
-
+
|
151
|
-
initialized to a randomly generated
|
152
|
-
|
143
|
+
Two new attribute-like methods are added to ActiveRecord: +captcha_challenge+
|
144
|
+
and +captcha_solution+. Those are made +attr_accessible+. The former is
|
145
|
+
initialized to a randomly generated captcha challenge on instantiation.
|
146
|
+
|
147
|
+
For a record to be valid, the value assigned to +captcha_solution=+ must
|
148
|
+
solve the return value of +captcha_challenge+. Within a +with_captcha_validation+
|
149
|
+
block, calling +valid?+ (as is done by +save+, +update_attributes+, etc.)
|
150
|
+
will also validate the value of +captcha_solution+ against +captcha_challenge+.
|
151
|
+
Outside +with_captcha_validation+, no captcha validation is performed.
|
152
|
+
|
153
|
+
|
154
|
+
|
155
|
+
== Question/answer challange captchas (default provider)
|
156
|
+
|
157
|
+
You can set the captcha provider to use question/answer challenges with
|
158
|
+
the code below. It is best to put this in a Rails initializer.
|
159
|
+
|
160
|
+
ValidatesCaptcha.provider = ValidatesCaptcha::Provider::Question.new # this is the default
|
161
|
+
|
162
|
+
If you want to replace the few default questions and answers, here's how
|
163
|
+
to do it.
|
153
164
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
165
|
+
ValidatesCaptcha::Provider::Question.questions_and_answers = {
|
166
|
+
"What's the opposite of bad?" => "good",
|
167
|
+
"What are the initials of the creator of Rails?" => "DHH",
|
168
|
+
"What's the sum of 3 and four?" => ["7", "seven"],
|
169
|
+
... }
|
170
|
+
|
171
|
+
|
172
|
+
|
173
|
+
== Image challenge captchas
|
174
|
+
|
175
|
+
You can set the captcha provider to use image challenges with
|
176
|
+
the code below. It is best to put this in a Rails initializer.
|
177
|
+
|
178
|
+
ValidatesCaptcha.provider = ValidatesCaptcha::Provider::Image.new
|
179
|
+
|
180
|
+
By default, image captchas have a length of 6 characters and the text displayed
|
181
|
+
on the captcha image is created by randomly selecting characters from a
|
182
|
+
predefined alphabet constisting of visually distinguishable letters and digits.
|
183
|
+
|
184
|
+
The number of characters and the alphabet used when generating strings can
|
185
|
+
be customized. Just put the following in a Rails initializer and adjust the
|
186
|
+
values to your needs.
|
187
|
+
|
188
|
+
ValidatesCaptcha::StringGenerator::Simple.alphabet = '01'
|
189
|
+
ValidatesCaptcha::StringGenerator::Simple.length = 8
|
160
190
|
|
161
191
|
|
162
192
|
|
163
193
|
== Extensibility
|
164
194
|
|
195
|
+
Don't like the built-in challenges? It's easy to extend them or to implement
|
196
|
+
your own.
|
197
|
+
|
165
198
|
Validates Captcha delegates tasks like string and image generation,
|
166
199
|
encryption/decryption of captcha codes, and responding to captcha requests
|
167
200
|
to dedicated backend classes.
|
168
201
|
|
169
|
-
Those classes can easily be replaced
|
170
|
-
you can achieve stronger encryption, can use a word list as captcha text
|
202
|
+
Those classes can easily be replaced with your custom implementations. So
|
203
|
+
you can achieve stronger encryption, can use a word list as image captcha text
|
171
204
|
generation source, or can replace the captcha image generator with one
|
172
205
|
that creates images that are harder to crack.
|
173
206
|
|
@@ -176,7 +209,10 @@ Please see the documentation of the following classes for further information.
|
|
176
209
|
* ValidatesCaptcha::StringGenerator::Simple
|
177
210
|
* ValidatesCaptcha::ReversibleEncrypter::Simple
|
178
211
|
* ValidatesCaptcha::ImageGenerator::Simple
|
179
|
-
|
212
|
+
|
213
|
+
Or you can implement a custom captcha challenge provider and assign it to
|
214
|
+
ValidatesCaptcha#provider=. See the documentation on ValidatesCaptcha::Provider
|
215
|
+
for an example.
|
180
216
|
|
181
217
|
|
182
218
|
|
@@ -185,8 +221,8 @@ Please see the documentation of the following classes for further information.
|
|
185
221
|
Using a Rack middleware to speed up the request/response cycle when fetching
|
186
222
|
captcha images, Validates Captcha requires Rails version 2.3 or greater.
|
187
223
|
|
188
|
-
The
|
189
|
-
|
224
|
+
The image captcha provider uses ImageMagick's +convert+ command to create
|
225
|
+
the captcha. So a recent and properly configured version of ImageMagick
|
190
226
|
must be installed on the system. The version used while developing was 6.4.5.
|
191
227
|
But you are not bound to ImageMagick. If you want to provide a custom image
|
192
228
|
generator, take a look at the documentation for
|
data/lib/validates_captcha.rb
CHANGED
@@ -22,17 +22,11 @@
|
|
22
22
|
#++
|
23
23
|
|
24
24
|
|
25
|
-
# This module contains the
|
26
|
-
#
|
27
|
-
# allows you to replace them with your custom implementations. For more
|
25
|
+
# This module contains the getter and setter for the captcha provider.
|
26
|
+
# This allows you to replace it with your custom implementation. For more
|
28
27
|
# information on how to bring Validates Captcha to use your own
|
29
28
|
# implementation instead of the default one, consult the documentation
|
30
|
-
# for the
|
31
|
-
#
|
32
|
-
# This module also contains convenience wrapper methods for all the
|
33
|
-
# methods provided by the configured backend classes. These wrapper
|
34
|
-
# methods form the API that is visible to the outside world and that
|
35
|
-
# all backend classes use for internal communication.
|
29
|
+
# for the default provider.
|
36
30
|
module ValidatesCaptcha
|
37
31
|
autoload :ModelValidation, 'validates_captcha/model_validation'
|
38
32
|
autoload :ControllerValidation, 'validates_captcha/controller_validation'
|
@@ -41,8 +35,9 @@ module ValidatesCaptcha
|
|
41
35
|
autoload :TestCase, 'validates_captcha/test_case'
|
42
36
|
autoload :VERSION, 'validates_captcha/version'
|
43
37
|
|
44
|
-
module
|
45
|
-
autoload :
|
38
|
+
module Provider
|
39
|
+
autoload :Question, 'validates_captcha/provider/question'
|
40
|
+
autoload :Image, 'validates_captcha/provider/image'
|
46
41
|
end
|
47
42
|
|
48
43
|
module StringGenerator
|
@@ -53,117 +48,27 @@ module ValidatesCaptcha
|
|
53
48
|
autoload :Simple, 'validates_captcha/reversible_encrypter/simple'
|
54
49
|
end
|
55
50
|
|
56
|
-
module
|
57
|
-
autoload :Simple, 'validates_captcha/
|
58
|
-
end
|
51
|
+
module ImageGenerator
|
52
|
+
autoload :Simple, 'validates_captcha/image_generator/simple'
|
53
|
+
end
|
59
54
|
|
60
|
-
@@
|
61
|
-
@@string_generator = nil
|
62
|
-
@@reversible_encrypter = nil
|
63
|
-
@@middleware = nil
|
55
|
+
@@provider = nil
|
64
56
|
|
65
57
|
class << self
|
66
|
-
# Returns
|
58
|
+
# Returns Validates Captcha's current version number.
|
67
59
|
def version
|
68
60
|
ValidatesCaptcha::VERSION::STRING
|
69
61
|
end
|
70
62
|
|
71
|
-
# Returns the current captcha
|
72
|
-
#
|
73
|
-
def
|
74
|
-
@@
|
75
|
-
end
|
76
|
-
|
77
|
-
# Sets the current captcha image generator. Used to set a custom
|
78
|
-
# image generator.
|
79
|
-
def image_generator=(generator)
|
80
|
-
@@image_generator = generator
|
81
|
-
end
|
82
|
-
|
83
|
-
# Returns the current captcha string generator. Defaults to an
|
84
|
-
# instance of the ValidatesCaptcha::StringGenerator::Simple class.
|
85
|
-
def string_generator
|
86
|
-
@@string_generator ||= StringGenerator::Simple.new
|
87
|
-
end
|
88
|
-
|
89
|
-
# Sets the current captcha string generator. Used to set a
|
90
|
-
# custom string generator.
|
91
|
-
def string_generator=(generator)
|
92
|
-
@@string_generator = generator
|
93
|
-
end
|
94
|
-
|
95
|
-
# Returns the current captcha reversible encrypter. Defaults to an
|
96
|
-
# instance of the ValidatesCaptcha::ReversibleEncrypter::Simple class.
|
97
|
-
def reversible_encrypter
|
98
|
-
@@reversible_encrypter ||= ReversibleEncrypter::Simple.new
|
99
|
-
end
|
100
|
-
|
101
|
-
# Sets the current captcha reversible encrypter. Used to set a
|
102
|
-
# custom reversible encrypter.
|
103
|
-
def reversible_encrypter=(encrypter)
|
104
|
-
@@reversible_encrypter = encrypter
|
105
|
-
end
|
106
|
-
|
107
|
-
# Returns the current captcha middleware. Defaults to the
|
108
|
-
# ValidatesCaptcha::Middleware::Simple class.
|
109
|
-
def middleware
|
110
|
-
@@middleware ||= Middleware::Simple.new
|
63
|
+
# Returns the current captcha challenge provider. Defaults to an instance of
|
64
|
+
# the ValidatesCaptcha::Provider::Question class.
|
65
|
+
def provider
|
66
|
+
@@provider ||= Provider::Question.new
|
111
67
|
end
|
112
68
|
|
113
|
-
# Sets the current captcha
|
114
|
-
|
115
|
-
|
116
|
-
@@middleware = middleware
|
117
|
-
end
|
118
|
-
|
119
|
-
# Randomly generates a string which can be used as the code
|
120
|
-
# displayed on captcha images. This method internally calls
|
121
|
-
# +string_generator.generate+.
|
122
|
-
def generate_captcha_code
|
123
|
-
string_generator.generate
|
124
|
-
end
|
125
|
-
|
126
|
-
# Returns the image data of the generated captcha image. This
|
127
|
-
# method internally calls +image_generator.generate+.
|
128
|
-
def generate_captcha_image(code)
|
129
|
-
image_generator.generate(code)
|
130
|
-
end
|
131
|
-
|
132
|
-
# Returns the image file extension of the captcha images. This
|
133
|
-
# method internally calls +image_generator.image_file_extension+.
|
134
|
-
def captcha_image_file_extension
|
135
|
-
image_generator.image_file_extension
|
136
|
-
end
|
137
|
-
|
138
|
-
# Returns the image mime type of the captcha images. This
|
139
|
-
# method internally calls +image_generator.image_mime_type+.
|
140
|
-
def captcha_image_mime_type
|
141
|
-
image_generator.image_mime_type
|
142
|
-
end
|
143
|
-
|
144
|
-
# Returns the encryption of a cleartext captcha code. This
|
145
|
-
# method internally calls +reversible_encrypter.encrypt+.
|
146
|
-
def encrypt_captcha_code(code)
|
147
|
-
reversible_encrypter.encrypt(code)
|
148
|
-
end
|
149
|
-
|
150
|
-
# Returns the decryption of an encrypted captcha code. This
|
151
|
-
# method internally calls +reversible_encrypter.decrypt+.
|
152
|
-
def decrypt_captcha_code(encrypted_code)
|
153
|
-
reversible_encrypter.decrypt(encrypted_code)
|
154
|
-
end
|
155
|
-
|
156
|
-
# Returns the captcha image path for a given encrypted code. This
|
157
|
-
# method internally calls +middleware.image_path+.
|
158
|
-
def captcha_image_path(encrypted_code)
|
159
|
-
middleware.image_path(encrypted_code)
|
160
|
-
end
|
161
|
-
|
162
|
-
# Returns the path that is used when requesting the regeneration
|
163
|
-
# of a captcha image. This method internally calls
|
164
|
-
# +middleware.regenerate_path+.
|
165
|
-
def regenerate_captcha_path
|
166
|
-
middleware.regenerate_path
|
69
|
+
# Sets the current captcha challenge provider. Used to set a custom provider.
|
70
|
+
def provider=(provider)
|
71
|
+
@@provider = provider
|
167
72
|
end
|
168
73
|
end
|
169
74
|
end
|
@@ -1,15 +1,15 @@
|
|
1
1
|
module ValidatesCaptcha
|
2
2
|
module FormBuilder #:nodoc:
|
3
|
-
def
|
4
|
-
@template.
|
3
|
+
def captcha_challenge(options = {}) #:nodoc:
|
4
|
+
@template.captcha_challenge @object_name, options.merge(:object => @object)
|
5
5
|
end
|
6
6
|
|
7
7
|
def captcha_field(options = {}) #:nodoc:
|
8
8
|
@template.captcha_field @object_name, options.merge(:object => @object)
|
9
9
|
end
|
10
10
|
|
11
|
-
def
|
12
|
-
@template.
|
11
|
+
def regenerate_captcha_challenge_link(options = {}, html_options = {}) #:nodoc:
|
12
|
+
@template.regenerate_captcha_challenge_link @object_name, options.merge(:object => @object), html_options
|
13
13
|
end
|
14
14
|
end
|
15
15
|
end
|
@@ -1,50 +1,38 @@
|
|
1
1
|
module ValidatesCaptcha
|
2
2
|
module FormHelper
|
3
|
-
# Returns
|
3
|
+
# Returns the captcha challenge.
|
4
4
|
#
|
5
|
-
# Internally calls
|
6
|
-
|
7
|
-
|
5
|
+
# Internally calls the +render_challenge+ method of ValidatesCaptcha#provider.
|
6
|
+
def captcha_challenge(object_name, options = {})
|
7
|
+
options.symbolize_keys!
|
8
|
+
|
8
9
|
object = options.delete(:object)
|
9
|
-
src = ValidatesCaptcha.captcha_image_path(object.encrypted_captcha)
|
10
10
|
sanitized_object_name = object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
|
11
11
|
|
12
|
-
|
13
|
-
options[:id] = "#{sanitized_object_name}_captcha_image"
|
14
|
-
|
15
|
-
image_tag src, options
|
12
|
+
ValidatesCaptcha.provider.render_challenge sanitized_object_name, object, options
|
16
13
|
end
|
17
14
|
|
18
|
-
# Returns an input tag of the "text" type tailored for entering the captcha
|
15
|
+
# Returns an input tag of the "text" type tailored for entering the captcha solution.
|
19
16
|
#
|
20
17
|
# Internally calls Rails' #text_field helper method, passing the +object_name+ and
|
21
18
|
# +options+ arguments.
|
22
19
|
def captcha_field(object_name, options = {})
|
23
20
|
options.delete(:id)
|
24
21
|
|
25
|
-
hidden_field(object_name, :
|
22
|
+
hidden_field(object_name, :captcha_challenge, options) + text_field(object_name, :captcha_solution, options)
|
26
23
|
end
|
27
24
|
|
28
|
-
#
|
29
|
-
# the
|
25
|
+
# By default, returns an anchor tag that makes an AJAX request to fetch a new captcha challenge and updates
|
26
|
+
# the current challenge after the request is complete.
|
30
27
|
#
|
31
|
-
# Internally calls
|
32
|
-
|
33
|
-
# to be available on the web page.
|
34
|
-
#
|
35
|
-
# The anchor text defaults to 'Regenerate Captcha'. You can set this to a custom value
|
36
|
-
# providing a +:text+ key in the +options+ hash.
|
37
|
-
def regenerate_captcha_link(object_name, options = {}, html_options = {})
|
28
|
+
# Internally calls +render_regenerate_challenge_link+ method of ValidatesCaptcha#provider.
|
29
|
+
def regenerate_captcha_challenge_link(object_name, options = {}, html_options = {})
|
38
30
|
options.symbolize_keys!
|
39
31
|
|
40
32
|
object = options.delete(:object)
|
41
|
-
text = options.delete(:text) || 'Regenerate Captcha'
|
42
33
|
sanitized_object_name = object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
|
43
34
|
|
44
|
-
|
45
|
-
success = "var result = request.responseJSON; $('#{sanitized_object_name}_captcha_image').src = result.captcha_image_path; $('#{sanitized_object_name}_encrypted_captcha').value = result.encrypted_captcha_code;"
|
46
|
-
|
47
|
-
link_to_remote text, options.reverse_merge(:url => url, :method => :get, :success => success), html_options
|
35
|
+
ValidatesCaptcha.provider.render_regenerate_challenge_link sanitized_object_name, object, options, html_options
|
48
36
|
end
|
49
37
|
end
|
50
38
|
end
|