roda 3.2.0 → 3.3.0

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.
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
  #