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