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 Catalyst::Controller::DBIC::API::Validator;
10 use namespace::autoclean;
12 use Catalyst::Controller::DBIC::API::JoinBuilder;
14 =attribute_private search_validator
16 A Catalyst::Controller::DBIC::API::Validator instance used solely to validate search parameters
20 =attribute_private select_validator
22 A Catalyst::Controller::DBIC::API::Validator instance used solely to validate select parameters
26 =attribute_private prefetch_validator
28 A Catalyst::Controller::DBIC::API::Validator instance used solely to validate prefetch parameters
32 has [qw( search_validator select_validator prefetch_validator )] => (
34 isa => 'Catalyst::Controller::DBIC::API::Validator',
36 builder => '_build_validator',
39 sub _build_validator {
40 return Catalyst::Controller::DBIC::API::Validator->new;
43 parameter static => ( isa => Bool, default => 0 );
51 requires qw/check_has_relation check_column_relation/;
55 requires qw/_controller check_has_relation check_column_relation/;
58 =attribute_public count is: ro, isa: Int
60 count is the number of rows to be returned during paging
67 writer => '_set_count',
69 predicate => 'has_count',
72 =attribute_public page is: ro, isa: Int
74 page is what page to return while paging
81 writer => '_set_page',
83 predicate => 'has_page',
86 =attribute_public offset is ro, isa: Int
88 offset specifies where to start the paged result (think SQL LIMIT)
95 writer => '_set_offset',
97 predicate => 'has_offset',
100 =attribute_public ordered_by is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/OrderedBy>
102 ordered_by is passed to ->search to determine sorting
109 writer => '_set_ordered_by',
111 predicate => 'has_ordered_by',
113 default => sub { $p->static ? [] : undef },
116 =attribute_public groupd_by is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/GroupedBy>
118 grouped_by is passed to ->search to determine aggregate results
125 writer => '_set_grouped_by',
127 predicate => 'has_grouped_by',
129 default => sub { $p->static ? [] : undef },
132 =attribute_public prefetch is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/Prefetch>
134 prefetch is passed to ->search to optimize the number of database fetches for joins
141 writer => '_set_prefetch',
143 default => sub { $p->static ? [] : undef },
147 my ($self, $new) = @_;
148 if($self->has_prefetch_allows and @{$self->prefetch_allows})
150 foreach my $pf (@$new)
152 if(HashRef->check($pf))
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];
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];
166 return if not defined($new);
167 die 'Prefetching is not allowed' if @$new;
172 =attribute_public prefetch_allows is: ro, isa: ArrayRef[ArrayRef|Str|HashRef]
174 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.
176 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.
178 prefetch_allows => [ 'cds', { cds => tracks }, { cds => producers } ] # to be explicit
179 prefetch_allows => [ 'cds', { cds => '*' } ] # wildcard means the same thing
183 has prefetch_allows =>
186 writer => '_set_prefetch_allows',
187 isa => ArrayRef[ArrayRef|Str|HashRef],
188 default => sub { [ ] },
189 predicate => 'has_prefetch_allows',
192 my ($self, $new) = @_;
195 my ($self, $rel, $static) = @_;
196 if(ArrayRef->check($rel))
198 foreach my $rel_sub (@$rel)
200 $self->_check_rel($rel_sub, $static);
203 elsif(HashRef->check($rel))
205 while(my($k,$v) = each %$rel)
207 $self->check_has_relation($k, $v, undef, $static);
209 $self->prefetch_validator->load($rel);
213 $self->check_has_relation($rel, undef, undef, $static);
214 $self->prefetch_validator->load($rel);
218 foreach my $rel (@$new)
220 $self->_check_rel($rel, $p->static);
225 =attribute_public search_exposes is: ro, isa: ArrayRef[Str|HashRef]
227 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.
229 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.
233 has 'search_exposes' =>
236 writer => '_set_search_exposes',
237 isa => ArrayRef[Str|HashRef],
238 predicate => 'has_search_exposes',
239 default => sub { [ ] },
242 my ($self, $new) = @_;
243 $self->search_validator->load($_) for @$new;
247 =attribute_public search is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/SearchParameters>
249 search contains the raw search parameters. Upon setting, a trigger will fire to format them, set search_parameters, and set search_attributes.
251 Please see L</generate_parameters_attributes> for details on how the format works.
258 writer => '_set_search',
259 isa => SearchParameters,
260 predicate => 'has_search',
264 my ($self, $new) = @_;
266 if($self->has_search_exposes and @{$self->search_exposes})
268 foreach my $foo (@$new)
270 while( my ($k, $v) = each %$foo)
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];
280 foreach my $foo (@$new)
282 while( my ($k, $v) = each %$foo)
284 $self->check_column_relation({$k => $v});
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);
296 =attribute_public search_parameters is:ro, isa: L<Catalyst::Controller::DBIC::API::Types/SearchParameters>
298 search_parameters stores the formatted search parameters that will be passed to ->search
302 has search_parameters =>
305 isa => SearchParameters,
306 writer => '_set_search_parameters',
307 predicate => 'has_search_parameters',
309 default => sub { [{}] },
312 =attribute_public search_attributes is:ro, isa: HashRef
314 search_attributes stores the formatted search attributes that will be passed to ->search
318 has search_attributes =>
322 writer => '_set_search_attributes',
323 predicate => 'has_search_attributes',
327 =attribute_public search_total_entries is: ro, isa: Int
329 search_total_entries stores the total number of entries in a paged search result
333 has search_total_entries =>
337 writer => '_set_search_total_entries',
338 predicate => 'has_search_total_entries',
341 =attribute_public select_exposes is: ro, isa: ArrayRef[Str|HashRef]
343 select_exposes limits what can actually be selected. Use this to whitelist database functions (such as COUNT).
345 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.
349 has 'select_exposes' =>
352 writer => '_set_select_exposes',
353 isa => ArrayRef[Str|HashRef],
354 predicate => 'has_select_exposes',
355 default => sub { [ ] },
358 my ($self, $new) = @_;
359 $self->select_validator->load($_) for @$new;
363 =attribute_public select is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/SelectColumns>
365 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.
367 Please see L<DBIx::Class::ResultSet/select> for more details.
374 writer => '_set_select',
375 isa => SelectColumns,
376 predicate => 'has_select',
377 default => sub { $p->static ? [] : undef },
381 my ($self, $new) = @_;
382 if($self->has_select_exposes)
384 foreach my $val (@$new)
386 die "'$val' is not allowed in a select"
387 unless $self->select_validator->validate($val);
392 $self->check_column_relation($_, $p->static) for @$new;
397 =attribute_public as is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/AsAliases>
399 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.
401 Please see L<DBIx::Class::ResultSet/as> for more details.
410 default => sub { $p->static ? [] : undef },
413 my ($self, $new) = @_;
414 if($self->has_select)
416 die "'as' argument count (${\scalar(@$new)}) must match 'select' argument count (${\scalar(@{$self->select || []})})"
417 unless @$new == @{$self->select || []};
421 die "'as' is only valid if 'select is also provided'";
426 =attribute_public joins is: ro, isa L<Catalyst::Controller::DBIC::API::Types/JoinBuilder>
428 joins holds the top level JoinBuilder object used to keep track of joins automagically while formatting complex search parameters.
430 Provides a single handle which returns the 'join' attribute for search_attributes:
432 build_joins => 'joins'
443 build_joins => 'joins',
447 =attribute_public request_data is: ro, isa: HashRef
449 request_data holds the raw (but deserialized) data for ths request
453 has 'request_data' =>
457 writer => '_set_request_data',
458 predicate => 'has_request_data',
461 my ($self, $new) = @_;
462 my $controller = $self->_controller;
463 return unless defined($new) && keys %$new;
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};
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};
471 $self->_set_offset($new->{$controller->offset_arg}) if exists $new->{$controller->offset_arg};
472 $self->_set_search($new->{$controller->search_arg}) if exists $new->{$controller->search_arg};
476 method _build_joins => sub { return Catalyst::Controller::DBIC::API::JoinBuilder->new(name => 'TOP') };
478 =method_protected format_search_parameters
480 format_search_parameters iterates through the provided params ArrayRef, calling generate_column_parameters on each one
484 method format_search_parameters => sub
486 my ($self, $params) = @_;
490 foreach my $param (@$params)
492 push(@$genparams, $self->generate_column_parameters($self->stored_result_source, $param, $self->joins));
498 =method_protected generate_column_parameters
500 generate_column_parameters recursively generates properly aliased parameters for search, building a new JoinBuilder each layer of recursion
504 method generate_column_parameters => sub
506 my ($self, $source, $param, $join, $base) = @_;
508 my $search_params = {};
511 foreach my $column (keys %$param)
513 if ($source->has_relationship($column))
515 # check if the value isn't a hashref
516 unless (ref($param->{$column}) && reftype($param->{$column}) eq 'HASH')
518 $search_params->{join('.', $base, $column)} = $param->{$column};
522 $search_params = { %$search_params, %{
523 $self->generate_column_parameters
525 $source->related_source($column),
527 Catalyst::Controller::DBIC::API::JoinBuilder->new(parent => $join, name => $column),
532 elsif ($source->has_column($column))
534 $search_params->{join('.', $base, $column)} = $param->{$column};
536 # might be a sql function instead of a column name
537 # e.g. {colname => {like => '%foo%'}}
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};
545 die "$column is neither a relationship nor a column\n";
550 return $search_params;
553 =method_protected generate_parameters_attributes
555 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.
559 method generate_parameters_attributes => sub
561 my ($self, $args) = @_;
563 return ( $self->format_search_parameters($args), $self->search_attributes );
566 =method_protected _build_search_attributes
568 This builder method generates the search attributes
572 method _build_search_attributes => sub
574 my ($self, $args) = @_;
575 my $static = $self->_controller;
576 my $search_attributes =
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,
584 page => $static->page,
585 offset => $self->offset,
586 join => $self->build_joins,
591 $search_attributes->{page} = $self->page;
593 elsif(!$self->has_page && defined($search_attributes->{offset}) && defined($search_attributes->{rows}))
595 $search_attributes->{page} = $search_attributes->{offset} / $search_attributes->{rows} + 1;
596 delete $search_attributes->{offset};
607 (ref($_->[1]) && reftype($_->[1]) eq 'HASH' && keys %{$_->[1]})
608 || (ref($_->[1]) && reftype($_->[1]) eq 'ARRAY' && @{$_->[1]})
613 map { [$_, $search_attributes->{$_}] }
614 keys %$search_attributes
618 if ($search_attributes->{page} && !$search_attributes->{rows}) {
619 die 'list_page can only be used with list_count';
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}];
629 return $search_attributes;
636 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.