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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e4935b8951819226ca81b82723628a6c555e505e
4
- data.tar.gz: c3fd3714c47c7b5e014ed34a2f817a249b257fa6
3
+ metadata.gz: 992b12eaa4153e29ad448ac21c6afafe12f9a89a
4
+ data.tar.gz: 3ce43ca69c2faeb4a335968959365f33309460ed
5
5
  SHA512:
6
- metadata.gz: 7bf5572895450e439a6559a79815603a98fd5dd1b2a1b11a1b13218dd15a35cbdbc67db60021752878c22fc1dceadc8f4926c79f90d65705ca78187a68fb8c55
7
- data.tar.gz: 4a6e9c478b28b58da51dedad966cd77b72ff5bca90a21d4adcba47c4560252549af8d9f75df2123fc30f46c600b7da06e7b6f5d8bb692b36d5f7052a0a364148
6
+ metadata.gz: af0baab222fcd78a7b49e5d8d2e204139ab7b1ec25cf75d001c2715777d31effa1527dda7ecfe9e1f18082978f77ad419d5c0b020667c6265d9273038d441483
7
+ data.tar.gz: 4b7da58d7c976ed77f7da407090c14f38f912ac0dbead739cc676a16b10665d09866a95f739c852139919a260805e839a80ea0c9797a3da8413aaef632109046
data/CHANGELOG CHANGED
@@ -1,3 +1,7 @@
1
+ = 3.3.0 (2017-12-14)
2
+
3
+ * Add typecast_params plugin for converting param values to explicit types (jeremyevans)
4
+
1
5
  = 3.2.0 (2017-11-16)
2
6
 
3
7
  * Use microseconds in assets plugin :timestamp_paths timestamps (jeremyevans)
@@ -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 params capturing plugin, as it does not
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. Example:
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 |value|
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 |value|
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 |value|
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 |value|
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.
@@ -32,8 +32,11 @@ class Roda
32
32
  # end
33
33
  #
34
34
  # r.post 'bar' do
35
- # bar = Bar.create(r.params['bar'])
36
- # r.redirect bar_path(bar) # /bar/1
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
  #