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 =for Pod::Coverage check_rel
15 =attribute_private search_validator
17 A Catalyst::Controller::DBIC::API::Validator instance used solely to validate search parameters
21 with 'MooseX::Role::BuildInstanceOf' =>
23 'target' => 'Catalyst::Controller::DBIC::API::Validator',
24 'prefix' => 'search_validator',
27 =attribute_private select_validator
29 A Catalyst::Controller::DBIC::API::Validator instance used solely to validate select parameters
33 with 'MooseX::Role::BuildInstanceOf' =>
35 'target' => 'Catalyst::Controller::DBIC::API::Validator',
36 'prefix' => 'select_validator',
39 =attribute_private prefetch_validator
41 A Catalyst::Controller::DBIC::API::Validator instance used solely to validate prefetch parameters
45 with 'MooseX::Role::BuildInstanceOf' =>
47 'target' => 'Catalyst::Controller::DBIC::API::Validator',
48 'prefix' => 'prefetch_validator',
51 parameter static => ( isa => Bool, default => 0 );
59 requires qw/check_has_relation check_column_relation/;
63 requires qw/_controller check_has_relation check_column_relation/;
66 =attribute_public count is: ro, isa: Int
68 count is the number of rows to be returned during paging
75 writer => '_set_count',
77 predicate => 'has_count',
80 =attribute_public page is: ro, isa: Int
82 page is what page to return while paging
89 writer => '_set_page',
91 predicate => 'has_page',
94 =attribute_public offset is ro, isa: Int
96 offset specifies where to start the paged result (think SQL LIMIT)
103 writer => '_set_offset',
105 predicate => 'has_offset',
108 =attribute_public ordered_by is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/OrderedBy>
110 ordered_by is passed to ->search to determine sorting
117 writer => '_set_ordered_by',
119 predicate => 'has_ordered_by',
121 default => sub { $p->static ? [] : undef },
124 =attribute_public groupd_by is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/GroupedBy>
126 grouped_by is passed to ->search to determine aggregate results
133 writer => '_set_grouped_by',
135 predicate => 'has_grouped_by',
137 default => sub { $p->static ? [] : undef },
140 =attribute_public prefetch is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/Prefetch>
142 prefetch is passed to ->search to optimize the number of database fetches for joins
149 writer => '_set_prefetch',
151 default => sub { $p->static ? [] : undef },
155 my ($self, $new) = @_;
156 if($self->has_prefetch_allows and @{$self->prefetch_allows})
158 foreach my $pf (@$new)
160 if(HashRef->check($pf))
162 die qq|'${\Dumper($pf)}' is not an allowed prefetch in: ${\join("\n", @{$self->prefetch_validator->templates})}|
163 unless $self->prefetch_validator->validate($pf)->[0];
167 die qq|'$pf' is not an allowed prefetch in: ${\join("\n", @{$self->prefetch_validator->templates})}|
168 unless $self->prefetch_validator->validate({$pf => 1})->[0];
174 return if not defined($new);
175 die 'Prefetching is not allowed' if @$new;
180 =attribute_public prefetch_allows is: ro, isa: ArrayRef[ArrayRef|Str|HashRef]
182 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.
184 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.
186 prefetch_allows => [ 'cds', { cds => tracks }, { cds => producers } ] # to be explicit
187 prefetch_allows => [ 'cds', { cds => '*' } ] # wildcard means the same thing
191 has prefetch_allows =>
194 writer => '_set_prefetch_allows',
195 isa => ArrayRef[ArrayRef|Str|HashRef],
196 default => sub { [ ] },
197 predicate => 'has_prefetch_allows',
200 my ($self, $new) = @_;
203 my ($self, $rel, $static) = @_;
204 if(ArrayRef->check($rel))
206 foreach my $rel_sub (@$rel)
208 $self->check_rel($rel_sub, $static);
211 elsif(HashRef->check($rel))
213 while(my($k,$v) = each %$rel)
215 $self->check_has_relation($k, $v, undef, $static);
217 $self->prefetch_validator->load($rel);
221 $self->check_has_relation($rel, undef, undef, $static);
222 $self->prefetch_validator->load($rel);
226 foreach my $rel (@$new)
228 $self->check_rel($rel, $p->static);
233 =attribute_public search_exposes is: ro, isa: ArrayRef[Str|HashRef]
235 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.
237 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.
241 has 'search_exposes' =>
244 writer => '_set_search_exposes',
245 isa => ArrayRef[Str|HashRef],
246 predicate => 'has_search_exposes',
247 default => sub { [ ] },
250 my ($self, $new) = @_;
251 $self->search_validator->load($_) for @$new;
255 =attribute_public search is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/SearchParameters>
257 search contains the raw search parameters. Upon setting, a trigger will fire to format them, set search_parameters, and set search_attributes.
259 Please see L</generate_parameters_attributes> for details on how the format works.
266 writer => '_set_search',
267 isa => SearchParameters,
268 predicate => 'has_search',
272 my ($self, $new) = @_;
274 if($self->has_search_exposes and @{$self->search_exposes})
276 foreach my $foo (@$new)
278 while( my ($k, $v) = each %$foo)
280 local $Data::Dumper::Terse = 1;
281 die qq|{ $k => ${\Dumper($v)} } is not an allowed search term in: ${\join("\n", @{$self->search_validator->templates})}|
282 unless $self->search_validator->validate({$k=>$v})->[0];
288 foreach my $foo (@$new)
290 while( my ($k, $v) = each %$foo)
292 $self->check_column_relation({$k => $v});
297 my ($search_parameters, $search_attributes) = $self->generate_parameters_attributes($new);
298 $self->_set_search_parameters($search_parameters);
299 $self->_set_search_attributes($search_attributes);
304 =attribute_public search_parameters is:ro, isa: L<Catalyst::Controller::DBIC::API::Types/SearchParameters>
306 search_parameters stores the formatted search parameters that will be passed to ->search
310 has search_parameters =>
313 isa => SearchParameters,
314 writer => '_set_search_parameters',
315 predicate => 'has_search_parameters',
317 default => sub { [{}] },
320 =attribute_public search_attributes is:ro, isa: HashRef
322 search_attributes stores the formatted search attributes that will be passed to ->search
326 has search_attributes =>
330 writer => '_set_search_attributes',
331 predicate => 'has_search_attributes',
335 =attribute_public search_total_entries is: ro, isa: Int
337 search_total_entries stores the total number of entries in a paged search result
341 has search_total_entries =>
345 writer => '_set_search_total_entries',
346 predicate => 'has_search_total_entries',
349 =attribute_public select_exposes is: ro, isa: ArrayRef[Str|HashRef]
351 select_exposes limits what can actually be selected. Use this to whitelist database functions (such as COUNT).
353 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.
357 has 'select_exposes' =>
360 writer => '_set_select_exposes',
361 isa => ArrayRef[Str|HashRef],
362 predicate => 'has_select_exposes',
363 default => sub { [ ] },
366 my ($self, $new) = @_;
367 $self->select_validator->load($_) for @$new;
371 =attribute_public select is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/SelectColumns>
373 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.
375 Please see L<DBIx::Class::ResultSet/select> for more details.
382 writer => '_set_select',
383 isa => SelectColumns,
384 predicate => 'has_select',
385 default => sub { $p->static ? [] : undef },
389 my ($self, $new) = @_;
390 if($self->has_select_exposes)
392 foreach my $val (@$new)
394 die "'$val' is not allowed in a select"
395 unless $self->select_validator->validate($val);
400 $self->check_column_relation($_, $p->static) for @$new;
405 =attribute_public as is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/AsAliases>
407 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.
409 Please see L<DBIx::Class::ResultSet/as> for more details.
418 default => sub { $p->static ? [] : undef },
421 my ($self, $new) = @_;
422 if($self->has_select)
424 die "'as' argument count (${\scalar(@$new)}) must match 'select' argument count (${\scalar(@{$self->select || []})})"
425 unless @$new == @{$self->select || []};
429 die "'as' is only valid if 'select is also provided'";
434 =attribute_public joins is: ro, isa L<Catalyst::Controller::DBIC::API::Types/JoinBuilder>
436 joins holds the top level JoinBuilder object used to keep track of joins automagically while formatting complex search parameters.
438 Provides a single handle which returns the 'join' attribute for search_attributes:
440 build_joins => 'joins'
451 build_joins => 'joins',
455 =attribute_public request_data is: ro, isa: HashRef
457 request_data holds the raw (but deserialized) data for ths request
461 has 'request_data' =>
465 writer => '_set_request_data',
466 predicate => 'has_request_data',
469 my ($self, $new) = @_;
470 my $controller = $self->_controller;
471 return unless defined($new) && keys %$new;
472 $self->_set_prefetch($new->{$controller->prefetch_arg}) if exists $new->{$controller->prefetch_arg};
473 $self->_set_select($new->{$controller->select_arg}) if exists $new->{$controller->select_arg};
474 $self->_set_as($new->{$controller->as_arg}) if exists $new->{$controller->as_arg};
475 $self->_set_grouped_by($new->{$controller->grouped_by_arg}) if exists $new->{$controller->grouped_by_arg};
476 $self->_set_ordered_by($new->{$controller->ordered_by_arg}) if exists $new->{$controller->ordered_by_arg};
477 $self->_set_count($new->{$controller->count_arg}) if exists $new->{$controller->count_arg};
478 $self->_set_page($new->{$controller->page_arg}) if exists $new->{$controller->page_arg};
479 $self->_set_offset($new->{$controller->offset_arg}) if exists $new->{$controller->offset_arg};
480 $self->_set_search($new->{$controller->search_arg}) if exists $new->{$controller->search_arg};
484 method _build_joins => sub { return Catalyst::Controller::DBIC::API::JoinBuilder->new(name => 'TOP') };
486 =method_protected format_search_parameters
488 format_search_parameters iterates through the provided params ArrayRef, calling generate_column_parameters on each one
492 method format_search_parameters => sub
494 my ($self, $params) = @_;
498 foreach my $param (@$params)
500 push(@$genparams, $self->generate_column_parameters($self->stored_result_source, $param, $self->joins));
506 =method_protected generate_column_parameters
508 generate_column_parameters recursively generates properly aliased parameters for search, building a new JoinBuilder each layer of recursion
512 method generate_column_parameters => sub
514 my ($self, $source, $param, $join, $base) = @_;
516 my $search_params = {};
519 foreach my $column (keys %$param)
521 if($source->has_relationship($column))
523 unless (ref($param->{$column}) && reftype($param->{$column}) eq 'HASH')
525 $search_params->{join('.', $base, $column)} = $param->{$column};
529 $search_params = { %$search_params, %{
530 $self->generate_column_parameters
532 $source->related_source($column),
534 Catalyst::Controller::DBIC::API::JoinBuilder->new(parent => $join, name => $column),
541 $search_params->{join('.', $base, $column)} = $param->{$column};
545 return $search_params;
548 =method_protected generate_parameters_attributes
550 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.
554 method generate_parameters_attributes => sub
556 my ($self, $args) = @_;
558 return ( $self->format_search_parameters($args), $self->search_attributes );
561 =method_protected _build_search_attributes
563 This builder method generates the search attributes
567 method _build_search_attributes => sub
569 my ($self, $args) = @_;
570 my $static = $self->_controller;
571 my $search_attributes =
573 group_by => $self->grouped_by || ((scalar(@{$static->grouped_by})) ? $static->grouped_by : undef),
574 order_by => $self->ordered_by || ((scalar(@{$static->ordered_by})) ? $static->ordered_by : undef),
575 select => $self->select || ((scalar(@{$static->select})) ? $static->select : undef),
576 as => $self->as || ((scalar(@{$static->as})) ? $static->as : undef),
577 prefetch => $self->prefetch || $static->prefetch || undef,
578 rows => $self->count || $static->count,
579 offset => $self->offset,
580 join => $self->build_joins,
585 $search_attributes->{page} = $self->page;
587 elsif(!$self->has_page && defined($search_attributes->{offset}) && defined($search_attributes->{rows}))
589 $search_attributes->{page} = $search_attributes->{offset} / $search_attributes->{rows} + 1;
590 delete $search_attributes->{offset};
601 (ref($_->[1]) && reftype($_->[1]) eq 'HASH' && keys %{$_->[1]})
602 || (ref($_->[1]) && reftype($_->[1]) eq 'ARRAY' && @{$_->[1]})
607 map { [$_, $search_attributes->{$_}] }
608 keys %$search_attributes
612 if ($search_attributes->{page} && !$search_attributes->{rows}) {
613 die 'list_page can only be used with list_count';
616 if ($search_attributes->{select}) {
617 # make sure all columns have an alias to avoid ambiguous issues
618 # but allow non strings (eg. hashrefs for db procs like 'count')
619 # to pass through unmolested
620 $search_attributes->{select} = [map { (Str->check($_) && $_ !~ m/\./) ? "me.$_" : $_ } (ref $search_attributes->{select}) ? @{$search_attributes->{select}} : $search_attributes->{select}];
623 return $search_attributes;
630 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.