File Coverage

blib/lib/Catalyst/TraitFor/Request/StructuredParameters.pm
Criterion Covered Total %
statement 18 18 100.0
branch 4 6 66.6
condition 3 6 50.0
subroutine 5 5 100.0
pod 3 3 100.0
total 33 38 86.8


line stmt bran cond sub pod time code
1              
2             our $VERSION = '0.009';
3              
4             use Moose::Role;
5 2     2   335442 use Catalyst::Utils::StructuredParameters;
  2         6  
  2         19  
6 2     2   11078  
  2         8  
  2         500  
7             # Yeah there's copy pasta here just right now I'm not sure we won't need more
8             # customization so I'm just going to leave it.
9              
10             my ($self, @args) = @_;
11             my $strong = Catalyst::Utils::StructuredParameters->new(
12 5     5 1 887 src => 'body',
13 5   50     130 flatten_array_value => 1,
14             context => $self->body_parameters||+{}
15             );
16             $strong->permitted(@args) if @args;
17             return $strong;
18 5 100       37 }
19 5         59  
20             my ($self, @args) = @_;
21             my $strong = Catalyst::Utils::StructuredParameters->new(
22             src => 'query',
23 4     4 1 60586 flatten_array_value => 1,
24 4   50     87 context => $self->query_parameters||+{}
25             );
26             $strong->permitted(@args) if @args;
27             return $strong;
28             }
29 4 50       63  
30 4         18 my ($self, @args) = @_;
31             my $strong = Catalyst::Utils::StructuredParameters->new(
32             src => 'data',
33             flatten_array_value => 0,
34 2     2 1 820 context => $self->body_data||+{}
35 2   50     59 );
36             $strong->permitted(@args) if @args;
37             return $strong;
38             }
39              
40 2 50       9 1;
41 2         43  
42             =head1 NAME
43              
44             Catalyst::TraitFor::Request::StructuredParameters - Enforce structural rules on your body and data parameters
45              
46             =head1 SYNOPSIS
47              
48             For L<Catalyst> v5.90090+
49            
50             package MyApp;
51            
52             use Catalyst;
53            
54             MyApp->request_class_traits(['Catalyst::TraitFor::Request::StructuredParameters']);
55             MyApp->setup;
56            
57             For L<Catalyst> older than v5.90090
58            
59             package MyApp;
60            
61             use Catalyst;
62             use CatalystX::RoleApplicator;
63            
64             MyApp->apply_request_class_roles('Catalyst::TraitFor::Request::StructuredParameters');
65             MyApp->setup;
66            
67             In a controller:
68              
69             package MyApp::Controller::User;
70              
71             use Moose;
72             use MooseX::MethodAttributes;
73              
74             extends 'Catalyst::Controller';
75              
76             sub user :Local {
77             my ($self, $c) = @_;
78              
79             # Basically this is like a whitelist for the allowed parameters. This is not a replacement
80             # for form validation but rather prior layer to make sure the incoming is semantically
81             # acceptable. It also does some sanity cleanup like flatten unexpected arrays. The following
82             # would accept body parameters like the following:
83             #
84             # $c->req->body_parameters == +{
85             # username => 'jnap',
86             # password => 'youllneverguess',
87             # password_confirmation => 'youllneverguess'
88             # 'name.first' => 'John',
89             # 'name.last' => 'Napiorkowski',
90             # 'email[0]' => 'jjn1056@example1.com',
91             # 'email[1]' => 'jjn1056@example2.com',
92             # }
93              
94             my %body_parameters = $c->req->structured_body
95             ->permitted('username', 'password', 'password_confirmation', name => ['first', 'last'], +{email=>[]} )
96             ->to_hash;
97              
98             # %body_parameters then looks like this, which is a form suitable for validating and creating
99             # or updating a database.
100             #
101             # %body_parameters == (
102             # username => 'jnap',
103             # password => 'youllneverguess',
104             # password_confirmation => 'youllneverguess'
105             # name => +{
106             # first => 'John',
107             # last => 'Napiorkowski',
108             # },
109             # email => ['jjn1056@example1.com', 'jjn1056@example2.com'],
110             # );
111              
112             # Ok so now you know %body_parameters are 'well-formed', you can use them to do stuff like
113             # value validation and updating a databases, etc.
114              
115             my $new_user = $c->model('Schema::User')->validate_and_create(\%body_parameters);
116             }
117              
118             =head1 DESCRIPTION
119              
120             This replaces L<Catalyst::TraitFor::Request::StrongParameters>. If you were using that you should switch
121             to this. This is right now just a name change but any bug fixes will happen here. Sooner or later I'll
122             remove L<Catalyst::TraitFor::Request::StrongParameters> from the indexes.
123              
124             WARNING: This is a quick midnight hack and the code could have sharp edges. Happy to take broken
125             test cases.
126              
127             When your web application receives incoming POST body or data you should treat that data with suspicion.
128             Even prior to validation you need to make sure the incoming structure is well formed (this is most
129             important when you have deeply nested structures, which can be a major security risk both in parsing
130             and in using that data to do things like update a database). L<Catalyst::TraitFor::Request::StructuredParameters>
131             offers a structured approach to whitelisting your incoming POSTed data, as well as a safe way to introduce
132             nested data structures into your classic HTML Form posted data. It is also compatible for POSTed data
133             (such as JSON POSTed data) although in the case of body data such as JSON we merely whitelist the fields
134             and structure since JSON can already support nested data structures.
135              
136             This is similar to a concept called 'strong parameters' in Rails although my implementation is somewhat
137             different based on the varying needs of the L<Catalyst> framework. However I consider this beta code
138             and subject to change should real life use cases arise that indicate a different approach is warranted.
139              
140             =head1 METHODS
141              
142             This role defines the following methods:
143              
144             =head2 structured_body
145              
146             Returns an instance of L<Catalyst::Utils::StructuredParameters> preconfigured with the current contents
147             of ->body_parameters. Any arguments are passed to that instances L</permitted> methods before return.
148              
149             =head2 structured_query
150              
151             Parses the URI query string; otherwise same as L</structured_body>.
152              
153             =head2 structured_data
154              
155             The same as L</structured_body> except aimed at body data such as JSON post. Basically works
156             the same except the default for handling array values is to leave them alone rather than to flatten.
157              
158             =head1 PARAMETER OBJECT METHODS
159              
160             The instance of L<Catalyst::Utils::StructuredParameters> which is returned by any of the three builder
161             methods above (L</structured_body>, L</structured_query and L</structured_data>) supports the following methods.
162              
163             =head2 namespace (\@fields)
164              
165             Sets the current 'namespace' to start looking for fields and values. Useful when all the fields are
166             under a key. For example if the value of ->body_parameters is:
167              
168             +{
169             'person.name' => 'John',
170             'person.age' => 52,
171             }
172              
173             If you set the namespace to C<['person']> then you can create rule specifications that assume to be
174             'under' that key. See the L</SYNOPSIS> for an example.
175              
176             =head2 permitted (?\@namespace, @rules)
177              
178             An array of rule specifications that are used to filter the current parameters as passed by the user
179             and present a sanitized version that can safely be used in your code.
180              
181             If the first argument is an arrayref, that value is used to set the starting L</namespace>.
182              
183             =head2 required (?\@namespace, @rules)
184              
185             An array of rule specifications that are used to filter the current parameters as passed by the user
186             and present a sanitized version that can safely be used in your code.
187              
188             If user submitted parameters do not match the spec an exception is throw (L<Catalyst::Exception::MissingParameter>
189             If you want to use required parameters then you should add code to catch this error and handle it
190             (see below for more)
191              
192             If the first argument is an arrayref, that value is used to set the starting L</namespace>.
193              
194             =head2 flatten_array_value ($bool)
195              
196             Switch to indicated if you want to flatten any arrayref values to 'pick last'. This is true by default
197             for body and query parameters since its a common hack around quirks with certain types of HTML form controls
198             (like checkboxes) which don't return a value when not selected or checked.
199              
200             =head2 max_array_depth (number)
201              
202             Prevent incoming parameters from having too many items in an array value. Default is 1,000. You may wish
203             to set a different value based on your requirements. Throws L<Catalyst::Exception::InvalidArrayLength> if violated.
204              
205             =head2 to_hash
206              
207             Returns the currently filtered parameters based on the current permitted and/or required specifications.
208              
209             =head2 keys
210              
211             Returns an unorderd list of all the top level keys
212              
213             =head2 get (@key_names)
214              
215             Given a list of key names, return values. Doesn't currently do anything if you use a key name that
216             doesn't exist (you just get 'undef').
217              
218             =head1 CHAINING
219              
220             All the public methods for L<Catalyst::Utils::StructuredParameters> return the current instance so that
221             you can chain methods easily (except for L</to_hash>). If you chain L</permitted> and L</required>
222             the accepted hashrefs are merged.
223              
224             =head1 RULE SPECIFICATIONS
225              
226             L<Catalyst::TraitFor::Request::StructuredParameters> offers a concise DSL for describing permitted and required
227             parameters, including flat parameters, hashes, arrays and any combination thereof.
228              
229             Given body_parameters of the following:
230              
231             +{
232             'person.name' => 'John',
233             'person.age' => '52',
234             'person.address.street' => '15604 Harry Lind Road',
235             'person.address.zip' => '78621',
236             'person.email[0]' => 'jjn1056@gmail.com',
237             'person.email[1]' => 'jjn1056@yahoo.com',
238             'person.credit_cards[0].number' => '245345345345345',
239             'person.credit_cards[0].exp' => '2024-01-01',
240             'person.credit_cards[1].number' => '666677777888878',
241             'person.credit_cards[1].exp' => '2024-01-01',
242             'person.credit_cards[].number' => '444444433333',
243             'person.credit_cards[].exp' => '4024-01-01',
244             }
245              
246             my %data = $c->req->strong_body
247             ->namespace(['person'])
248             ->permitted('name','age');
249              
250             # %data = ( name => 'John', age => 53 );
251            
252             my %data = $c->req->structured_body
253             ->namespace(['person'])
254             ->permitted('name','age', address => ['street', 'zip']); # arrayref means the following are subkeys
255              
256             # %data = (
257             # name => 'John',
258             # age => 53,
259             # address => +{
260             # street => '15604 Harry Lind Road',
261             # zip '78621',
262             # }
263             # );
264              
265             my %data = $c->req->structured_body
266             ->namespace(['person'])
267             ->permitted('name','age', +{email => []} ); # wrapping in a hashref mean 'under this is an arrayref
268              
269             # %data = (
270             # name => 'John',
271             # age => 53,
272             # email => ['jjn1056@gmail.com', 'jjn1056@yahoo.com']
273             # );
274            
275             # Combine hashref and arrayref to indicate array of subkeyu
276             my %data = $c->req->structured_body
277             ->namespace(['person'])
278             ->permitted('name','age', +{credit_cards => [qw/number exp/]} );
279              
280             # %data = (
281             # name => 'John',
282             # age => 53,
283             # credit_cards => [
284             # {
285             # number => "245345345345345",
286             # exp => "2024-01-01",
287             # },
288             # {
289             # number => "666677777888878",
290             # exp => "2024-01-01",
291             # },
292             # {
293             # number => "444444433333",
294             # exp => "4024-01-01",
295             # },
296             # ]
297             # );
298              
299             You can specify more than one specification for the same key. For example if body
300             parameters are:
301              
302             +{
303             'person.credit_cards[0].number' => '245345345345345',
304             'person.credit_cards[0].exp' => '2024-01-01',
305             'person.credit_cards[1].number' => '666677777888878',
306             'person.credit_cards[1].exp.year' => '2024',
307             'person.credit_cards[1].exp.month' => '01',
308             }
309              
310             my %data = $c->req->structured_body
311             ->namespace(['person'])
312             ->permitted(+{credit_cards => ['number', 'exp', exp=>[qw/year month/] ]} );
313              
314             # %data = (
315             # credit_cards => [
316             # {
317             # number => "245345345345345",
318             # exp => "2024-01-01",
319             # },
320             # {
321             # number => "666677777888878",
322             # exp => +{
323             # year => '2024',
324             # month => '01'
325             # },
326             # },
327             # ]
328             # );
329              
330              
331             =head2 ARRAY DATA AND ARRAY VALUE FLATTENING
332              
333             Please note this only applies to L</structured_body> / L</structured_query>
334              
335             In the situation when you have a array value for a given namespace specification such as
336             the following :
337              
338             'person.name' => 2,
339             'person.name' => 'John', # flatten array should jsut pick the last one
340              
341             We automatically pick the last POSTed value. This can be a useful hack around some HTML form elements
342             that don't set an 'off' value (like checkboxes).
343              
344             =head2 'EMPTY' FINAL INDEXES
345              
346             Please note this only applies to L</structured_body> / L</structured_query>
347              
348             Since the index values used to sort arrays are not preserved (they indicate order but are not used to
349             set the index since that could open your code to potential hackers) we permit final 'empty' indexes:
350              
351             'person.credit_cards[0].number' => '245345345345345',
352             'person.credit_cards[0].exp' => '2024-01-01',
353             'person.credit_cards[1].number' => '666677777888878',
354             'person.credit_cards[1].exp' => '2024-01-01',
355             'person.credit_cards[].number' => '444444433333',
356             'person.credit_cards[].exp' => '4024-01-01',
357              
358             This 'empty' index will always be considered the final element when sorting. You may have more than
359             one final empty index as well when its either the last rule or the rule only contains a single index
360              
361             'person.notes[]' => 'This is a note',
362             'person.notes[]' => 'This is another note',
363             'person.person_roles[1].role_id' => '1',
364             'person.person_roles[2].role_id' => '2',
365             'person.person_roles[].role_id' => '3',
366             'person.person_roles[].role_id' => '4',
367              
368             Would produce:
369              
370             +{
371             person => {
372             notes => [
373             'This is a note',
374             'This is another note',
375             ],
376             person_roles => [
377             {
378             role_id => 1,
379             },
380             {
381             role_id => 2,
382             },
383             {
384             role_id => 3,
385             },
386             {
387             role_id => 4,
388             },
389             ],
390             },
391             };
392              
393             =head1 EXCEPTIONS
394              
395             The following exceptions can be raised by these methods and you should add code to recognize and
396             handle them. For example you can add a global or controller scoped 'end' action:
397              
398             sub end :Action {
399             my ($self, $c) = @_;
400             if(my $error = $c->last_error) {
401             $c->clear_errors;
402             if(blessed($error) && $error->isa('Catalyst::Exception::StructuredParameter')) {
403             # Handle the error perhaps by forwarding to a view and setting a 4xx
404             # bad request response code.
405             }
406             }
407             }
408              
409             Alternatively (and probably neater) just use L<CatalystX::Error>.
410              
411             sub end :Action Does(RenderError) { }
412              
413             You'll need to add the included L<Catalyst::Plugin::Errors> plugin to your application class in order
414             to use this ActionRole.
415              
416             =head2 Exception: Base Class
417              
418             L<Catalyst::Exception::StructuredParameter>
419              
420             There's a number of different exceptions that this trait can throw but they all inherit from
421             L<Catalyst::Exception::StructuredParameter> so you can just check for that since those are all going
422             to be considered 'Bad Request' type issues. This also inherits from L<CatalystX::Utils::HttpException>
423             so you can use the L<CatalystX::Errors> package to neaten up / regularize your error control.
424              
425             =head2 EXCEPTION: MISSING PARAMETER
426              
427             L<Catalyst::Exception::MissingParameter> ISA L<Catalyst::Exception::StructuredParameter>
428              
429             If you use L</required> and a parameter is not present you will raise this exception, which will
430             contain a message indicating the first found missing parameter. For example:
431              
432             "Required parameter 'username' is missing."
433              
434             This will not be an exhaustive list of the missing parameters and this feature in not intended to
435             be used as a sort of form validation system.
436              
437             =head1 AUTHOR
438            
439             John Napiorkowski L<email:jjnapiork@cpan.org>
440            
441             =head1 SEE ALSO
442            
443             L<Catalyst>, L<Catalyst::Request>
444              
445             =head1 COPYRIGHT & LICENSE
446            
447             Copyright 20121, John Napiorkowski L<email:jjnapiork@cpan.org>
448            
449             This library is free software; you can redistribute it and/or modify it under
450             the same terms as Perl itself.
451              
452             =cut