fixed failure because of Data::DPath >=0.41 by not using MooseX::Role::BuildInstanceO...
[catagits/Catalyst-Controller-DBIC-API.git] / lib / Catalyst / Controller / DBIC / API / RequestArguments.pm
1 package Catalyst::Controller::DBIC::API::RequestArguments;
2
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');
8 use Data::Dumper;
9 use Catalyst::Controller::DBIC::API::Validator;
10 use namespace::autoclean;
11
12 use Catalyst::Controller::DBIC::API::JoinBuilder;
13
14 =attribute_private search_validator
15
16 A Catalyst::Controller::DBIC::API::Validator instance used solely to validate search parameters
17
18 =cut
19
20 =attribute_private select_validator
21
22 A Catalyst::Controller::DBIC::API::Validator instance used solely to validate select parameters
23
24 =cut
25
26 =attribute_private prefetch_validator
27
28 A Catalyst::Controller::DBIC::API::Validator instance used solely to validate prefetch parameters
29
30 =cut
31
32 has [qw( search_validator select_validator prefetch_validator )] => (
33     is => 'ro',
34     isa => 'Catalyst::Controller::DBIC::API::Validator',
35     lazy => 1,
36     builder => '_build_validator',
37 );
38
39 sub _build_validator {
40     return Catalyst::Controller::DBIC::API::Validator->new;
41 }
42
43 parameter static => ( isa => Bool, default => 0 );
44
45 role {
46
47     my $p = shift;
48
49     if($p->static)
50     {
51         requires qw/check_has_relation check_column_relation/;
52     }
53     else
54     {
55         requires qw/_controller check_has_relation check_column_relation/;
56     }
57
58 =attribute_public count is: ro, isa: Int
59
60 count is the number of rows to be returned during paging
61
62 =cut
63
64     has 'count' =>
65     (
66         is => 'ro',
67         writer => '_set_count',
68         isa => Int,
69         predicate => 'has_count',
70     );
71
72 =attribute_public page is: ro, isa: Int
73
74 page is what page to return while paging
75
76 =cut
77
78     has 'page' =>
79     (
80         is => 'ro',
81         writer => '_set_page',
82         isa => Int,
83         predicate => 'has_page',
84     );
85
86 =attribute_public offset is ro, isa: Int
87
88 offset specifies where to start the paged result (think SQL LIMIT)
89
90 =cut
91
92     has 'offset' =>
93     (
94         is => 'ro',
95         writer => '_set_offset',
96         isa => Int,
97         predicate => 'has_offset',
98     );
99
100 =attribute_public ordered_by is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/OrderedBy>
101
102 ordered_by is passed to ->search to determine sorting
103
104 =cut
105
106     has 'ordered_by' =>
107     (
108         is => 'ro',
109         writer => '_set_ordered_by',
110         isa => OrderedBy,
111         predicate => 'has_ordered_by',
112         coerce => 1,
113         default => sub { $p->static ? [] : undef },
114     );
115
116 =attribute_public groupd_by is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/GroupedBy>
117
118 grouped_by is passed to ->search to determine aggregate results
119
120 =cut
121
122     has 'grouped_by' =>
123     (
124         is => 'ro',
125         writer => '_set_grouped_by',
126         isa => GroupedBy,
127         predicate => 'has_grouped_by',
128         coerce => 1,
129         default => sub { $p->static ? [] : undef },
130     );
131
132 =attribute_public prefetch is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/Prefetch>
133
134 prefetch is passed to ->search to optimize the number of database fetches for joins
135
136 =cut
137
138     has prefetch =>
139     (
140         is => 'ro',
141         writer => '_set_prefetch',
142         isa => Prefetch,
143         default => sub { $p->static ? [] : undef },
144         coerce => 1,
145         trigger => sub
146         {
147             my ($self, $new) = @_;
148             if($self->has_prefetch_allows and @{$self->prefetch_allows})
149             {
150                 foreach my $pf (@$new)
151                 {
152                     if(HashRef->check($pf))
153                     {
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];
156                     }
157                     else
158                     {
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];
161                     }
162                 }
163             }
164             else
165             {
166                 return if not defined($new);
167                 die 'Prefetching is not allowed' if @$new;
168             }
169         },
170     );
171
172 =attribute_public prefetch_allows is: ro, isa: ArrayRef[ArrayRef|Str|HashRef]
173
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.
175
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.
177
178     prefetch_allows => [ 'cds', { cds => tracks }, { cds => producers } ] # to be explicit
179     prefetch_allows => [ 'cds', { cds => '*' } ] # wildcard means the same thing
180
181 =cut
182
183     has prefetch_allows =>
184     (
185         is => 'ro',
186         writer => '_set_prefetch_allows',
187         isa => ArrayRef[ArrayRef|Str|HashRef],
188         default => sub { [ ] },
189         predicate => 'has_prefetch_allows',
190         trigger => sub
191         {
192             my ($self, $new) = @_;
193
194             sub _check_rel {
195                 my ($self, $rel, $static) = @_;
196                 if(ArrayRef->check($rel))
197                 {
198                     foreach my $rel_sub (@$rel)
199                     {
200                         $self->_check_rel($rel_sub, $static);
201                     }
202                 }
203                 elsif(HashRef->check($rel))
204                 {
205                     while(my($k,$v) = each %$rel)
206                     {
207                         $self->check_has_relation($k, $v, undef, $static);
208                     }
209                     $self->prefetch_validator->load($rel);
210                 }
211                 else
212                 {
213                     $self->check_has_relation($rel, undef, undef, $static);
214                     $self->prefetch_validator->load($rel);
215                 }
216             }
217
218             foreach my $rel (@$new)
219             {
220                 $self->_check_rel($rel, $p->static);
221             }
222         },
223     );
224
225 =attribute_public search_exposes is: ro, isa: ArrayRef[Str|HashRef]
226
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.
228
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.
230
231 =cut
232
233     has 'search_exposes' =>
234     (
235         is => 'ro',
236         writer => '_set_search_exposes',
237         isa => ArrayRef[Str|HashRef],
238         predicate => 'has_search_exposes',
239         default => sub { [ ] },
240         trigger => sub
241         {
242             my ($self, $new) = @_;
243             $self->search_validator->load($_) for @$new;
244         },
245     );
246
247 =attribute_public search is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/SearchParameters>
248
249 search contains the raw search parameters. Upon setting, a trigger will fire to format them, set search_parameters, and set search_attributes.
250
251 Please see L</generate_parameters_attributes> for details on how the format works.
252
253 =cut
254
255     has 'search' =>
256     (
257         is => 'ro',
258         writer => '_set_search',
259         isa => SearchParameters,
260         predicate => 'has_search',
261         coerce => 1,
262         trigger => sub
263         {
264             my ($self, $new) = @_;
265
266             if($self->has_search_exposes and @{$self->search_exposes})
267             {
268                 foreach my $foo (@$new)
269                 {
270                     while( my ($k, $v) = each %$foo)
271                     {
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];
275                     }
276                 }
277             }
278             else
279             {
280                 foreach my $foo (@$new)
281                 {
282                     while( my ($k, $v) = each %$foo)
283                     {
284                         $self->check_column_relation({$k => $v});
285                     }
286                 }
287             }
288
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);
292
293         },
294     );
295
296 =attribute_public search_parameters is:ro, isa: L<Catalyst::Controller::DBIC::API::Types/SearchParameters>
297
298 search_parameters stores the formatted search parameters that will be passed to ->search
299
300 =cut
301
302     has search_parameters =>
303     (
304         is => 'ro',
305         isa => SearchParameters,
306         writer => '_set_search_parameters',
307         predicate => 'has_search_parameters',
308         coerce => 1,
309         default => sub { [{}] },
310     );
311
312 =attribute_public search_attributes is:ro, isa: HashRef
313
314 search_attributes stores the formatted search attributes that will be passed to ->search
315
316 =cut
317
318     has search_attributes =>
319     (
320         is => 'ro',
321         isa => HashRef,
322         writer => '_set_search_attributes',
323         predicate => 'has_search_attributes',
324         lazy_build => 1,
325     );
326
327 =attribute_public search_total_entries is: ro, isa: Int
328
329 search_total_entries stores the total number of entries in a paged search result
330
331 =cut
332
333     has search_total_entries =>
334     (
335         is => 'ro',
336         isa => Int,
337         writer => '_set_search_total_entries',
338         predicate => 'has_search_total_entries',
339     );
340
341 =attribute_public select_exposes is: ro, isa: ArrayRef[Str|HashRef]
342
343 select_exposes limits what can actually be selected. Use this to whitelist database functions (such as COUNT).
344
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.
346
347 =cut
348
349     has 'select_exposes' =>
350     (
351         is => 'ro',
352         writer => '_set_select_exposes',
353         isa => ArrayRef[Str|HashRef],
354         predicate => 'has_select_exposes',
355         default => sub { [ ] },
356         trigger => sub
357         {
358             my ($self, $new) = @_;
359             $self->select_validator->load($_) for @$new;
360         },
361     );
362
363 =attribute_public select is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/SelectColumns>
364
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.
366
367 Please see L<DBIx::Class::ResultSet/select> for more details.
368
369 =cut
370
371     has select =>
372     (
373         is => 'ro',
374         writer => '_set_select',
375         isa => SelectColumns,
376         predicate => 'has_select',
377         default => sub { $p->static ? [] : undef },
378         coerce => 1,
379         trigger => sub
380         {
381             my ($self, $new) = @_;
382             if($self->has_select_exposes)
383             {
384                 foreach my $val (@$new)
385                 {
386                     die "'$val' is not allowed in a select"
387                         unless $self->select_validator->validate($val);
388                 }
389             }
390             else
391             {
392                 $self->check_column_relation($_, $p->static) for @$new;
393             }
394         },
395     );
396
397 =attribute_public as is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/AsAliases>
398
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.
400
401 Please see L<DBIx::Class::ResultSet/as> for more details.
402
403 =cut
404
405     has as =>
406     (
407         is => 'ro',
408         writer => '_set_as',
409         isa => AsAliases,
410         default => sub { $p->static ? [] : undef },
411         trigger => sub
412         {
413             my ($self, $new) = @_;
414             if($self->has_select)
415             {
416                 die "'as' argument count (${\scalar(@$new)}) must match 'select' argument count (${\scalar(@{$self->select || []})})"
417                     unless @$new == @{$self->select || []};
418             }
419             elsif(defined $new)
420             {
421                 die "'as' is only valid if 'select is also provided'";
422             }
423         }
424     );
425
426 =attribute_public joins is: ro, isa L<Catalyst::Controller::DBIC::API::Types/JoinBuilder>
427
428 joins holds the top level JoinBuilder object used to keep track of joins automagically while formatting complex search parameters.
429
430 Provides a single handle which returns the 'join' attribute for search_attributes:
431
432     build_joins => 'joins'
433
434 =cut
435
436     has joins =>
437     (
438         is => 'ro',
439         isa => JoinBuilder,
440         lazy_build => 1,
441         handles =>
442         {
443             build_joins => 'joins',
444         }
445     );
446
447 =attribute_public request_data is: ro, isa: HashRef
448
449 request_data holds the raw (but deserialized) data for ths request
450
451 =cut
452
453     has 'request_data' =>
454     (
455         is => 'ro',
456         isa => HashRef,
457         writer => '_set_request_data',
458         predicate => 'has_request_data',
459         trigger => sub
460         {
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};
473         }
474     );
475
476     method _build_joins => sub { return Catalyst::Controller::DBIC::API::JoinBuilder->new(name => 'TOP') };
477
478 =method_protected format_search_parameters
479
480 format_search_parameters iterates through the provided params ArrayRef, calling generate_column_parameters on each one
481
482 =cut
483
484     method format_search_parameters => sub
485     {
486         my ($self, $params) = @_;
487
488         my $genparams = [];
489
490         foreach my $param (@$params)
491         {
492             push(@$genparams, $self->generate_column_parameters($self->stored_result_source, $param, $self->joins));
493         }
494
495         return $genparams;
496     };
497
498 =method_protected generate_column_parameters
499
500 generate_column_parameters recursively generates properly aliased parameters for search, building a new JoinBuilder each layer of recursion
501
502 =cut
503
504     method generate_column_parameters => sub
505     {
506         my ($self, $source, $param, $join, $base) = @_;
507         $base ||= 'me';
508         my $search_params = {};
509
510         # build up condition
511         foreach my $column (keys %$param)
512         {
513             if ($source->has_relationship($column))
514             {
515                 # check if the value isn't a hashref
516                 unless (ref($param->{$column}) && reftype($param->{$column}) eq 'HASH')
517                 {
518                     $search_params->{join('.', $base, $column)} = $param->{$column};
519                     next;
520                 }
521
522                 $search_params = { %$search_params, %{
523                     $self->generate_column_parameters
524                     (
525                         $source->related_source($column),
526                         $param->{$column},
527                         Catalyst::Controller::DBIC::API::JoinBuilder->new(parent => $join, name => $column),
528                         $column
529                     )
530                 }};
531             }
532             elsif ($source->has_column($column))
533             {
534                 $search_params->{join('.', $base, $column)} = $param->{$column};
535             }
536             # might be a sql function instead of a column name
537             # e.g. {colname => {like => '%foo%'}}
538             else
539             {
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};
543                 }
544                 else {
545                     die "$column is neither a relationship nor a column\n";
546                 }
547             }
548         }
549
550         return $search_params;
551     };
552
553 =method_protected generate_parameters_attributes
554
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.
556
557 =cut
558
559     method generate_parameters_attributes => sub
560     {
561         my ($self, $args) = @_;
562
563         return ( $self->format_search_parameters($args), $self->search_attributes );
564     };
565
566 =method_protected _build_search_attributes
567
568 This builder method generates the search attributes
569
570 =cut
571
572     method _build_search_attributes => sub
573     {
574         my ($self, $args) = @_;
575         my $static = $self->_controller;
576         my $search_attributes =
577         {
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,
587         };
588
589         if($self->has_page)
590         {
591             $search_attributes->{page} = $self->page;
592         }
593         elsif(!$self->has_page && defined($search_attributes->{offset}) && defined($search_attributes->{rows}))
594         {
595             $search_attributes->{page} = $search_attributes->{offset} / $search_attributes->{rows} + 1;
596             delete $search_attributes->{offset};
597         }
598
599
600         $search_attributes =
601         {
602             map { @$_ }
603             grep
604             {
605                 defined($_->[1])
606                 ?
607                     (ref($_->[1]) && reftype($_->[1]) eq 'HASH' && keys %{$_->[1]})
608                     || (ref($_->[1]) && reftype($_->[1]) eq 'ARRAY' && @{$_->[1]})
609                     || length($_->[1])
610                 :
611                     undef
612             }
613             map { [$_, $search_attributes->{$_}] }
614             keys %$search_attributes
615         };
616
617
618         if ($search_attributes->{page} && !$search_attributes->{rows}) {
619             die 'list_page can only be used with list_count';
620         }
621
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}];
627         }
628
629         return $search_attributes;
630
631     };
632
633 };
634 =head1 DESCRIPTION
635
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.
637
638 =cut
639
640
641 1;