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 offset is ro, isa: Int
95 offset specifies where to start the paged result (think SQL LIMIT)
102 writer => '_set_offset',
104 predicate => 'has_offset',
107 =attribute_public ordered_by is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/OrderedBy>
109 ordered_by is passed to ->search to determine sorting
116 writer => '_set_ordered_by',
118 predicate => 'has_ordered_by',
120 default => sub { $p->static ? [] : undef },
123 =attribute_public groupd_by is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/GroupedBy>
125 grouped_by is passed to ->search to determine aggregate results
132 writer => '_set_grouped_by',
134 predicate => 'has_grouped_by',
136 default => sub { $p->static ? [] : undef },
139 =attribute_public prefetch is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/Prefetch>
141 prefetch is passed to ->search to optimize the number of database fetches for joins
148 writer => '_set_prefetch',
150 default => sub { $p->static ? [] : undef },
154 my ($self, $new) = @_;
155 if($self->has_prefetch_allows and @{$self->prefetch_allows})
157 foreach my $pf (@$new)
159 if(HashRef->check($pf))
161 die qq|'${\Dumper($pf)}' is not an allowed prefetch in: ${\join("\n", @{$self->prefetch_validator->templates})}|
162 unless $self->prefetch_validator->validate($pf)->[0];
166 die qq|'$pf' is not an allowed prefetch in: ${\join("\n", @{$self->prefetch_validator->templates})}|
167 unless $self->prefetch_validator->validate({$pf => 1})->[0];
173 return if not defined($new);
174 die 'Prefetching is not allowed' if @$new;
179 =attribute_public prefetch_allows is: ro, isa: ArrayRef[ArrayRef|Str|HashRef]
181 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.
183 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.
185 prefetch_allows => [ 'cds', { cds => tracks }, { cds => producers } ] # to be explicit
186 prefetch_allows => [ 'cds', { cds => '*' } ] # wildcard means the same thing
190 has prefetch_allows =>
193 writer => '_set_prefetch_allows',
194 isa => ArrayRef[ArrayRef|Str|HashRef],
195 default => sub { [ ] },
196 predicate => 'has_prefetch_allows',
199 my ($self, $new) = @_;
202 my ($self, $rel, $static) = @_;
203 if(ArrayRef->check($rel))
205 foreach my $rel_sub (@$rel)
207 $self->check_rel($rel_sub, $static);
210 elsif(HashRef->check($rel))
212 while(my($k,$v) = each %$rel)
214 $self->check_has_relation($k, $v, undef, $static);
216 $self->prefetch_validator->load($rel);
220 $self->check_has_relation($rel, undef, undef, $static);
221 $self->prefetch_validator->load($rel);
225 foreach my $rel (@$new)
227 $self->check_rel($rel, $p->static);
232 =attribute_public search_exposes is: ro, isa: ArrayRef[Str|HashRef]
234 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.
236 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.
240 has 'search_exposes' =>
243 writer => '_set_search_exposes',
244 isa => ArrayRef[Str|HashRef],
245 predicate => 'has_search_exposes',
246 default => sub { [ ] },
249 my ($self, $new) = @_;
250 $self->search_validator->load($_) for @$new;
254 =attribute_public search is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/SearchParameters>
256 search contains the raw search parameters. Upon setting, a trigger will fire to format them, set search_parameters, and set search_attributes.
258 Please see L</generate_parameters_attributes> for details on how the format works.
265 writer => '_set_search',
266 isa => SearchParameters,
267 predicate => 'has_search',
271 my ($self, $new) = @_;
273 if($self->has_search_exposes and @{$self->search_exposes})
275 foreach my $foo (@$new)
277 while( my ($k, $v) = each %$foo)
279 local $Data::Dumper::Terse = 1;
280 die qq|{ $k => ${\Dumper($v)} } is not an allowed search term in: ${\join("\n", @{$self->search_validator->templates})}|
281 unless $self->search_validator->validate({$k=>$v})->[0];
287 foreach my $foo (@$new)
289 while( my ($k, $v) = each %$foo)
291 $self->check_column_relation({$k => $v});
296 my ($search_parameters, $search_attributes) = $self->generate_parameters_attributes($new);
297 $self->_set_search_parameters($search_parameters);
298 $self->_set_search_attributes($search_attributes);
303 =attribute_public search_parameters is:ro, isa: L<Catalyst::Controller::DBIC::API::Types/SearchParameters>
305 search_parameters stores the formatted search parameters that will be passed to ->search
309 has search_parameters =>
312 isa => SearchParameters,
313 writer => '_set_search_parameters',
314 predicate => 'has_search_parameters',
316 default => sub { [{}] },
319 =attribute_public search_attributes is:ro, isa: HashRef
321 search_attributes stores the formatted search attributes that will be passed to ->search
325 has search_attributes =>
329 writer => '_set_search_attributes',
330 predicate => 'has_search_attributes',
334 =attribute_public search_total_entries is: ro, isa: Int
336 search_total_entries stores the total number of entries in a paged search result
340 has search_total_entries =>
344 writer => '_set_search_total_entries',
345 predicate => 'has_search_total_entries',
348 =attribute_public select_exposes is: ro, isa: ArrayRef[Str|HashRef]
350 select_exposes limits what can actually be selected. Use this to whitelist database functions (such as COUNT).
352 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.
356 has 'select_exposes' =>
359 writer => '_set_select_exposes',
360 isa => ArrayRef[Str|HashRef],
361 predicate => 'has_select_exposes',
362 default => sub { [ ] },
365 my ($self, $new) = @_;
366 $self->select_validator->load($_) for @$new;
370 =attribute_public select is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/SelectColumns>
372 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.
374 Please see L<DBIx::Class::ResultSet/select> for more details.
381 writer => '_set_select',
382 isa => SelectColumns,
383 predicate => 'has_select',
384 default => sub { $p->static ? [] : undef },
388 my ($self, $new) = @_;
389 if($self->has_select_exposes)
391 foreach my $val (@$new)
393 die "'$val' is not allowed in a select"
394 unless $self->select_validator->validate($val);
399 $self->check_column_relation($_, $p->static) for @$new;
404 =attribute_public as is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/AsAliases>
406 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.
408 Please see L<DBIx::Class::ResultSet/as> for more details.
417 default => sub { $p->static ? [] : undef },
420 my ($self, $new) = @_;
421 if($self->has_select)
423 die "'as' argument count (${\scalar(@$new)}) must match 'select' argument count (${\scalar(@{$self->select || []})})"
424 unless @$new == @{$self->select || []};
428 die "'as' is only valid if 'select is also provided'";
433 =attribute_public joins is: ro, isa L<Catalyst::Controller::DBIC::API::Types/JoinBuilder>
435 joins holds the top level JoinBuilder object used to keep track of joins automagically while formatting complex search parameters.
437 Provides a single handle which returns the 'join' attribute for search_attributes:
439 build_joins => 'joins'
450 build_joins => 'joins',
454 =attribute_public request_data is: ro, isa: HashRef
456 request_data holds the raw (but deserialized) data for ths request
460 has 'request_data' =>
464 writer => '_set_request_data',
467 my ($self, $new) = @_;
468 my $controller = $self->_controller;
469 $self->_set_prefetch($new->{$controller->prefetch_arg}) if exists $new->{$controller->prefetch_arg};
470 $self->_set_select($new->{$controller->select_arg}) if exists $new->{$controller->select_arg};
471 $self->_set_as($new->{$controller->as_arg}) if exists $new->{$controller->as_arg};
472 $self->_set_grouped_by($new->{$controller->grouped_by_arg}) if exists $new->{$controller->grouped_by_arg};
473 $self->_set_ordered_by($new->{$controller->ordered_by_arg}) if exists $new->{$controller->ordered_by_arg};
474 $self->_set_count($new->{$controller->count_arg}) if exists $new->{$controller->count_arg};
475 $self->_set_page($new->{$controller->page_arg}) if exists $new->{$controller->page_arg};
476 $self->_set_offset($new->{$controller->offset_arg}) if exists $new->{$controller->offset_arg};
477 $self->_set_search($new->{$controller->search_arg}) if exists $new->{$controller->search_arg};
481 method _build_joins => sub { return Catalyst::Controller::DBIC::API::JoinBuilder->new(name => 'TOP') };
483 =method_protected format_search_parameters
485 format_search_parameters iterates through the provided params ArrayRef, calling generate_column_parameters on each one
489 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
513 my ($self, $source, $param, $join, $base) = @_;
518 foreach my $column (keys %$param)
520 if($source->has_relationship($column))
522 unless (ref($param->{$column}) && reftype($param->{$column}) eq 'HASH')
524 $search_params->{join('.', $base, $column)} = $param->{$column};
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
557 my ($self, $args) = @_;
559 return ( $self->format_search_parameters($args), $self->search_attributes );
562 =method_protected _build_search_attributes
564 This builder method generates the search attributes
568 method _build_search_attributes => sub
570 my ($self, $args) = @_;
571 my $static = $self->_controller;
572 my $search_attributes =
574 group_by => $self->grouped_by || ((scalar(@{$static->grouped_by})) ? $static->grouped_by : undef),
575 order_by => $self->ordered_by || ((scalar(@{$static->ordered_by})) ? $static->ordered_by : undef),
576 select => $self->select || ((scalar(@{$static->select})) ? $static->select : undef),
577 as => $self->as || ((scalar(@{$static->as})) ? $static->as : undef),
578 prefetch => $self->prefetch || $static->prefetch || undef,
579 rows => $self->count || $static->count,
580 offset => $self->offset,
581 join => $self->build_joins,
586 $search_attributes->{page} = $self->page;
588 elsif(!$self->has_page && defined($search_attributes->{offset}) && defined($search_attributes->{rows}))
590 $search_attributes->{page} = $search_attributes->{offset} / $search_attributes->{rows} + 1;
591 delete $search_attributes->{offset};
602 (ref($_->[1]) && reftype($_->[1]) eq 'HASH' && keys %{$_->[1]})
603 || (ref($_->[1]) && reftype($_->[1]) eq 'ARRAY' && @{$_->[1]})
608 map { [$_, $search_attributes->{$_}] }
609 keys %$search_attributes
613 if ($search_attributes->{page} && !$search_attributes->{rows}) {
614 die 'list_page can only be used with list_count';
617 if ($search_attributes->{select}) {
618 # make sure all columns have an alias to avoid ambiguous issues
619 # but allow non strings (eg. hashrefs for db procs like 'count')
620 # to pass through unmolested
621 $search_attributes->{select} = [map { (Str->check($_) && $_ !~ m/\./) ? "me.$_" : $_ } (ref $search_attributes->{select}) ? @{$search_attributes->{select}} : $search_attributes->{select}];
624 return $search_attributes;
631 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.