File Coverage

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


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