fixed static configured page attribute not being used (RT#56226)
[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;
9use namespace::autoclean;
10
11use Catalyst::Controller::DBIC::API::JoinBuilder;
12
d2739840 13=attribute_private search_validator
14
15A Catalyst::Controller::DBIC::API::Validator instance used solely to validate search parameters
16
17=cut
18
19with 'MooseX::Role::BuildInstanceOf' =>
20{
21 'target' => 'Catalyst::Controller::DBIC::API::Validator',
22 'prefix' => 'search_validator',
23};
24
25=attribute_private select_validator
26
27A Catalyst::Controller::DBIC::API::Validator instance used solely to validate select parameters
28
29=cut
30
31with 'MooseX::Role::BuildInstanceOf' =>
32{
33 'target' => 'Catalyst::Controller::DBIC::API::Validator',
34 'prefix' => 'select_validator',
35};
36
37=attribute_private prefetch_validator
38
39A Catalyst::Controller::DBIC::API::Validator instance used solely to validate prefetch parameters
40
41=cut
42
43with 'MooseX::Role::BuildInstanceOf' =>
44{
45 'target' => 'Catalyst::Controller::DBIC::API::Validator',
46 'prefix' => 'prefetch_validator',
47};
48
49parameter static => ( isa => Bool, default => 0 );
50
51role {
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
66count 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
80page 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
94offset 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
108ordered_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
124grouped_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
140prefetch 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
180prefetch_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
182Like 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
233search_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
235Like 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
255search contains the raw search parameters. Upon setting, a trigger will fire to format them, set search_parameters, and set search_attributes.
256
257Please 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
304search_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
320search_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
335search_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
349select_exposes limits what can actually be selected. Use this to whitelist database functions (such as COUNT).
350
351Like 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
371select 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
373Please 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
405as 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
407Please 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
434joins holds the top level JoinBuilder object used to keep track of joins automagically while formatting complex search parameters.
435
436Provides 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
455request_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
486format_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
506generate_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
548generate_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
561This 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,
4a805f62 577 page => $static->page,
33003023 578 offset => $self->offset,
d2739840 579 join => $self->build_joins,
580 };
581
33003023 582 if($self->has_page)
583 {
584 $search_attributes->{page} = $self->page;
585 }
586 elsif(!$self->has_page && defined($search_attributes->{offset}) && defined($search_attributes->{rows}))
587 {
588 $search_attributes->{page} = $search_attributes->{offset} / $search_attributes->{rows} + 1;
fa2501f0 589 delete $search_attributes->{offset};
33003023 590 }
33003023 591
406086f3 592
593 $search_attributes =
594 {
d2739840 595 map { @$_ }
596 grep
597 {
406086f3 598 defined($_->[1])
599 ?
d2739840 600 (ref($_->[1]) && reftype($_->[1]) eq 'HASH' && keys %{$_->[1]})
601 || (ref($_->[1]) && reftype($_->[1]) eq 'ARRAY' && @{$_->[1]})
602 || length($_->[1])
603 :
604 undef
605 }
606 map { [$_, $search_attributes->{$_}] }
607 keys %$search_attributes
608 };
609
610
611 if ($search_attributes->{page} && !$search_attributes->{rows}) {
612 die 'list_page can only be used with list_count';
613 }
406086f3 614
d2739840 615 if ($search_attributes->{select}) {
616 # make sure all columns have an alias to avoid ambiguous issues
617 # but allow non strings (eg. hashrefs for db procs like 'count')
618 # to pass through unmolested
619 $search_attributes->{select} = [map { (Str->check($_) && $_ !~ m/\./) ? "me.$_" : $_ } (ref $search_attributes->{select}) ? @{$search_attributes->{select}} : $search_attributes->{select}];
620 }
621
622 return $search_attributes;
406086f3 623
d2739840 624 };
625
626};
627=head1 DESCRIPTION
628
629RequestArguments 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.
630
631=cut
632
633
6341;