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;
14 =attribute_private search_validator
16 A Catalyst::Controller::DBIC::API::Validator instance used solely to validate search parameters
20 with 'MooseX::Role::BuildInstanceOf' =>
22 'target' => 'Catalyst::Controller::DBIC::API::Validator',
23 'prefix' => 'search_validator',
26 =attribute_private select_validator
28 A Catalyst::Controller::DBIC::API::Validator instance used solely to validate select parameters
32 with 'MooseX::Role::BuildInstanceOf' =>
34 'target' => 'Catalyst::Controller::DBIC::API::Validator',
35 'prefix' => 'select_validator',
38 =attribute_private prefetch_validator
40 A Catalyst::Controller::DBIC::API::Validator instance used solely to validate prefetch parameters
44 with 'MooseX::Role::BuildInstanceOf' =>
46 'target' => 'Catalyst::Controller::DBIC::API::Validator',
47 'prefix' => 'prefetch_validator',
50 parameter static => ( isa => Bool, default => 0 );
58 requires qw/check_has_relation check_column_relation/;
62 requires qw/_controller check_has_relation check_column_relation/;
65 =attribute_public count is: ro, isa: Int
67 count is the number of rows to be returned during paging
74 writer => '_set_count',
76 predicate => 'has_count',
79 =attribute_public page is: ro, isa: Int
81 page is what page to return while paging
88 writer => '_set_page',
90 predicate => 'has_page',
93 =attribute_public ordered_by is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/OrderedBy>
95 ordered_by is passed to ->search to determine sorting
102 writer => '_set_ordered_by',
104 predicate => 'has_ordered_by',
106 default => sub { $p->static ? [] : undef },
109 =attribute_public groupd_by is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/GroupedBy>
111 grouped_by is passed to ->search to determine aggregate results
118 writer => '_set_grouped_by',
120 predicate => 'has_grouped_by',
122 default => sub { $p->static ? [] : undef },
125 =attribute_public prefetch is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/Prefetch>
127 prefetch is passed to ->search to optimize the number of database fetches for joins
134 writer => '_set_prefetch',
136 default => sub { $p->static ? [] : undef },
140 my ($self, $new) = @_;
141 if($self->has_prefetch_allows and @{$self->prefetch_allows})
143 foreach my $pf (@$new)
145 if(HashRef->check($pf))
147 die qq|'${\Dumper($pf)}' is not an allowed prefetch in: ${\join("\n", @{$self->prefetch_validator->templates})}|
148 unless $self->prefetch_validator->validate($pf)->[0];
152 die qq|'$pf' is not an allowed prefetch in: ${\join("\n", @{$self->prefetch_validator->templates})}|
153 unless $self->prefetch_validator->validate({$pf => 1})->[0];
159 return if not defined($new);
160 die 'Prefetching is not allowed' if @$new;
165 =attribute_public prefetch_allows is: ro, isa: ArrayRef[ArrayRef|Str|HashRef]
167 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.
169 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.
171 prefetch_allows => [ 'cds', { cds => tracks }, { cds => producers } ] # to be explicit
172 prefetch_allows => [ 'cds', { cds => '*' } ] # wildcard means the same thing
176 has prefetch_allows =>
179 writer => '_set_prefetch_allows',
180 isa => ArrayRef[ArrayRef|Str|HashRef],
181 default => sub { [ ] },
182 predicate => 'has_prefetch_allows',
185 my ($self, $new) = @_;
188 my ($self, $rel, $static) = @_;
189 if(ArrayRef->check($rel))
191 foreach my $rel_sub (@$rel)
193 $self->check_rel($rel_sub, $static);
196 elsif(HashRef->check($rel))
198 while(my($k,$v) = each %$rel)
200 $self->check_has_relation($k, $v, undef, $static);
202 $self->prefetch_validator->load($rel);
206 $self->check_has_relation($rel, undef, undef, $static);
207 $self->prefetch_validator->load($rel);
211 foreach my $rel (@$new)
213 $self->check_rel($rel, $p->static);
218 =attribute_public search_exposes is: ro, isa: ArrayRef[Str|HashRef]
220 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.
222 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.
226 has 'search_exposes' =>
229 writer => '_set_search_exposes',
230 isa => ArrayRef[Str|HashRef],
231 predicate => 'has_search_exposes',
232 default => sub { [ ] },
235 my ($self, $new) = @_;
236 $self->search_validator->load($_) for @$new;
240 =attribute_public search is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/SearchParameters>
242 search contains the raw search parameters. Upon setting, a trigger will fire to format them, set search_parameters, and set search_attributes.
244 Please see L</generate_parameters_attributes> for details on how the format works.
251 writer => '_set_search',
252 isa => SearchParameters,
253 predicate => 'has_search',
257 my ($self, $new) = @_;
259 if($self->has_search_exposes and @{$self->search_exposes})
261 foreach my $foo (@$new)
263 while( my ($k, $v) = each %$foo)
265 local $Data::Dumper::Terse = 1;
266 die qq|{ $k => ${\Dumper($v)} } is not an allowed search term in: ${\join("\n", @{$self->search_validator->templates})}|
267 unless $self->search_validator->validate({$k=>$v})->[0];
273 foreach my $foo (@$new)
275 while( my ($k, $v) = each %$foo)
277 $self->check_column_relation({$k => $v});
282 my ($search_parameters, $search_attributes) = $self->generate_parameters_attributes($new);
283 $self->_set_search_parameters($search_parameters);
284 $self->_set_search_attributes($search_attributes);
289 =attribute_public search_parameters is:ro, isa: L<Catalyst::Controller::DBIC::API::Types/SearchParameters>
291 search_parameters stores the formatted search parameters that will be passed to ->search
295 has search_parameters =>
298 isa => SearchParameters,
299 writer => '_set_search_parameters',
300 predicate => 'has_search_parameters',
302 default => sub { [{}] },
305 =attribute_public search_attributes is:ro, isa: HashRef
307 search_attributes stores the formatted search attributes that will be passed to ->search
311 has search_attributes =>
315 writer => '_set_search_attributes',
316 predicate => 'has_search_attributes',
320 =attribute_public search_total_entries is: ro, isa: Int
322 search_total_entries stores the total number of entries in a paged search result
326 has search_total_entries =>
330 writer => '_set_search_total_entries',
331 predicate => 'has_search_total_entries',
334 =attribute_public select_exposes is: ro, isa: ArrayRef[Str|HashRef]
336 select_exposes limits what can actually be selected. Use this to whitelist database functions (such as COUNT).
338 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.
342 has 'select_exposes' =>
345 writer => '_set_select_exposes',
346 isa => ArrayRef[Str|HashRef],
347 predicate => 'has_select_exposes',
348 default => sub { [ ] },
351 my ($self, $new) = @_;
352 $self->select_validator->load($_) for @$new;
356 =attribute_public select is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/SelectColumns>
358 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.
360 Please see L<DBIx::Class::ResultSet/select> for more details.
367 writer => '_set_select',
368 isa => SelectColumns,
369 predicate => 'has_select',
370 default => sub { $p->static ? [] : undef },
374 my ($self, $new) = @_;
375 if($self->has_select_exposes)
377 foreach my $val (@$new)
379 die "'$val' is not allowed in a select"
380 unless $self->select_validator->validate($val);
385 $self->check_column_relation($_, $p->static) for @$new;
390 =attribute_public as is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/AsAliases>
392 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.
394 Please see L<DBIx::Class::ResultSet/as> for more details.
403 default => sub { $p->static ? [] : undef },
406 my ($self, $new) = @_;
407 if($self->has_select)
409 die "'as' argument count (${\scalar(@$new)}) must match 'select' argument count (${\scalar(@{$self->select || []})})"
410 unless @$new == @{$self->select || []};
414 die "'as' is only valid if 'select is also provided'";
419 =attribute_public joins is: ro, isa L<Catalyst::Controller::DBIC::API::Types/JoinBuilder>
421 joins holds the top level JoinBuilder object used to keep track of joins automagically while formatting complex search parameters.
423 Provides a single handle which returns the 'join' attribute for search_attributes:
425 build_joins => 'joins'
436 build_joins => 'joins',
440 =attribute_public request_data is: ro, isa: HashRef
442 request_data holds the raw (but deserialized) data for ths request
446 has 'request_data' =>
450 writer => '_set_request_data',
453 my ($self, $new) = @_;
454 my $controller = $self->_controller;
455 $self->_set_prefetch($new->{$controller->prefetch_arg}) if exists $new->{$controller->prefetch_arg};
456 $self->_set_select($new->{$controller->select_arg}) if exists $new->{$controller->select_arg};
457 $self->_set_as($new->{$controller->as_arg}) if exists $new->{$controller->as_arg};
458 $self->_set_grouped_by($new->{$controller->grouped_by_arg}) if exists $new->{$controller->grouped_by_arg};
459 $self->_set_ordered_by($new->{$controller->ordered_by_arg}) if exists $new->{$controller->ordered_by_arg};
460 $self->_set_search($new->{$controller->search_arg}) if exists $new->{$controller->search_arg};
461 $self->_set_count($new->{$controller->count_arg}) if exists $new->{$controller->count_arg};
462 $self->_set_page($new->{$controller->page_arg}) if exists $new->{$controller->page_arg};
466 method _build_joins => sub { return Catalyst::Controller::DBIC::API::JoinBuilder->new(name => 'TOP') };
468 =method_protected format_search_parameters
470 format_search_parameters iterates through the provided params ArrayRef, calling generate_column_parameters on each one
474 method format_search_parameters => sub
477 my ($self, $params) = @_;
481 foreach my $param (@$params)
483 push(@$genparams, $self->generate_column_parameters($self->stored_result_source, $param, $self->joins));
489 =method_protected generate_column_parameters
491 generate_column_parameters recursively generates properly aliased parameters for search, building a new JoinBuilder each layer of recursion
495 method generate_column_parameters => sub
498 my ($self, $source, $param, $join, $base) = @_;
503 foreach my $column (keys %$param)
505 if($source->has_relationship($column))
507 unless (ref($param->{$column}) && reftype($param->{$column}) eq 'HASH')
509 $search_params->{join('.', $base, $column)} = $param->{$column};
515 $self->generate_column_parameters
517 $source->related_source($column),
519 Catalyst::Controller::DBIC::API::JoinBuilder->new(parent => $join, name => $column),
526 $search_params->{join('.', $base, $column)} = $param->{$column};
530 return $search_params;
533 =method_protected generate_parameters_attributes
535 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.
539 method generate_parameters_attributes => sub
542 my ($self, $args) = @_;
544 return ( $self->format_search_parameters($args), $self->search_attributes );
547 =method_protected _build_search_attributes
549 This builder method generates the search attributes
553 method _build_search_attributes => sub
555 my ($self, $args) = @_;
556 my $static = $self->_controller;
557 my $search_attributes =
559 group_by => $self->grouped_by || ((scalar(@{$static->grouped_by})) ? $static->grouped_by : undef),
560 order_by => $self->ordered_by || ((scalar(@{$static->ordered_by})) ? $static->ordered_by : undef),
561 select => $self->select || ((scalar(@{$static->select})) ? $static->select : undef),
562 as => $self->as || ((scalar(@{$static->as})) ? $static->as : undef),
563 prefetch => $self->prefetch || $static->prefetch || undef,
564 rows => $self->count || $static->count,
566 join => $self->build_joins,
576 (ref($_->[1]) && reftype($_->[1]) eq 'HASH' && keys %{$_->[1]})
577 || (ref($_->[1]) && reftype($_->[1]) eq 'ARRAY' && @{$_->[1]})
582 map { [$_, $search_attributes->{$_}] }
583 keys %$search_attributes
587 if ($search_attributes->{page} && !$search_attributes->{rows}) {
588 die 'list_page can only be used with list_count';
591 if ($search_attributes->{select}) {
592 # make sure all columns have an alias to avoid ambiguous issues
593 # but allow non strings (eg. hashrefs for db procs like 'count')
594 # to pass through unmolested
595 $search_attributes->{select} = [map { (Str->check($_) && $_ !~ m/\./) ? "me.$_" : $_ } (ref $search_attributes->{select}) ? @{$search_attributes->{select}} : $search_attributes->{select}];
598 return $search_attributes;
605 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.