File Coverage

blib/lib/Catalyst/TraitFor/Request/StrongParameters.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::StrongParameters;
2              
3             our $VERSION = '0.002';
4              
5 2     2   412617 use Moose::Role;
  2         6  
  2         23  
6 2     2   13705 use Catalyst::Utils::StrongParameters;
  2         8  
  2         590  
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 strong_body {
12 2     2 1 1000 my ($self, @args) = @_;
13 2   50     59 my $strong = Catalyst::Utils::StrongParameters->new(
14             src => 'body',
15             flatten_array_value => 1,
16             context => $self->body_parameters||+{}
17             );
18 2 50       11 $strong->permitted(@args) if @args;
19 2         19 return $strong;
20             }
21              
22             sub strong_query {
23 4     4 1 73667 my ($self, @args) = @_;
24 4   50     112 my $strong = Catalyst::Utils::StrongParameters->new(
25             src => 'query',
26             flatten_array_value => 1,
27             context => $self->query_parameters||+{}
28             );
29 4 50       35 $strong->permitted(@args) if @args;
30 4         25 return $strong;
31             }
32              
33             sub strong_data {
34 2     2 1 989 my ($self, @args) = @_;
35 2   50     67 my $strong = Catalyst::Utils::StrongParameters->new(
36             src => 'data',
37             flatten_array_value => 0,
38             context => $self->body_data||+{}
39             );
40 2 50       11 $strong->permitted(@args) if @args;
41 2         49 return $strong;
42             }
43              
44             1;
45              
46             =head1 NAME
47              
48             Catalyst::TraitFor::Request::StrongParameters - 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::StrongParameters']);
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::StrongParameters');
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->strong_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             # If you don't have theses that meant the request was ill-formed.
117             $c->detach('errors/400_bad_request') unless %body_parameters;
118              
119             # Ok so now you know %body_parameters are 'well-formed', you can use them to do stuff like
120             # value validation and updating a databases, etc.
121              
122             my $new_user = $c->model('Schema::User')->validate_and_create(\%body_parameters);
123             }
124              
125             =head1 DESCRIPTION
126              
127             WARNING: This is a quick midnight hack and the code could have sharp edges. Happy to take broken
128             test cases.
129              
130             When your web application receives incoming POST body or data you should treat that data with suspicion.
131             Even prior to validation you need to make sure the incoming structure is well formed (this is most
132             important when you have deeply nested structures, which can be a major security risk both in parsing
133             and in using that data to do things like update a database). L<Catalyst::TraitFor::Request::StrongParameters>
134             offers a structured approach to whitelisting your incoming POSTed data, as well as a safe way to introduce
135             nested data structures into your classic HTML Form posted data. It is also compatible for POSTed data
136             (such as JSON POSTed data) although in the case of body data such as JSON we merely whitelist the fields
137             and structure since JSON can already support nested data structures.
138              
139             This is similar to a concept called 'strong parameters' in Rails although my implementation is somewhat
140             different based on the varying needs of the L<Catalyst> framework. However I consider this beta code
141             and subject to change should real life use cases arise that indicate a different approach is warranted.
142              
143             =head1 METHODS
144              
145             This role defines the following methods:
146              
147             =head2 strong_body
148              
149             Returns an instance of L<Catalyst::Utils::StrongParameters> preconfigured with the current contents
150             of ->body_parameters. Any arguments are passed to that instances L</permitted> methods before return.
151              
152             =head2 strong_query
153              
154             Parses the URI query string; otherwise same as L</strong_body>.
155              
156             =head2 strong_data
157              
158             The same as L</strong_body> except aimed at body data such as JSON post. Basically works
159             the same except the default for handling array values is to leave them alone rather than to flatten.
160              
161             =head1 PARAMETER OBJECT METHODS
162              
163             The instance of L<Catalyst::Utils::StrongParameters> which is returned by any of the three builder
164             methods above (L</strong_body>, L</strong_query and L</strong_data>) supports the following methods.
165              
166             =head2 namespace (\@fields)
167              
168             Sets the current 'namespace' to start looking for fields and values. Useful when all the fields are
169             under a key. For example if the value of ->body_parameters is:
170              
171             +{
172             'person.name' => 'John',
173             'person.age' => 52,
174             }
175              
176             If you set the namespace to C<['person']> then you can create rule specifications that assume to be
177             'under' that key. See the L</SYNOPSIS> for an example.
178              
179             =head2 permitted (?\@namespace, @rules)
180              
181             An array of rule specifications that are used to filter the current parameters as passed by the user
182             and present a sanitized version that can safely be used in your code.
183              
184             If the first argument is an arrayref, that value is used to set the starting L</namespace>.
185              
186             =head2 required (?\@namespace, @rules)
187              
188             An array of rule specifications that are used to filter the current parameters as passed by the user
189             and present a sanitized version that can safely be used in your code.
190              
191             If user submitted parameters do not match the spec an exception is throw (L<Catalyst::Exception::MissingParameter>
192             If you want to use required parameters then you should add code to catch this error and handle it
193             (see below for more)
194              
195             If the first argument is an arrayref, that value is used to set the starting L</namespace>.
196              
197             =head2 flatten_array_value ($bool)
198              
199             Switch to indicated if you want to flatten any arrayref values to 'pick last'. This is true by default
200             for body and query parameters since its a common hack around quirks with certain types of HTML form controls
201             (like checkboxes) which don't return a value when not selected or checked.
202              
203             =head2 to_hash
204              
205             Returns the currently filtered parameters based on the current permitted and/or required specifications.
206              
207             =head1 CHAINING
208              
209             All the public methods for L<Catalyst::Utils::StrongParameters> return the current instance so that
210             you can chain methods easilu (except for L</to_hash>). If you chain L</permitted> and L</required>
211             the accepted hashrefs are merged.
212              
213             =head1 RULE SPECIFICATIONS
214              
215             L<Catalyst::TraitFor::Request::StrongParameters> offers a concise DSL for describing permitted and required
216             parameters, including flat parameters, hashes, arrays and any combination thereof.
217              
218             Given body_parameters of the following:
219              
220             +{
221             'person.name' => 'John',
222             'person.age' => '52',
223             'person.address.street' => '15604 Harry Lind Road',
224             'person.address.zip' => '78621',
225             'person.email[0]' => 'jjn1056@gmail.com',
226             'person.email[1]' => 'jjn1056@yahoo.com',
227             'person.credit_cards[0].number' => '245345345345345',
228             'person.credit_cards[0].exp' => '2024-01-01',
229             'person.credit_cards[1].number' => '666677777888878',
230             'person.credit_cards[1].exp' => '2024-01-01',
231             'person.credit_cards[].number' => '444444433333',
232             'person.credit_cards[].exp' => '4024-01-01',
233             }
234              
235             my %data = $c->req->strong_body
236             ->namespace(['person'])
237             ->permitted('name','age');
238              
239             # %data = ( name => 'John', age => 53 );
240            
241             my %data = $c->req->strong_body
242             ->namespace(['person'])
243             ->permitted('name','age', address => ['street', 'zip']); # arrayref means the following are subkeys
244              
245             # %data = (
246             # name => 'John',
247             # age => 53,
248             # address => +{
249             # street => '15604 Harry Lind Road',
250             # zip '78621',
251             # }
252             # );
253              
254             my %data = $c->req->strong_body
255             ->namespace(['person'])
256             ->permitted('name','age', +{email => []} ); # wrapping in a hashref mean 'under this is an arrayref
257              
258             # %data = (
259             # name => 'John',
260             # age => 53,
261             # email => ['jjn1056@gmail.com', 'jjn1056@yahoo.com']
262             # );
263            
264             # Combine hashref and arrayref to indicate array of subkeyu
265             my %data = $c->req->strong_body
266             ->namespace(['person'])
267             ->permitted('name','age', +{credit_cards => [qw/number exp/]} );
268              
269             # %data = (
270             # name => 'John',
271             # age => 53,
272             # credit_cards => [
273             # {
274             # number => "245345345345345",
275             # exp => "2024-01-01",
276             # },
277             # {
278             # number => "666677777888878",
279             # exp => "2024-01-01",
280             # },
281             # {
282             # number => "444444433333",
283             # exp => "4024-01-01",
284             # },
285             # ]
286             # );
287              
288             You can specify more than one specification for the same key. For example if body
289             parameters are:
290              
291             +{
292             'person.credit_cards[0].number' => '245345345345345',
293             'person.credit_cards[0].exp' => '2024-01-01',
294             'person.credit_cards[1].number' => '666677777888878',
295             'person.credit_cards[1].exp.year' => '2024',
296             'person.credit_cards[1].exp.month' => '01',
297             }
298              
299             my %data = $c->req->strong_body
300             ->namespace(['person'])
301             ->permitted(+{credit_cards => ['number', 'exp', exp=>[qw/year month/] ]} );
302              
303             # %data = (
304             # credit_cards => [
305             # {
306             # number => "245345345345345",
307             # exp => "2024-01-01",
308             # },
309             # {
310             # number => "666677777888878",
311             # exp => +{
312             # year => '2024',
313             # month => '01'
314             # },
315             # },
316             # ]
317             # );
318              
319              
320             =head2 ARRAY DATA AND ARRAY VALUE FLATTENING
321              
322             Please note this only applies to L</strong_body> / L</strong_query>
323              
324             In the situation when you have a array value for a given namespace specification such as
325             the following :
326              
327             'person.name' => 2,
328             'person.name' => 'John', # flatten array should jsut pick the last one
329              
330             We automatically pick the last POSTed value. This can be a useful hack around some HTML form elements
331             that don't set an 'off' value (like checkboxes).
332              
333             =head2 'EMPTY' FINAL INDEXES
334              
335             Please note this only applies to L</strong_body> / L</strong_query>
336              
337             Since the index values used to sort arrays are not preserved (they indicate order but are not used to
338             set the index since that could open your code to potential hackers) we permit a final 'empty' index:
339              
340             'person.credit_cards[0].number' => '245345345345345',
341             'person.credit_cards[0].exp' => '2024-01-01',
342             'person.credit_cards[1].number' => '666677777888878',
343             'person.credit_cards[1].exp' => '2024-01-01',
344             'person.credit_cards[].number' => '444444433333',
345             'person.credit_cards[].exp' => '4024-01-01',
346              
347             This 'empty' index will always be considered the finall element when sorting
348              
349             =head1 EXCEPTIONS
350              
351             The following exceptions can be raised by these methods and you should add code to recognize and
352             handle them. For example you can add a global or controller scoped 'end' action:
353              
354             sub end :Action {
355             my ($self, $c) = @_;
356             if(my $error = $c->last_error) {
357             $c->clear_errors;
358             if(blessed($error) && $error->isa('Catalyst::Exception::StrongParameter')) {
359             # Handle the error perhaps by forwarding to a view and setting a 4xx
360             # bad request response code.
361             }
362             }
363             }
364              
365             =head2 Exception: Base Class
366              
367             L<Catalyst::Exception::StrongParameter>
368              
369             There's a number of different exceptions that this trait can throw but they all inherit from
370             L<Catalyst::Exception::StrongParameter> so you can just check for that since those are all going
371             to be considered 'Bad Request' type issues.
372              
373             =head2 EXCEPTION: MISSING PARAMETER
374              
375             L<Catalyst::Exception::MissingParameter> ISA L<Catalyst::Exception::StrongParameter>
376              
377             If you use L</required> and a parameter is not present you will raise this exception, which will
378             contain a message indicating the first found missing parameter. For example:
379              
380             "Required parameter 'username' is missing."
381              
382             This will not be an exhaustive list of the missing parameters and this feature in not intended to
383             be used as a sort of form validation system.
384              
385             =head1 AUTHOR
386            
387             John Napiorkowski L<email:jjnapiork@cpan.org>
388            
389             =head1 SEE ALSO
390            
391             L<Catalyst>, L<Catalyst::Request>
392              
393             =head1 COPYRIGHT & LICENSE
394            
395             Copyright 20121, John Napiorkowski L<email:jjnapiork@cpan.org>
396            
397             This library is free software; you can redistribute it and/or modify it under
398             the same terms as Perl itself.
399              
400             =cut