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