1 package Catalyst::Controller::DBIC::API::RequestArguments;
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');
9 use namespace::autoclean;
11 use Catalyst::Controller::DBIC::API::JoinBuilder;
13 =attribute_private search_validator
15 A Catalyst::Controller::DBIC::API::Validator instance used solely to validate search parameters
19 with 'MooseX::Role::BuildInstanceOf' =>
21 'target' => 'Catalyst::Controller::DBIC::API::Validator',
22 'prefix' => 'search_validator',
25 =attribute_private select_validator
27 A Catalyst::Controller::DBIC::API::Validator instance used solely to validate select parameters
31 with 'MooseX::Role::BuildInstanceOf' =>
33 'target' => 'Catalyst::Controller::DBIC::API::Validator',
34 'prefix' => 'select_validator',
37 =attribute_private prefetch_validator
39 A Catalyst::Controller::DBIC::API::Validator instance used solely to validate prefetch parameters
43 with 'MooseX::Role::BuildInstanceOf' =>
45 'target' => 'Catalyst::Controller::DBIC::API::Validator',
46 'prefix' => 'prefetch_validator',
49 parameter static => ( isa => Bool, default => 0 );
57 requires qw/check_has_relation check_column_relation/;
61 requires qw/_controller check_has_relation check_column_relation/;
64 =attribute_public count is: ro, isa: Int
66 count is the number of rows to be returned during paging
73 writer => '_set_count',
75 predicate => 'has_count',
78 =attribute_public page is: ro, isa: Int
80 page is what page to return while paging
87 writer => '_set_page',
89 predicate => 'has_page',
92 =attribute_public offset is ro, isa: Int
94 offset specifies where to start the paged result (think SQL LIMIT)
101 writer => '_set_offset',
103 predicate => 'has_offset',
106 =attribute_public ordered_by is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/OrderedBy>
108 ordered_by is passed to ->search to determine sorting
115 writer => '_set_ordered_by',
117 predicate => 'has_ordered_by',
119 default => sub { $p->static ? [] : undef },
122 =attribute_public groupd_by is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/GroupedBy>
124 grouped_by is passed to ->search to determine aggregate results
131 writer => '_set_grouped_by',
133 predicate => 'has_grouped_by',
135 default => sub { $p->static ? [] : undef },
138 =attribute_public prefetch is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/Prefetch>
140 prefetch is passed to ->search to optimize the number of database fetches for joins
147 writer => '_set_prefetch',
149 default => sub { $p->static ? [] : undef },
153 my ($self, $new) = @_;
154 if($self->has_prefetch_allows and @{$self->prefetch_allows})
156 foreach my $pf (@$new)
158 if(HashRef->check($pf))
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];
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];
172 return if not defined($new);
173 die 'Prefetching is not allowed' if @$new;
178 =attribute_public prefetch_allows is: ro, isa: ArrayRef[ArrayRef|Str|HashRef]
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.
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.
184 prefetch_allows => [ 'cds', { cds => tracks }, { cds => producers } ] # to be explicit
185 prefetch_allows => [ 'cds', { cds => '*' } ] # wildcard means the same thing
189 has prefetch_allows =>
192 writer => '_set_prefetch_allows',
193 isa => ArrayRef[ArrayRef|Str|HashRef],
194 default => sub { [ ] },
195 predicate => 'has_prefetch_allows',
198 my ($self, $new) = @_;
201 my ($self, $rel, $static) = @_;
202 if(ArrayRef->check($rel))
204 foreach my $rel_sub (@$rel)
206 $self->_check_rel($rel_sub, $static);
209 elsif(HashRef->check($rel))
211 while(my($k,$v) = each %$rel)
213 $self->check_has_relation($k, $v, undef, $static);
215 $self->prefetch_validator->load($rel);
219 $self->check_has_relation($rel, undef, undef, $static);
220 $self->prefetch_validator->load($rel);
224 foreach my $rel (@$new)
226 $self->_check_rel($rel, $p->static);
231 =attribute_public search_exposes is: ro, isa: ArrayRef[Str|HashRef]
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.
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.
239 has 'search_exposes' =>
242 writer => '_set_search_exposes',
243 isa => ArrayRef[Str|HashRef],
244 predicate => 'has_search_exposes',
245 default => sub { [ ] },
248 my ($self, $new) = @_;
249 $self->search_validator->load($_) for @$new;
253 =attribute_public search is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/SearchParameters>
255 search contains the raw search parameters. Upon setting, a trigger will fire to format them, set search_parameters, and set search_attributes.
257 Please see L</generate_parameters_attributes> for details on how the format works.
264 writer => '_set_search',
265 isa => SearchParameters,
266 predicate => 'has_search',
270 my ($self, $new) = @_;
272 if($self->has_search_exposes and @{$self->search_exposes})
274 foreach my $foo (@$new)
276 while( my ($k, $v) = each %$foo)
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];
286 foreach my $foo (@$new)
288 while( my ($k, $v) = each %$foo)
290 $self->check_column_relation({$k => $v});
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);
302 =attribute_public search_parameters is:ro, isa: L<Catalyst::Controller::DBIC::API::Types/SearchParameters>
304 search_parameters stores the formatted search parameters that will be passed to ->search
308 has search_parameters =>
311 isa => SearchParameters,
312 writer => '_set_search_parameters',
313 predicate => 'has_search_parameters',
315 default => sub { [{}] },
318 =attribute_public search_attributes is:ro, isa: HashRef
320 search_attributes stores the formatted search attributes that will be passed to ->search
324 has search_attributes =>
328 writer => '_set_search_attributes',
329 predicate => 'has_search_attributes',
333 =attribute_public search_total_entries is: ro, isa: Int
335 search_total_entries stores the total number of entries in a paged search result
339 has search_total_entries =>
343 writer => '_set_search_total_entries',
344 predicate => 'has_search_total_entries',
347 =attribute_public select_exposes is: ro, isa: ArrayRef[Str|HashRef]
349 select_exposes limits what can actually be selected. Use this to whitelist database functions (such as COUNT).
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.
355 has 'select_exposes' =>
358 writer => '_set_select_exposes',
359 isa => ArrayRef[Str|HashRef],
360 predicate => 'has_select_exposes',
361 default => sub { [ ] },
364 my ($self, $new) = @_;
365 $self->select_validator->load($_) for @$new;
369 =attribute_public select is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/SelectColumns>
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.
373 Please see L<DBIx::Class::ResultSet/select> for more details.
380 writer => '_set_select',
381 isa => SelectColumns,
382 predicate => 'has_select',
383 default => sub { $p->static ? [] : undef },
387 my ($self, $new) = @_;
388 if($self->has_select_exposes)
390 foreach my $val (@$new)
392 die "'$val' is not allowed in a select"
393 unless $self->select_validator->validate($val);
398 $self->check_column_relation($_, $p->static) for @$new;
403 =attribute_public as is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/AsAliases>
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.
407 Please see L<DBIx::Class::ResultSet/as> for more details.
416 default => sub { $p->static ? [] : undef },
419 my ($self, $new) = @_;
420 if($self->has_select)
422 die "'as' argument count (${\scalar(@$new)}) must match 'select' argument count (${\scalar(@{$self->select || []})})"
423 unless @$new == @{$self->select || []};
427 die "'as' is only valid if 'select is also provided'";
432 =attribute_public joins is: ro, isa L<Catalyst::Controller::DBIC::API::Types/JoinBuilder>
434 joins holds the top level JoinBuilder object used to keep track of joins automagically while formatting complex search parameters.
436 Provides a single handle which returns the 'join' attribute for search_attributes:
438 build_joins => 'joins'
449 build_joins => 'joins',
453 =attribute_public request_data is: ro, isa: HashRef
455 request_data holds the raw (but deserialized) data for ths request
459 has 'request_data' =>
463 writer => '_set_request_data',
464 predicate => 'has_request_data',
467 my ($self, $new) = @_;
468 my $controller = $self->_controller;
469 return unless defined($new) && keys %$new;
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};
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};
477 $self->_set_offset($new->{$controller->offset_arg}) if exists $new->{$controller->offset_arg};
478 $self->_set_search($new->{$controller->search_arg}) if exists $new->{$controller->search_arg};
482 method _build_joins => sub { return Catalyst::Controller::DBIC::API::JoinBuilder->new(name => 'TOP') };
484 =method_protected format_search_parameters
486 format_search_parameters iterates through the provided params ArrayRef, calling generate_column_parameters on each one
490 method format_search_parameters => sub
492 my ($self, $params) = @_;
496 foreach my $param (@$params)
498 push(@$genparams, $self->generate_column_parameters($self->stored_result_source, $param, $self->joins));
504 =method_protected generate_column_parameters
506 generate_column_parameters recursively generates properly aliased parameters for search, building a new JoinBuilder each layer of recursion
510 method generate_column_parameters => sub
512 my ($self, $source, $param, $join, $base) = @_;
514 my $search_params = {};
517 foreach my $column (keys %$param)
519 if($source->has_relationship($column))
521 unless (ref($param->{$column}) && reftype($param->{$column}) eq 'HASH')
523 $search_params->{join('.', $base, $column)} = $param->{$column};
527 $search_params = { %$search_params, %{
528 $self->generate_column_parameters
530 $source->related_source($column),
532 Catalyst::Controller::DBIC::API::JoinBuilder->new(parent => $join, name => $column),
539 $search_params->{join('.', $base, $column)} = $param->{$column};
543 return $search_params;
546 =method_protected generate_parameters_attributes
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.
552 method generate_parameters_attributes => sub
554 my ($self, $args) = @_;
556 return ( $self->format_search_parameters($args), $self->search_attributes );
559 =method_protected _build_search_attributes
561 This builder method generates the search attributes
565 method _build_search_attributes => sub
567 my ($self, $args) = @_;
568 my $static = $self->_controller;
569 my $search_attributes =
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,
577 offset => $self->offset,
578 join => $self->build_joins,
583 $search_attributes->{page} = $self->page;
585 elsif(!$self->has_page && defined($search_attributes->{offset}) && defined($search_attributes->{rows}))
587 $search_attributes->{page} = $search_attributes->{offset} / $search_attributes->{rows} + 1;
588 delete $search_attributes->{offset};
599 (ref($_->[1]) && reftype($_->[1]) eq 'HASH' && keys %{$_->[1]})
600 || (ref($_->[1]) && reftype($_->[1]) eq 'ARRAY' && @{$_->[1]})
605 map { [$_, $search_attributes->{$_}] }
606 keys %$search_attributes
610 if ($search_attributes->{page} && !$search_attributes->{rows}) {
611 die 'list_page can only be used with list_count';
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}];
621 return $search_attributes;
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.