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