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