Commit | Line | Data |
d2739840 |
1 | package Catalyst::Controller::DBIC::API::RequestArguments; |
2 | |
3 | #ABSTRACT: Provides Request argument validation |
4 | use MooseX::Role::Parameterized; |
5 | use Catalyst::Controller::DBIC::API::Types(':all'); |
6 | use MooseX::Types::Moose(':all'); |
7 | use Scalar::Util('reftype'); |
8 | use Data::Dumper; |
9 | use namespace::autoclean; |
10 | |
11 | use Catalyst::Controller::DBIC::API::JoinBuilder; |
12 | |
13 | |
14 | =attribute_private search_validator |
15 | |
16 | A Catalyst::Controller::DBIC::API::Validator instance used solely to validate search parameters |
17 | |
18 | =cut |
19 | |
20 | with 'MooseX::Role::BuildInstanceOf' => |
21 | { |
22 | 'target' => 'Catalyst::Controller::DBIC::API::Validator', |
23 | 'prefix' => 'search_validator', |
24 | }; |
25 | |
26 | =attribute_private select_validator |
27 | |
28 | A Catalyst::Controller::DBIC::API::Validator instance used solely to validate select parameters |
29 | |
30 | =cut |
31 | |
32 | with 'MooseX::Role::BuildInstanceOf' => |
33 | { |
34 | 'target' => 'Catalyst::Controller::DBIC::API::Validator', |
35 | 'prefix' => 'select_validator', |
36 | }; |
37 | |
38 | =attribute_private prefetch_validator |
39 | |
40 | A Catalyst::Controller::DBIC::API::Validator instance used solely to validate prefetch parameters |
41 | |
42 | =cut |
43 | |
44 | with 'MooseX::Role::BuildInstanceOf' => |
45 | { |
46 | 'target' => 'Catalyst::Controller::DBIC::API::Validator', |
47 | 'prefix' => 'prefetch_validator', |
48 | }; |
49 | |
50 | parameter static => ( isa => Bool, default => 0 ); |
51 | |
52 | role { |
53 | |
54 | my $p = shift; |
55 | |
56 | if($p->static) |
57 | { |
58 | requires qw/check_has_relation check_column_relation/; |
59 | } |
60 | else |
61 | { |
62 | requires qw/_controller check_has_relation check_column_relation/; |
63 | } |
64 | |
65 | =attribute_public count is: ro, isa: Int |
66 | |
67 | count is the number of rows to be returned during paging |
68 | |
69 | =cut |
70 | |
71 | has 'count' => |
72 | ( |
73 | is => 'ro', |
74 | writer => '_set_count', |
75 | isa => Int, |
76 | predicate => 'has_count', |
77 | ); |
78 | |
79 | =attribute_public page is: ro, isa: Int |
80 | |
81 | page is what page to return while paging |
82 | |
83 | =cut |
84 | |
85 | has 'page' => |
86 | ( |
87 | is => 'ro', |
88 | writer => '_set_page', |
89 | isa => Int, |
90 | predicate => 'has_page', |
91 | ); |
92 | |
93 | =attribute_public ordered_by is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/OrderedBy> |
94 | |
95 | ordered_by is passed to ->search to determine sorting |
96 | |
97 | =cut |
98 | |
99 | has 'ordered_by' => |
100 | ( |
101 | is => 'ro', |
102 | writer => '_set_ordered_by', |
103 | isa => OrderedBy, |
104 | predicate => 'has_ordered_by', |
105 | coerce => 1, |
106 | default => sub { $p->static ? [] : undef }, |
107 | ); |
108 | |
109 | =attribute_public groupd_by is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/GroupedBy> |
110 | |
111 | grouped_by is passed to ->search to determine aggregate results |
112 | |
113 | =cut |
114 | |
115 | has 'grouped_by' => |
116 | ( |
117 | is => 'ro', |
118 | writer => '_set_grouped_by', |
119 | isa => GroupedBy, |
120 | predicate => 'has_grouped_by', |
121 | coerce => 1, |
122 | default => sub { $p->static ? [] : undef }, |
123 | ); |
124 | |
125 | =attribute_public prefetch is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/Prefetch> |
126 | |
127 | prefetch is passed to ->search to optimize the number of database fetches for joins |
128 | |
129 | =cut |
130 | |
131 | has prefetch => |
132 | ( |
133 | is => 'ro', |
134 | writer => '_set_prefetch', |
135 | isa => Prefetch, |
136 | default => sub { $p->static ? [] : undef }, |
137 | coerce => 1, |
138 | trigger => sub |
139 | { |
140 | my ($self, $new) = @_; |
141 | if($self->has_prefetch_allows and @{$self->prefetch_allows}) |
142 | { |
143 | foreach my $pf (@$new) |
144 | { |
145 | if(HashRef->check($pf)) |
146 | { |
147 | die qq|'${\Dumper($pf)}' is not an allowed prefetch in: ${\join("\n", @{$self->prefetch_validator->templates})}| |
148 | unless $self->prefetch_validator->validate($pf)->[0]; |
149 | } |
150 | else |
151 | { |
152 | die qq|'$pf' is not an allowed prefetch in: ${\join("\n", @{$self->prefetch_validator->templates})}| |
153 | unless $self->prefetch_validator->validate({$pf => 1})->[0]; |
154 | } |
155 | } |
156 | } |
157 | else |
158 | { |
159 | return if not defined($new); |
160 | die 'Prefetching is not allowed' if @$new; |
161 | } |
162 | }, |
163 | ); |
164 | |
165 | =attribute_public prefetch_allows is: ro, isa: ArrayRef[ArrayRef|Str|HashRef] |
166 | |
167 | prefetch_allows limits what relations may be prefetched when executing searches with joins. This is necessary to avoid denial of service attacks in form of queries which would return a large number of data and unwanted disclosure of data. |
168 | |
169 | Like the synopsis in DBIC::API shows, you can declare a "template" of what is allowed (by using an '*'). Each element passed in, will be converted into a Data::DPath and added to the validator. |
170 | |
171 | prefetch_allows => [ 'cds', { cds => tracks }, { cds => producers } ] # to be explicit |
172 | prefetch_allows => [ 'cds', { cds => '*' } ] # wildcard means the same thing |
173 | |
174 | =cut |
175 | |
176 | has prefetch_allows => |
177 | ( |
178 | is => 'ro', |
179 | writer => '_set_prefetch_allows', |
180 | isa => ArrayRef[ArrayRef|Str|HashRef], |
181 | default => sub { [ ] }, |
182 | predicate => 'has_prefetch_allows', |
183 | trigger => sub |
184 | { |
185 | my ($self, $new) = @_; |
186 | |
187 | sub check_rel { |
188 | my ($self, $rel, $static) = @_; |
189 | if(ArrayRef->check($rel)) |
190 | { |
191 | foreach my $rel_sub (@$rel) |
192 | { |
193 | $self->check_rel($rel_sub, $static); |
194 | } |
195 | } |
196 | elsif(HashRef->check($rel)) |
197 | { |
198 | while(my($k,$v) = each %$rel) |
199 | { |
200 | $self->check_has_relation($k, $v, undef, $static); |
201 | } |
202 | $self->prefetch_validator->load($rel); |
203 | } |
204 | else |
205 | { |
206 | $self->check_has_relation($rel, undef, undef, $static); |
207 | $self->prefetch_validator->load($rel); |
208 | } |
209 | } |
210 | |
211 | foreach my $rel (@$new) |
212 | { |
213 | $self->check_rel($rel, $p->static); |
214 | } |
215 | }, |
216 | ); |
217 | |
218 | =attribute_public search_exposes is: ro, isa: ArrayRef[Str|HashRef] |
219 | |
220 | search_exposes limits what can actually be searched. If a certain column isn't indexed or perhaps a BLOB, you can explicitly say which columns can be search and exclude that one. |
221 | |
222 | Like the synopsis in DBIC::API shows, you can declare a "template" of what is allowed (by using an '*'). Each element passed in, will be converted into a Data::DPath and added to the validator. |
223 | |
224 | =cut |
225 | |
226 | has 'search_exposes' => |
227 | ( |
228 | is => 'ro', |
229 | writer => '_set_search_exposes', |
230 | isa => ArrayRef[Str|HashRef], |
231 | predicate => 'has_search_exposes', |
232 | default => sub { [ ] }, |
233 | trigger => sub |
234 | { |
235 | my ($self, $new) = @_; |
236 | $self->search_validator->load($_) for @$new; |
237 | }, |
238 | ); |
239 | |
d666a194 |
240 | =attribute_public search is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/SearchParameters> |
d2739840 |
241 | |
242 | search contains the raw search parameters. Upon setting, a trigger will fire to format them, set search_parameters, and set search_attributes. |
243 | |
244 | Please see L</generate_parameters_attributes> for details on how the format works. |
245 | |
246 | =cut |
247 | |
248 | has 'search' => |
249 | ( |
250 | is => 'ro', |
251 | writer => '_set_search', |
252 | isa => SearchParameters, |
253 | predicate => 'has_search', |
254 | coerce => 1, |
255 | trigger => sub |
256 | { |
257 | my ($self, $new) = @_; |
258 | |
259 | if($self->has_search_exposes and @{$self->search_exposes}) |
260 | { |
261 | foreach my $foo (@$new) |
262 | { |
263 | while( my ($k, $v) = each %$foo) |
264 | { |
265 | local $Data::Dumper::Terse = 1; |
266 | die qq|{ $k => ${\Dumper($v)} } is not an allowed search term in: ${\join("\n", @{$self->search_validator->templates})}| |
267 | unless $self->search_validator->validate({$k=>$v})->[0]; |
268 | } |
269 | } |
270 | } |
271 | else |
272 | { |
273 | foreach my $foo (@$new) |
274 | { |
275 | while( my ($k, $v) = each %$foo) |
276 | { |
277 | $self->check_column_relation({$k => $v}); |
278 | } |
279 | } |
280 | } |
281 | |
282 | my ($search_parameters, $search_attributes) = $self->generate_parameters_attributes($new); |
283 | $self->_set_search_parameters($search_parameters); |
284 | $self->_set_search_attributes($search_attributes); |
285 | |
286 | }, |
287 | ); |
288 | |
d666a194 |
289 | =attribute_public search_parameters is:ro, isa: L<Catalyst::Controller::DBIC::API::Types/SearchParameters> |
d2739840 |
290 | |
291 | search_parameters stores the formatted search parameters that will be passed to ->search |
292 | |
293 | =cut |
294 | |
295 | has search_parameters => |
296 | ( |
297 | is => 'ro', |
298 | isa => SearchParameters, |
299 | writer => '_set_search_parameters', |
300 | predicate => 'has_search_parameters', |
301 | coerce => 1, |
302 | default => sub { [{}] }, |
303 | ); |
304 | |
305 | =attribute_public search_attributes is:ro, isa: HashRef |
306 | |
307 | search_attributes stores the formatted search attributes that will be passed to ->search |
308 | |
309 | =cut |
310 | |
311 | has search_attributes => |
312 | ( |
313 | is => 'ro', |
314 | isa => HashRef, |
315 | writer => '_set_search_attributes', |
316 | predicate => 'has_search_attributes', |
317 | lazy_build => 1, |
318 | ); |
319 | |
320 | =attribute_public search_total_entries is: ro, isa: Int |
321 | |
322 | search_total_entries stores the total number of entries in a paged search result |
323 | |
324 | =cut |
325 | |
326 | has search_total_entries => |
327 | ( |
328 | is => 'ro', |
329 | isa => Int, |
330 | writer => '_set_search_total_entries', |
331 | predicate => 'has_search_total_entries', |
332 | ); |
333 | |
334 | =attribute_public select_exposes is: ro, isa: ArrayRef[Str|HashRef] |
335 | |
336 | select_exposes limits what can actually be selected. Use this to whitelist database functions (such as COUNT). |
337 | |
338 | Like the synopsis in DBIC::API shows, you can declare a "template" of what is allowed (by using an '*'). Each element passed in, will be converted into a Data::DPath and added to the validator. |
339 | |
340 | =cut |
341 | |
342 | has 'select_exposes' => |
343 | ( |
344 | is => 'ro', |
345 | writer => '_set_select_exposes', |
346 | isa => ArrayRef[Str|HashRef], |
347 | predicate => 'has_select_exposes', |
348 | default => sub { [ ] }, |
349 | trigger => sub |
350 | { |
351 | my ($self, $new) = @_; |
352 | $self->select_validator->load($_) for @$new; |
353 | }, |
354 | ); |
355 | |
356 | =attribute_public select is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/SelectColumns> |
357 | |
358 | select is the search attribute that allows you to both limit what is returned in the result set, and also make use of database functions like COUNT. |
359 | |
360 | Please see L<DBIx::Class::ResultSet/select> for more details. |
361 | |
362 | =cut |
363 | |
364 | has select => |
365 | ( |
366 | is => 'ro', |
367 | writer => '_set_select', |
368 | isa => SelectColumns, |
369 | predicate => 'has_select', |
370 | default => sub { $p->static ? [] : undef }, |
371 | coerce => 1, |
372 | trigger => sub |
373 | { |
374 | my ($self, $new) = @_; |
375 | if($self->has_select_exposes) |
376 | { |
377 | foreach my $val (@$new) |
378 | { |
379 | die "'$val' is not allowed in a select" |
380 | unless $self->select_validator->validate($val); |
381 | } |
382 | } |
383 | else |
384 | { |
385 | $self->check_column_relation($_, $p->static) for @$new; |
386 | } |
387 | }, |
388 | ); |
389 | |
390 | =attribute_public as is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/AsAliases> |
391 | |
392 | as is the search attribute compliment to L</select> that allows you to label columns for object inflaction and actually reference database functions like COUNT. |
393 | |
394 | Please see L<DBIx::Class::ResultSet/as> for more details. |
395 | |
396 | =cut |
397 | |
398 | has as => |
399 | ( |
400 | is => 'ro', |
401 | writer => '_set_as', |
402 | isa => AsAliases, |
403 | default => sub { $p->static ? [] : undef }, |
404 | trigger => sub |
405 | { |
406 | my ($self, $new) = @_; |
407 | if($self->has_select) |
408 | { |
409 | die "'as' argument count (${\scalar(@$new)}) must match 'select' argument count (${\scalar(@{$self->select || []})})" |
410 | unless @$new == @{$self->select || []}; |
411 | } |
412 | elsif(defined $new) |
413 | { |
414 | die "'as' is only valid if 'select is also provided'"; |
415 | } |
416 | } |
417 | ); |
418 | |
419 | =attribute_public joins is: ro, isa L<Catalyst::Controller::DBIC::API::Types/JoinBuilder> |
420 | |
421 | joins holds the top level JoinBuilder object used to keep track of joins automagically while formatting complex search parameters. |
422 | |
423 | Provides a single handle which returns the 'join' attribute for search_attributes: |
424 | |
425 | build_joins => 'joins' |
426 | |
427 | =cut |
428 | |
429 | has joins => |
430 | ( |
431 | is => 'ro', |
432 | isa => JoinBuilder, |
433 | lazy_build => 1, |
434 | handles => |
435 | { |
436 | build_joins => 'joins', |
437 | } |
438 | ); |
439 | |
440 | =attribute_public request_data is: ro, isa: HashRef |
441 | |
442 | request_data holds the raw (but deserialized) data for ths request |
443 | |
444 | =cut |
445 | |
446 | has 'request_data' => |
447 | ( |
448 | is => 'ro', |
449 | isa => HashRef, |
450 | writer => '_set_request_data', |
451 | trigger => sub |
452 | { |
453 | my ($self, $new) = @_; |
454 | my $controller = $self->_controller; |
455 | $self->_set_prefetch($new->{$controller->prefetch_arg}) if exists $new->{$controller->prefetch_arg}; |
456 | $self->_set_select($new->{$controller->select_arg}) if exists $new->{$controller->select_arg}; |
457 | $self->_set_as($new->{$controller->as_arg}) if exists $new->{$controller->as_arg}; |
458 | $self->_set_grouped_by($new->{$controller->grouped_by_arg}) if exists $new->{$controller->grouped_by_arg}; |
459 | $self->_set_ordered_by($new->{$controller->ordered_by_arg}) if exists $new->{$controller->ordered_by_arg}; |
460 | $self->_set_search($new->{$controller->search_arg}) if exists $new->{$controller->search_arg}; |
461 | $self->_set_count($new->{$controller->count_arg}) if exists $new->{$controller->count_arg}; |
462 | $self->_set_page($new->{$controller->page_arg}) if exists $new->{$controller->page_arg}; |
463 | } |
464 | ); |
465 | |
466 | method _build_joins => sub { return Catalyst::Controller::DBIC::API::JoinBuilder->new(name => 'TOP') }; |
467 | |
468 | =method_protected format_search_parameters |
469 | |
470 | format_search_parameters iterates through the provided params ArrayRef, calling generate_column_parameters on each one |
471 | |
472 | =cut |
473 | |
474 | method format_search_parameters => sub |
475 | { |
476 | $DB::single = 1; |
477 | my ($self, $params) = @_; |
478 | |
479 | my $genparams = []; |
480 | |
481 | foreach my $param (@$params) |
482 | { |
483 | push(@$genparams, $self->generate_column_parameters($self->stored_result_source, $param, $self->joins)); |
484 | } |
485 | |
486 | return $genparams; |
487 | }; |
488 | |
489 | =method_protected generate_column_parameters |
490 | |
491 | generate_column_parameters recursively generates properly aliased parameters for search, building a new JoinBuilder each layer of recursion |
492 | |
493 | =cut |
494 | |
495 | method generate_column_parameters => sub |
496 | { |
497 | $DB::single = 1; |
498 | my ($self, $source, $param, $join, $base) = @_; |
499 | $base ||= 'me'; |
500 | my $search_params; |
501 | |
502 | # build up condition |
503 | foreach my $column (keys %$param) |
504 | { |
505 | if($source->has_relationship($column)) |
506 | { |
507 | unless (ref($param->{$column}) && reftype($param->{$column}) eq 'HASH') |
508 | { |
509 | $search_params->{join('.', $base, $column)} = $param->{$column}; |
510 | next; |
511 | } |
512 | |
513 | %$search_params = |
514 | %{ |
515 | $self->generate_column_parameters |
516 | ( |
517 | $source->related_source($column), |
518 | $param->{$column}, |
519 | Catalyst::Controller::DBIC::API::JoinBuilder->new(parent => $join, name => $column), |
520 | $column |
521 | ) |
522 | }; |
523 | } |
524 | else |
525 | { |
526 | $search_params->{join('.', $base, $column)} = $param->{$column}; |
527 | } |
528 | } |
529 | |
530 | return $search_params; |
531 | }; |
532 | |
533 | =method_protected generate_parameters_attributes |
534 | |
535 | generate_parameters_attributes takes the raw search arguments and formats the parameters by calling format_search_parameters. Then builds the related attributes, preferring request-provided arguments for things like grouped_by over statically configured options. Finally tacking on the appropriate joins. Returns both formatted search parameters and the search attributes. |
536 | |
537 | =cut |
538 | |
539 | method generate_parameters_attributes => sub |
540 | { |
541 | $DB::single = 1; |
542 | my ($self, $args) = @_; |
543 | |
544 | return ( $self->format_search_parameters($args), $self->search_attributes ); |
545 | }; |
546 | |
547 | =method_protected _build_search_attributes |
548 | |
549 | This builder method generates the search attributes |
550 | |
551 | =cut |
552 | |
553 | method _build_search_attributes => sub |
554 | { |
555 | my ($self, $args) = @_; |
556 | my $static = $self->_controller; |
557 | my $search_attributes = |
558 | { |
559 | group_by => $self->grouped_by || ((scalar(@{$static->grouped_by})) ? $static->grouped_by : undef), |
560 | order_by => $self->ordered_by || ((scalar(@{$static->ordered_by})) ? $static->ordered_by : undef), |
561 | select => $self->select || ((scalar(@{$static->select})) ? $static->select : undef), |
562 | as => $self->as || ((scalar(@{$static->as})) ? $static->as : undef), |
563 | prefetch => $self->prefetch || $static->prefetch || undef, |
564 | rows => $self->count || $static->count, |
565 | page => $self->page, |
566 | join => $self->build_joins, |
567 | }; |
568 | |
569 | $search_attributes = |
570 | { |
571 | map { @$_ } |
572 | grep |
573 | { |
574 | defined($_->[1]) |
575 | ? |
576 | (ref($_->[1]) && reftype($_->[1]) eq 'HASH' && keys %{$_->[1]}) |
577 | || (ref($_->[1]) && reftype($_->[1]) eq 'ARRAY' && @{$_->[1]}) |
578 | || length($_->[1]) |
579 | : |
580 | undef |
581 | } |
582 | map { [$_, $search_attributes->{$_}] } |
583 | keys %$search_attributes |
584 | }; |
585 | |
586 | |
587 | if ($search_attributes->{page} && !$search_attributes->{rows}) { |
588 | die 'list_page can only be used with list_count'; |
589 | } |
590 | |
591 | if ($search_attributes->{select}) { |
592 | # make sure all columns have an alias to avoid ambiguous issues |
593 | # but allow non strings (eg. hashrefs for db procs like 'count') |
594 | # to pass through unmolested |
595 | $search_attributes->{select} = [map { (Str->check($_) && $_ !~ m/\./) ? "me.$_" : $_ } (ref $search_attributes->{select}) ? @{$search_attributes->{select}} : $search_attributes->{select}]; |
596 | } |
597 | |
598 | return $search_attributes; |
599 | |
600 | }; |
601 | |
602 | }; |
603 | =head1 DESCRIPTION |
604 | |
605 | RequestArguments embodies those arguments that are provided as part of a request or effect validation on request arguments. This Role can be consumed in one of two ways. As this is a parameterized Role, it accepts a single argument at composition time: 'static'. This indicates that those parameters should be stored statically and used as a fallback when the current request doesn't provide them. |
606 | |
607 | =cut |
608 | |
609 | |
610 | 1; |