roda 3.2.0 → 3.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +4 -0
- data/README.rdoc +29 -1
- data/doc/release_notes/3.3.0.txt +291 -0
- data/lib/roda/plugins/class_matchers.rb +1 -1
- data/lib/roda/plugins/indifferent_params.rb +8 -1
- data/lib/roda/plugins/param_matchers.rb +11 -4
- data/lib/roda/plugins/path.rb +5 -2
- data/lib/roda/plugins/symbol_status.rb +2 -2
- data/lib/roda/plugins/typecast_params.rb +968 -0
- data/lib/roda/version.rb +1 -1
- data/spec/plugin/public_spec.rb +18 -0
- data/spec/plugin/typecast_params_spec.rb +1215 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 992b12eaa4153e29ad448ac21c6afafe12f9a89a
|
4
|
+
data.tar.gz: 3ce43ca69c2faeb4a335968959365f33309460ed
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: af0baab222fcd78a7b49e5d8d2e204139ab7b1ec25cf75d001c2715777d31effa1527dda7ecfe9e1f18082978f77ad419d5c0b020667c6265d9273038d441483
|
7
|
+
data.tar.gz: 4b7da58d7c976ed77f7da407090c14f38f912ac0dbead739cc676a16b10665d09866a95f739c852139919a260805e839a80ea0c9797a3da8413aaef632109046
|
data/CHANGELOG
CHANGED
data/README.rdoc
CHANGED
@@ -443,7 +443,7 @@ treat it as one route with an optional segment. One simple way to do that is to
|
|
443
443
|
use a parameter instead of an optional segment (e.g. +/items/123?opt=456+).
|
444
444
|
|
445
445
|
r.is "items", Integer do |item_id|
|
446
|
-
optional_data = r.params['opt']
|
446
|
+
optional_data = r.params['opt'].to_s
|
447
447
|
end
|
448
448
|
|
449
449
|
However, if you really do want to use a optional segment, there are a couple different
|
@@ -812,6 +812,34 @@ are not escaping the output of the content template:
|
|
812
812
|
|
813
813
|
This support requires {Erubi}[https://github.com/jeremyevans/erubi].
|
814
814
|
|
815
|
+
=== Unexpected Parameter Types
|
816
|
+
|
817
|
+
Rack converts submitted parameters into a hash of strings, arrays, and
|
818
|
+
nested hashes. Since the user controls the submission of parameters, you
|
819
|
+
should treat any submission of parameters with caution, and should be
|
820
|
+
explicitly checking and/or converting types before using any submitted
|
821
|
+
parameters. One way to do this is explicitly after accessing them:
|
822
|
+
|
823
|
+
# Convert foo_id parameter to an integer
|
824
|
+
request.params['foo_id'].to_i
|
825
|
+
|
826
|
+
However, it is easy to forget to convert the type, and if the user
|
827
|
+
submits +foo_id+ as a hash or array, a NoMethodError will be raised.
|
828
|
+
Worse is if you do:
|
829
|
+
|
830
|
+
some_method(request.params['bar'])
|
831
|
+
|
832
|
+
Where +some_method+ supports both a string argument and a hash
|
833
|
+
argument, and you expect the parameter will be submitted as a
|
834
|
+
string, and +some_method+'s handling of a hash argument performs
|
835
|
+
an unauthorized action.
|
836
|
+
|
837
|
+
Roda ships with a +typecast_params+ plugin that can easily handle
|
838
|
+
the typecasting of submitted parameters, and it is recommended
|
839
|
+
that all Roda applications that deal with parameters use it or
|
840
|
+
another tool to explicitly convert submitted parameters to the
|
841
|
+
expected types.
|
842
|
+
|
815
843
|
=== Security Related HTTP Headers
|
816
844
|
|
817
845
|
You may want to look into setting the following HTTP headers, which
|
@@ -0,0 +1,291 @@
|
|
1
|
+
= New Features
|
2
|
+
|
3
|
+
* A typecast_params plugin has been added for handling the
|
4
|
+
conversion of params to the expected type. This plugin is
|
5
|
+
recommended for all applications that deal with submitted
|
6
|
+
parameters.
|
7
|
+
|
8
|
+
Submitted parameters should be considered untrusted input, and in
|
9
|
+
standard use with browsers, parameters are submitted as strings
|
10
|
+
(or a hash/array containing strings). In most cases it makes sense
|
11
|
+
to explicitly convert the parameter to the desired type. While this
|
12
|
+
can be done via manual conversion:
|
13
|
+
|
14
|
+
key = request.params['key'].to_i
|
15
|
+
key = nil unless key > 0
|
16
|
+
|
17
|
+
the typecast_params plugin adds a friendlier interface:
|
18
|
+
|
19
|
+
key = typecast_params.pos_int('key')
|
20
|
+
|
21
|
+
As typecast_params is a fairly long method name, you may want to
|
22
|
+
consider aliasing it to something more terse in your application,
|
23
|
+
such as tp.
|
24
|
+
|
25
|
+
One advantage of using typecast_params is that access or conversion
|
26
|
+
errors are raised as a specific exception class
|
27
|
+
(Roda::RodaPlugins::TypecastParams::Error). This allows you to
|
28
|
+
handle this specific exception class globally and return an
|
29
|
+
appropriate 4xx response to the client. You can use the
|
30
|
+
Error#param_name and Error#reason methods to get more information
|
31
|
+
about the error.
|
32
|
+
|
33
|
+
typecast_params offers support for default values:
|
34
|
+
|
35
|
+
key = typecast_params.pos_int('key', 1)
|
36
|
+
|
37
|
+
The default value is only used if no value has been submitted for
|
38
|
+
the parameter, or if the conversion of the value results in nil.
|
39
|
+
Handling defaults for parameter conversion manually is more
|
40
|
+
difficult, since the parameter may not be present at all, or it may
|
41
|
+
be present but an empty string because the user did not enter a
|
42
|
+
value on the related form. Use of typecast_params for the
|
43
|
+
conversion handles both cases.
|
44
|
+
|
45
|
+
In many cases, parameters should be required, and if they aren't
|
46
|
+
submitted, that should be considered an error. typecast_params
|
47
|
+
handles this with ! methods:
|
48
|
+
|
49
|
+
key = typecast_params.pos_int!('key')
|
50
|
+
|
51
|
+
These ! methods raise an error instead of returning nil, and do not
|
52
|
+
allow defaults.
|
53
|
+
|
54
|
+
To make it easy to handle cases where many parameters need the same
|
55
|
+
conversion done, you can pass an array of keys to a conversion
|
56
|
+
method, and it will return an array of converted values:
|
57
|
+
|
58
|
+
key1, key2 = typecast_params.pos_int(['key1', 'key2'])
|
59
|
+
|
60
|
+
This is equivalent to:
|
61
|
+
|
62
|
+
key1 = typecast_params.pos_int('key1')
|
63
|
+
key2 = typecast_params.pos_int('key2')
|
64
|
+
|
65
|
+
The ! methods also support arrays of keys, ensuring that all
|
66
|
+
parameters have a value:
|
67
|
+
|
68
|
+
key1, key2 = typecast_params.pos_int!(['key1', 'key2'])
|
69
|
+
|
70
|
+
For handling of array parameters, where all entries in the array
|
71
|
+
use the same conversion, there is an array method which takes the
|
72
|
+
type as the first argument and the keys to convert as the second
|
73
|
+
argument:
|
74
|
+
|
75
|
+
keys = typecast_params.array(:pos_int, 'keys')
|
76
|
+
|
77
|
+
If you want to ensure that all entries in the array are converted
|
78
|
+
successfully and that there is a value for the array itself, you
|
79
|
+
can use array!:
|
80
|
+
|
81
|
+
keys = typecast_params.array!(:pos_int, 'keys')
|
82
|
+
|
83
|
+
This will raise an exception if any of the values in the array for
|
84
|
+
parameter keys cannot be converted to a positive integer.
|
85
|
+
|
86
|
+
Both array and array! support default values which are used if no
|
87
|
+
value is present for the parameter:
|
88
|
+
|
89
|
+
keys = typecast_params.array(:pos_int, 'keys', [])
|
90
|
+
keys = typecast_params.array!(:pos_int, 'keys', [])
|
91
|
+
|
92
|
+
You can also pass an array of keys to array or array!, if you would
|
93
|
+
like to perform the same conversion on multiple arrays:
|
94
|
+
|
95
|
+
foo_ids, bar_ids = typecast_params.array!(:pos_int, ['foo_ids', 'bar_ids'])
|
96
|
+
|
97
|
+
The previous examples have shown use of the pos_int method, which
|
98
|
+
uses to_i to convert the value to an integer, but returns nil if the
|
99
|
+
resulting integer is not positive. Unless you need to handle
|
100
|
+
negative numbers, it is recommended to use pos_int instead of int as
|
101
|
+
int will convert invalid values to 0 (since that is how
|
102
|
+
<tt>String#to_i</tt> works).
|
103
|
+
|
104
|
+
There are many built in methods for type conversion:
|
105
|
+
|
106
|
+
any :: Returns the value as is without conversion
|
107
|
+
str :: Raises if value is not already a string
|
108
|
+
nonempty_str :: Raises if value is not already a string, and
|
109
|
+
converts the empty string or string containing only
|
110
|
+
whitespace to nil
|
111
|
+
bool :: Converts entry to boolean if in one of the recognized
|
112
|
+
formats (case insensitive for strings):
|
113
|
+
nil :: nil, ''
|
114
|
+
true :: true, 1, '1', 't', 'true', 'yes', 'y', 'on'
|
115
|
+
false :: false, 0, '0', 'f', 'false', 'no', 'n', 'off'
|
116
|
+
If not in one of those formats, raises an error.
|
117
|
+
int :: Converts value to integer using to_i (note that invalid
|
118
|
+
input strings will be converted to 0)
|
119
|
+
pos_int :: Converts value using to_i, but non-positive values
|
120
|
+
are converted to nil
|
121
|
+
Integer :: Converts value to integer using
|
122
|
+
Kernel::Integer(value, 10)
|
123
|
+
float :: Converts value to float using to_f (note that invalid
|
124
|
+
input strings will be converted to 0.0)
|
125
|
+
Float :: Converts value to float using Kernel::Float(value)
|
126
|
+
Hash :: Raises if value is not already a hash
|
127
|
+
date :: Converts value to Date using Date.parse(value)
|
128
|
+
time :: Converts value to Time using Time.parse(value)
|
129
|
+
datetime :: Converts value to DateTime using DateTime.parse(value)
|
130
|
+
file :: Raises if value is not already a hash with a :tempfile key
|
131
|
+
whose value responds to read (this is the format rack uses
|
132
|
+
for uploaded files).
|
133
|
+
|
134
|
+
All of these methods also support ! methods (e.g. pos_int!), and all
|
135
|
+
of them can be used in the array and array! methods to support
|
136
|
+
arrays of values.
|
137
|
+
|
138
|
+
Since parameter hashes can be nested, the [] method can be used to
|
139
|
+
access nested
|
140
|
+
hashes:
|
141
|
+
|
142
|
+
# params: {'key'=>{'sub_key'=>'1'}}
|
143
|
+
typecast_params['key'].pos_int!('sub_key') # => 1
|
144
|
+
|
145
|
+
This works to an arbitrary depth:
|
146
|
+
|
147
|
+
# params: {'key'=>{'sub_key'=>{'sub_sub_key'=>'1'}}}
|
148
|
+
typecast_params['key']['sub_key'].pos_int!('sub_sub_key') # => 1
|
149
|
+
|
150
|
+
And also works with arrays at any depth, if those arrays contain
|
151
|
+
hashes:
|
152
|
+
|
153
|
+
# params: {'key'=>[{'sub_key'=>{'sub_sub_key'=>'1'}}]}
|
154
|
+
typecast_params['key'][0]['sub_key'].pos_int!('sub_sub_key') # => 1
|
155
|
+
|
156
|
+
# params: {'key'=>[{'sub_key'=>['1']}]}
|
157
|
+
typecast_params['key'][0].array!(:pos_int, 'sub_key') # => [1]
|
158
|
+
|
159
|
+
To allow easier access to nested data, there is a dig method:
|
160
|
+
|
161
|
+
typecast_params.dig(:pos_int, 'key', 'sub_key')
|
162
|
+
typecast_params.dig(:pos_int, 'key', 0, 'sub_key', 'sub_sub_key')
|
163
|
+
|
164
|
+
dig will return nil if any access while looking up the nested value
|
165
|
+
returns nil. There is also a dig! method, which will raise an Error
|
166
|
+
if dig would return nil:
|
167
|
+
|
168
|
+
typecast_params.dig!(:pos_int, 'key', 'sub_key')
|
169
|
+
typecast_params.dig!(:pos_int, 'key', 0, 'sub_key', 'sub_sub_key')
|
170
|
+
|
171
|
+
Note that none of these conversion methods modify request.params.
|
172
|
+
They purely do the conversion and return the converted value.
|
173
|
+
However, in some cases it is useful to do all the conversion up
|
174
|
+
front, and then pass a hash of converted parameters to an internal
|
175
|
+
method that expects to receive values in specific types. The
|
176
|
+
convert! method does this, and there is also a convert_each! method
|
177
|
+
designed for converting multiple values using the same block:
|
178
|
+
|
179
|
+
converted_params = typecast_params.convert! do |tp|
|
180
|
+
tp.int('page')
|
181
|
+
tp.pos_int!('artist_id')
|
182
|
+
tp.array!(:pos_int, 'album_ids')
|
183
|
+
tp.convert!('sales') do |stp|
|
184
|
+
tp.pos_int!(['num_sold', 'num_shipped'])
|
185
|
+
end
|
186
|
+
tp.convert!('members') do |mtp|
|
187
|
+
mtp.convert_each! do |stp|
|
188
|
+
stp.str!(['first_name', 'last_name'])
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# converted_params:
|
194
|
+
# {
|
195
|
+
# 'page' => 1,
|
196
|
+
# 'artist_id' => 2,
|
197
|
+
# 'album_ids' => [3, 4],
|
198
|
+
# 'sales' => {
|
199
|
+
# 'num_sold' => 5,
|
200
|
+
# 'num_shipped' => 6
|
201
|
+
# },
|
202
|
+
# 'members' => [
|
203
|
+
# {'first_name' => 'Foo', 'last_name' => 'Bar'},
|
204
|
+
# {'first_name' => 'Baz', 'last_name' => 'Quux'}
|
205
|
+
# ]
|
206
|
+
# }
|
207
|
+
|
208
|
+
convert! and convert_each! only return values you explicitly specify
|
209
|
+
for conversion inside the passed block.
|
210
|
+
|
211
|
+
You can specify the :symbolize option to convert! or convert_each!,
|
212
|
+
which will symbolize the resulting hash keys:
|
213
|
+
|
214
|
+
converted_params = typecast_params.convert!(symbolize: true) do |tp|
|
215
|
+
tp.int('page')
|
216
|
+
tp.pos_int!('artist_id')
|
217
|
+
tp.array!(:pos_int, 'album_ids')
|
218
|
+
tp.convert!('sales') do |stp|
|
219
|
+
tp.pos_int!(['num_sold', 'num_shipped'])
|
220
|
+
end
|
221
|
+
tp.convert!('members') do |mtp|
|
222
|
+
mtp.convert_each! do |stp|
|
223
|
+
stp.str!(['first_name', 'last_name'])
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# converted_params:
|
229
|
+
# {
|
230
|
+
# :page => 1,
|
231
|
+
# :artist_id => 2,
|
232
|
+
# :album_ids => [3, 4],
|
233
|
+
# :sales => {
|
234
|
+
# :num_sold => 5,
|
235
|
+
# :num_shipped => 6
|
236
|
+
# },
|
237
|
+
# :members => [
|
238
|
+
# {:first_name => 'Foo', :last_name => 'Bar'},
|
239
|
+
# {:first_name => 'Baz', :last_name => 'Quux'}
|
240
|
+
# ]
|
241
|
+
# }
|
242
|
+
|
243
|
+
Using the :symbolize option makes it simpler to transition from
|
244
|
+
untrusted external data (string keys), to trusted data that can be
|
245
|
+
used internally (trusted in the sense that the expected types are
|
246
|
+
used).
|
247
|
+
|
248
|
+
Note that if there are multiple conversion errors raised inside a
|
249
|
+
convert! or convert_each! block, they are recorded and a single
|
250
|
+
Roda::RodaPlugins::TypecastParams::Error instance is raised after
|
251
|
+
processing the block. TypecastParams::Error#params_names can be
|
252
|
+
called on the exception to get an array of all parameter names
|
253
|
+
with conversion issues, and TypecastParams::Error#all_errors
|
254
|
+
can be used to get an array of all Error instances.
|
255
|
+
|
256
|
+
Because of how convert! and convert_each! work, you should avoid
|
257
|
+
calling TypecastParams::Params#[] inside the block you pass to
|
258
|
+
these methods, because if the #[] call fails, it will skip the
|
259
|
+
reminder of the block.
|
260
|
+
|
261
|
+
Be aware that when you use convert! and convert_each!, the
|
262
|
+
conversion methods called inside the block may return nil if there
|
263
|
+
is a error raised, and nested calls to convert! and convert_each!
|
264
|
+
may not return values.
|
265
|
+
|
266
|
+
When loading the typecast_params plugin, a subclass of
|
267
|
+
TypecastParams::Params is created specific to the Roda application.
|
268
|
+
You can add support for custom types by passing a block when loading
|
269
|
+
the typecast_params plugin. This block is executed in the context
|
270
|
+
of the subclass, and calling handle_type in the block can be used to
|
271
|
+
add conversion methods. handle_type accepts a type name and the
|
272
|
+
block used to convert the type:
|
273
|
+
|
274
|
+
plugin :typecast_params do
|
275
|
+
handle_type(:album) do |value|
|
276
|
+
if id = convert_pos_int(val)
|
277
|
+
Album[id]
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
By default, the typecast_params conversion procs are passed the
|
283
|
+
parameter value directly from request.params without modification.
|
284
|
+
In some cases, it may be beneficial to strip leading and trailing
|
285
|
+
whitespace from parameter string values before processing, which
|
286
|
+
you can do by passing the strip: :all> option when loading the
|
287
|
+
plugin.
|
288
|
+
|
289
|
+
By design, typecast_params only deals with string keys, it is not
|
290
|
+
possible to use symbol keys as arguments to the conversion methods
|
291
|
+
and have them converted.
|
@@ -28,7 +28,7 @@ class Roda
|
|
28
28
|
# This is useful to DRY up code if you are using the same type of pattern and
|
29
29
|
# type conversion in multiple places in your application.
|
30
30
|
#
|
31
|
-
# This plugin does not work with the
|
31
|
+
# This plugin does not work with the params_capturing plugin, as it does not
|
32
32
|
# offer the ability to associate block arguments with named keys.
|
33
33
|
module ClassMatchers
|
34
34
|
module ClassMethods
|
@@ -6,7 +6,14 @@ class Roda
|
|
6
6
|
# The indifferent_params plugin adds a +params+ instance
|
7
7
|
# method which offers indifferent access to the request
|
8
8
|
# params, allowing you to use symbols to lookup values in
|
9
|
-
# a hash where the keys are strings.
|
9
|
+
# a hash where the keys are strings. Note that while this
|
10
|
+
# allows for an easier transition from some other ruby frameworks,
|
11
|
+
# it is a bad idea in general as it makes it more difficult to
|
12
|
+
# separate external data from internal data, and doesn't handle
|
13
|
+
# any typecasting of the data. Consider using the typecast_params
|
14
|
+
# plugin instead of this plugin for accessing parameters.
|
15
|
+
#
|
16
|
+
# Example:
|
10
17
|
#
|
11
18
|
# plugin :indifferent_params
|
12
19
|
#
|
@@ -9,7 +9,7 @@ class Roda
|
|
9
9
|
# It adds a :param matcher for matching on any param with the
|
10
10
|
# same name, yielding the value of the param:
|
11
11
|
#
|
12
|
-
# r.on param: 'foo' do |
|
12
|
+
# r.on param: 'foo' do |foo|
|
13
13
|
# # Matches '?foo=bar', '?foo='
|
14
14
|
# # Doesn't match '?bar=foo'
|
15
15
|
# end
|
@@ -17,7 +17,7 @@ class Roda
|
|
17
17
|
# It adds a :param! matcher for matching on any non-empty param
|
18
18
|
# with the same name, yielding the value of the param:
|
19
19
|
#
|
20
|
-
# r.on(param!: 'foo') do |
|
20
|
+
# r.on(param!: 'foo') do |foo|
|
21
21
|
# # Matches '?foo=bar'
|
22
22
|
# # Doesn't match '?foo=', '?bar=foo'
|
23
23
|
# end
|
@@ -25,16 +25,23 @@ class Roda
|
|
25
25
|
# It also adds :params and :params! matchers, for matching multiple
|
26
26
|
# params at the same time:
|
27
27
|
#
|
28
|
-
# r.on params: ['foo', 'baz'] do |
|
28
|
+
# r.on params: ['foo', 'baz'] do |foo, baz|
|
29
29
|
# # Matches '?foo=bar&baz=quuz', '?foo=&baz='
|
30
30
|
# # Doesn't match '?foo=bar', '?baz='
|
31
31
|
# end
|
32
32
|
#
|
33
|
-
# r.on params!: ['foo', 'baz'] do |
|
33
|
+
# r.on params!: ['foo', 'baz'] do |foo, baz|
|
34
34
|
# # Matches '?foo=bar&baz=quuz'
|
35
35
|
# # Doesn't match '?foo=bar', '?baz=', '?foo=&baz=', '?foo=bar&baz='
|
36
36
|
# end
|
37
37
|
#
|
38
|
+
# Because users have some control over the types of submitted parameters,
|
39
|
+
# it is recommended that you explicitly force the correct type for values
|
40
|
+
# yielded by the block:
|
41
|
+
#
|
42
|
+
# r.get(:param=>'foo') do |foo|
|
43
|
+
# foo = foo.to_s
|
44
|
+
# end
|
38
45
|
module ParamMatchers
|
39
46
|
module RequestMethods
|
40
47
|
# Match the given parameter if present, even if the parameter is empty.
|
data/lib/roda/plugins/path.rb
CHANGED
@@ -32,8 +32,11 @@ class Roda
|
|
32
32
|
# end
|
33
33
|
#
|
34
34
|
# r.post 'bar' do
|
35
|
-
#
|
36
|
-
#
|
35
|
+
# bar_params = r.params['bar']
|
36
|
+
# if bar_params.is_a?(Hash)
|
37
|
+
# bar = Bar.create(bar_params)
|
38
|
+
# r.redirect bar_path(bar) # /bar/1
|
39
|
+
# end
|
37
40
|
# end
|
38
41
|
#
|
39
42
|
# r.post 'baz' do
|
@@ -7,10 +7,10 @@ class Roda
|
|
7
7
|
# the default behaviour is used.
|
8
8
|
#
|
9
9
|
# Examples:
|
10
|
-
# r.is "needs_authorization"
|
10
|
+
# r.is "needs_authorization" do
|
11
11
|
# response.status = :unauthorized
|
12
12
|
# end
|
13
|
-
# r.is "nothing"
|
13
|
+
# r.is "nothing" do
|
14
14
|
# response.status = :no_content
|
15
15
|
# end
|
16
16
|
#
|