]> git.decadent.org.uk Git - maypole.git/blob - lib/Maypole/Model/CDBI/FromCGI.pm
c5ad4582ea3992dd89fa38560d7e9433fd7dcc15
[maypole.git] / lib / Maypole / Model / CDBI / FromCGI.pm
1 package Maypole::Model::CDBI::FromCGI;
2 use strict;
3
4 =head1 NAME
5
6 Maypole::Model:CDBI::FromCGI - Validate form input and populate Model objects
7
8 =head1 SYNOPSIS
9
10   $obj = $class->create_from_cgi($r);
11   $obj = $class->create_from_cgi($r, { params => {data1=>...}, required => [..],
12                  ignore => [...], all => [...]);
13   $obj = $class->create_from_cgi($h, $options); # CDBI::FromCGI style, see docs
14
15   $obj->update_from_cgi($r);
16   $obj->update_from_cgi($h, $options);
17
18   $obj = $obj->add_to_from_cgi($r);
19   $obj = $obj->add_to_from_cgi($r, { params => {...} } );
20   
21   # This does not work like in CDBI::FromCGI and probably never will :
22   # $class->update_from_cgi($h, @columns);
23
24
25 =head1 DESCRIPTION
26
27 Provides a way to validate form input and populate Model Objects, based
28 on Class::DBI::FromCGI.
29
30 =cut
31
32 use warnings;
33
34 # The base base model class for apps 
35 # provides good search and create functions
36
37 use base qw(Exporter); 
38 use CGI::Untaint;
39 use Maypole::Constants;
40 use CGI::Untaint::Maypole;
41 our $Untainter = 'CGI::Untaint::Maypole';
42
43 our @EXPORT = qw/update_from_cgi create_from_cgi untaint_columns add_to_from_cgi
44     cgi_update_errors untaint_type validate_inputs validate_all _do_update_all 
45     _do_create_all _create_related classify_form_inputs/;
46
47
48
49 use Data::Dumper; # for debugging
50
51 =head1 METHODS
52
53 =head2 untaint_columns
54
55 Replicates Class::DBI::FromCGI method of same name :
56
57   __PACKAGE__->untaint_columns(
58     printable => [qw/Title Director/],
59     integer   => [qw/DomesticGross NumExplodingSheep],
60     date      => [qw/OpeningDate/],
61   );
62
63 =cut
64
65 sub untaint_columns {
66     die "untaint_columns() needs a hash" unless @_ % 2;
67     my ($class, %args) = @_;
68     $class->mk_classdata('__untaint_types')
69         unless $class->can('__untaint_types');
70     my %types = %{ $class->__untaint_types || {} };
71     while (my ($type, $ref) = each(%args)) {
72         $types{$type} = $ref;
73     }
74     $class->__untaint_types(\%types);
75 }
76
77 =head2 untaint_type
78
79   gets the  untaint type for a column as set in "untaint_types"
80
81 =cut
82
83 # get/set untaint_type for a column
84 sub untaint_type {
85     my ($class, $field, $new_type) = @_;
86     my %handler = __PACKAGE__->_untaint_handlers($class);
87     return $handler{$field} if $handler{$field};
88     my $handler = eval {
89         local $SIG{__WARN__} = sub { };
90         my $type = $class->column_type($field) or die;
91         _column_type_for($type);
92     };
93     return $handler || undef;
94 }
95
96 =head2 cgi_update_errors
97
98 Returns errors that ocurred during an operation.
99
100 =cut
101
102 sub cgi_update_errors { %{ shift->{_cgi_update_error} || {} } }
103
104
105
106 =head2 create_from_cgi
107
108 Based on the same method in Class::DBI::FromCGI.
109
110 Creates  multiple objects  from a  cgi form. 
111 Errors are returned in cgi_update_errors
112
113 It can be called Maypole style passing the Maypole request object as the
114 first arg, or Class::DBI::FromCGI style passing the Untaint Handler ($h)
115 as the first arg. 
116
117 A hashref of options can be passed as the second argument. Unlike 
118 in the CDBI equivalent, you can *not* pass a list as the second argument.
119 Options can be :
120  params -- hashref of cgi data to use instead of $r->params,
121  required -- list of fields that are required
122  ignore   -- list of fields to ignore
123  all      -- list of all fields (defaults to $class->columns)
124
125 =cut
126
127 sub create_from_cgi {
128   my ($self, $r, $opts) = @_;
129   $self->_croak( "create_from_cgi can only be called as a class method")
130     if ref $self;
131   my ($errors, $validated);
132   
133   
134   if ($r->isa('CGI::Untaint')) { # FromCGI interface compatibility
135     ($validated, $errors) = $self->validate_inputs($r,$opts); 
136   } else {
137     my $params = $opts->{params} || $r->params;
138     $opts->{params} = $self->classify_form_inputs($params);
139     ($validated, $errors) = $self->validate_all($r, $opts);
140   }
141
142   if (keys %$errors) {
143     return bless { _cgi_update_error => $errors }, $self;
144   }
145
146   # Insert all the data
147   my ($obj, $err ) = $self->_do_create_all($validated); 
148   if ($err) {
149     return bless { _cgi_update_error => $err }, $self;
150   }
151   return $obj;
152 }
153
154
155 =head2 update_from_cgi
156
157 Replicates the Class::DBI::FromCGI method of same name. It updates an object and
158 returns 1 upon success. It can take the same arguments as create_form_cgi. 
159 If errors, it sets the cgi_update_errors.
160
161 =cut
162
163 sub update_from_cgi {
164   my ($self, $r, $opts) = @_;
165   $self->_croak( "update_from_cgi can only be called as an object method") unless ref $self;
166   my ($errors, $validated);
167   $self->{_cgi_update_error} = {};
168   $opts->{updating} = 1;
169
170   # FromCGI interface compatibility 
171   if ($r->isa('CGI::Untaint')) {
172     # REHASH the $opts for updating:
173     # 1: we ignore any fields we dont have parmeter for. (safe ?)
174     # 2: we dont want to update fields unless they change
175
176     my @ignore = @{$opts->{ignore} || []};
177     push @ignore, $self->primary_column->name;
178     my $raw = $r->raw_data;
179     #print "*** raw data ****" . Dumper($raw);
180     foreach my $field ($self->columns) {
181       #print "*** field is $field ***\n";
182         if (not defined $raw->{$field}) {
183                         push @ignore, $field->name; 
184                         #print "*** ignoring $field because it is not present ***\n";
185                         next;
186         }
187         # stupid inflation , cant get at raw db value easy, must call
188         # deflate ***FIXME****
189         my $cur_val = ref $self->$field ? $self->$field->id : $self->$field;
190         if ($raw->{$field} eq $cur_val) {
191                         #print "*** ignoring $field because unchanged ***\n";
192                         push @ignore, "$field"; 
193         }
194     }
195     $opts->{ignore} = \@ignore;
196     ($validated, $errors) = $self->validate_inputs($r,$opts); 
197   } else {
198     my $params = $opts->{params} || $r->params;
199     $opts->{params} = $self->classify_form_inputs($params);
200     ($validated, $errors) = $self->validate_all($r, $opts);
201     #print "*** errors for validate all   ****" . Dumper($errors);
202   }
203
204   if (keys %$errors) {
205     #print "*** we have errors   ****" . Dumper($errors);
206     $self->{_cgi_update_error} = $errors;
207     return;
208   }
209
210   # Update all the data
211   my ($obj, $err ) = $self->_do_update_all($validated); 
212   if ($err) {
213     $self->{_cgi_update_error} = $err;
214     return; 
215   }
216   return 1;
217 }
218
219 =head2 add_to_from_cgi
220
221 $obj->add_to_from_cgi($r[, $opts]); 
222
223 Like add_to_* for has_many relationships but will add nay objects it can 
224 figure out from the data.  It returns a list of objects it creates or nothing
225 on error. Call cgi_update_errors with the calling object to get errors.
226 Fatal errors are in the respective "FATAL" key.
227
228 =cut
229
230 sub add_to_from_cgi {
231   my ($self, $r, $opts) = @_;
232   $self->_croak( "add_to_from_cgi can only be called as an object method")
233     unless ref $self;
234   my ($errors, $validated, @created);
235    
236   
237   my $params = $opts->{params} || $r->params;
238   $opts->{params} = $self->classify_form_inputs($params);
239   ($validated, $errors) = $self->validate_all($r, $opts);
240
241   
242   if (keys %$errors) {
243     $self->{_cgi_update_error} = $errors;
244         return;
245   }
246
247   # Insert all the data
248   foreach my $hm (keys %$validated) { 
249         my ($obj, $errs) = $self->_create_related($hm, $validated->{$hm}); 
250         if (not $errs) {
251                 push @created, $obj;
252         }else {
253                 $errors->{$hm} = $errs;
254         }
255   }
256   
257   if (keys %$errors) {
258     $self->{_cgi_update_error} = $errors;
259         return;
260   }
261
262   return @created;
263 }
264
265  
266
267
268 =head2 validate_all
269
270 Validates (untaints) a hash of possibly mixed table data. 
271 Returns validated and errors ($validated, $errors).
272 If no errors then undef in that spot.
273
274 =cut
275
276 sub validate_all {
277   my ($self, $r, $opts) = @_;
278   my $class = ref $self || $self;
279   my $classified = $opts->{params};
280   my $updating   = $opts->{updating};
281
282   # Base case - validate this classes data
283   $opts->{all}   ||= eval{ $r->config->{$self->table}{all_cols} }               ||
284     [$self->columns('All')];
285   $opts->{required} ||= eval{ $r->config->{$self->table}{required_cols} } ||    
286         [];
287   my $ignore = $opts->{ignore} || eval{ $r->config->{$self->table}{ignore_cols} }       
288     || [];
289   push @$ignore, $self->primary_column->name if $updating;
290   
291   # Ignore hashes of foreign inputs. This takes care of required has_a's 
292   # for main object that we have foreign inputs for. 
293   foreach (keys %$classified) {
294     push @$ignore, $_ if  ref $classified->{$_} eq 'HASH'; 
295   }
296   $opts->{ignore} = $ignore;
297   my $h = $Untainter->new($classified);
298   my ($validated, $errs) = $self->validate_inputs($h, $opts);
299
300   # Validate all foreign input
301         
302   #warn "Classified data is " . Dumper($classified); 
303   foreach my $field (keys %$classified) {
304     if (ref $classified->{$field} eq "HASH") {
305       my $data = $classified->{$field};
306           my $ignore = [];
307       my @usr_entered_vals = ();
308       foreach ( values %$data ) {
309                 push @usr_entered_vals, $_  if $_  ne '';
310       }
311
312       # filled in values
313       # IF we have some inputs for the related
314       if ( @usr_entered_vals ) {
315                 # We need to ignore us if we are a required has_a in this foreign class
316                 my $rel_meta = $self->related_meta($r, $field);
317             my $fclass   = $rel_meta->{foreign_class};
318                 my $fmeta    = $fclass->meta_info('has_a');
319                 for (keys %$fmeta) {
320                         if ($fmeta->{$_}{foreign_class} eq $class) {
321                                 push @$ignore, $_;
322                         }
323                 }
324                 my ($valid, $ferrs) = $fclass->validate_all($r,
325                 {params => $data, updating => $updating, ignore => $ignore } );         
326
327                 $errs->{$field} = $ferrs if $ferrs;
328                 $validated->{$field} = $valid;
329
330       } else { 
331                 # Check this foreign object is not requeired
332                 my %req = map { $_ => 1 } $opts->{required};
333                 if ($req{$field}) {
334                         $errs->{$field}{FATAL} = "This is required. Please enter the required fields in this section." 
335                         }
336                 }
337         }
338   }
339   #warn "Validated inputs are " . Dumper($validated);
340   undef $errs unless keys %$errs;
341   return ($validated, $errs);   
342 }
343
344
345
346 =head2 validate_inputs
347
348 $self->validate_inputs($h, $opts);
349
350 This is the main validation method to validate inputs for a single class.
351 Most of the time you use validate_all.
352
353 Returns validated and errors.
354
355 If no errors then undef in that slot.
356
357 Note: This method is currently experimental (in 2.11) and may be subject to change
358 without notice.
359
360 =cut
361
362 sub validate_inputs {
363   my ($self, $h, $opts) = @_;
364   my $updating = $opts->{updating};
365   my %required = map { $_ => 1 } @{$opts->{required}};
366   my %seen;
367   $seen{$_}++ foreach @{$opts->{ignore}};
368   my $errors    = {}; 
369   my $fields    = {};
370   $opts->{all} = [ $self->columns ] unless @{$opts->{all} || [] } ;
371   foreach my $field (@{$opts->{required}}, @{$opts->{all}}) {
372     next if $seen{$field}++;
373     my $type = $self->untaint_type($field) or 
374       do { warn "No untaint type for $self 's field $field. Ignoring.";
375            next;
376          };
377     my $value = $h->extract("-as_$type" => $field);
378     my $err = $h->error;
379
380     # Required field error 
381     if ($required{$field} and !ref($value) and $err =~ /^No input for/) {
382                                 #($value eq '' or !defined $value)) 
383       $errors->{$field} = "You must supply '$field'" 
384         #unless ($updating and$self->field;
385     } elsif ($err) {
386
387       # 1: No inupt entered
388       if ($err =~ /^No input for/) {
389                                 # A : Updating -- set the field to undef or '' 
390         if ($updating) { 
391           $fields->{$field} = eval{$self->column_nullable($field)} ? 
392             undef : ''; 
393         }
394                                 # B : Creating -- dont set a value and RDMS will put default
395       }
396
397       # 2: A real untaint error -- just set the error 
398       elsif ($err !~ /^No parameter for/) {
399         $errors->{$field} =  $err;
400       }
401     } else {
402       $fields->{$field} = $value
403     }
404   }
405   undef $errors unless keys %$errors;
406   return ($fields, $errors);
407 }
408
409
410 ##################
411 # _do_create_all #
412 ##################
413
414 # Untaints and Creates objects from hashed params.
415 # Returns parent object and errors ($obj, $errors).  
416 # If no errors, then undef in that slot.
417 sub _do_create_all {
418   my ($self, $validated) = @_;
419   my $class = ref $self  || $self;
420   my ($errors, $accssr); 
421
422   # Separate out related objects' data from main hash 
423   my %related;
424   foreach (keys %$validated) {
425     $related{$_}= delete $validated->{$_} if ref $validated->{$_} eq 'HASH';
426   }
427   # Make has_own/a rel type objects and put id in parent's data hash 
428 #  foreach $accssr (keys %related) {
429 #    my $rel_meta = $self->related_meta('r', $accssr); 
430 #    $self->_croak("No relationship found for $accssr to $class.")
431 #      unless $rel_meta;
432 #    my $rel_type   = $rel_meta->{name};
433 #    if ($rel_type =~ /(^has_own$|^has_a$)/) {
434 #      my $fclass= $rel_meta->{foreign_class};
435 #      my ($rel_obj, $errs) = $fclass->_do_create_all($related{$accssr});
436 #      # put id in parent's data hash 
437 #      if (not keys %$errs) {
438 #       $validated->{$accssr} = $rel_obj->id;
439 #      } else {
440 #       $errors->{$accssr} = $errs;
441 #      }
442 #      delete $related{$accssr}; # done with this 
443 #    }
444 #  }
445
446   # Make main object -- base case
447   #warn "\n*** validated data is " . Dumper($validated). "***\n";
448   my $me_obj  = eval { $self->create($validated) };
449   if ($@) { 
450         warn "Just failed making a " . $self. " FATAL Error is $@"
451                 if (eval{$self->model_debug});  
452     $errors->{FATAL} = $@; 
453     return (undef, $errors);
454   }
455         
456   if (eval{$self->model_debug}) {
457     if ($me_obj) {
458       warn "Just made a $self : $me_obj ( " . $me_obj->id . ")";
459     } else {
460       warn "Just failed making a " . $self. " FATAL Error is $@" if not $me_obj;
461     }
462   }
463
464   # Make other related (must_have, might_have, has_many  etc )
465   foreach $accssr ( keys %related ) {
466     my ($rel_obj, $errs) = 
467       $me_obj->_create_related($accssr, $related{$accssr});
468     $errors->{$accssr} = $errs if $errs;
469         
470   }
471   #warn "Errors are " . Dumper($errors);
472
473   undef $errors unless keys %$errors;
474   return ($me_obj, $errors);
475 }
476
477
478 ##################
479 # _do_update_all #
480 ##################
481
482 #  Updates objects from hashed untainted data 
483 # Returns 1 
484
485 sub _do_update_all {
486         my ($self, $validated) = @_;
487         my ($errors, $accssr); 
488
489         #  Separate out related objects' data from main hash 
490         my %related;
491         foreach (keys %$validated) {
492                 $related{$_}= delete $validated->{$_} if ref $validated->{$_} eq 'HASH';
493         }
494         # Update main obj 
495         # set does not work with IsA right now so we set each col individually
496         #$self->set(%$validated);
497         my $old = $self->autoupdate(0); 
498         for (keys %$validated) {
499                 $self->$_($validated->{$_});
500         }
501         $self->update;
502         $self->autoupdate($old);
503
504         # Update related
505         foreach $accssr (keys %related) {
506                 my $fobj = $self->$accssr;
507                 my $validated = $related{$accssr};
508                 if ($fobj) {
509                         my $old = $fobj->autoupdate(0); 
510                         for (keys %$validated) {
511                                 $fobj->$_($validated->{$_});
512                         }
513                         $fobj->update;
514                         $fobj->autoupdate($old);
515                 }
516                 else { 
517                         $fobj = $self->_create_related($accssr, $related{$accssr});
518                 }       
519         }
520         return 1;
521 }
522         
523
524 ###################
525 # _create_related #
526 ###################
527
528 # Creates and automatically relates newly created object to calling object 
529 # Returns related object and errors ($obj, $errors).  
530 # If no errors, then undef in that slot.
531
532 sub _create_related {
533         # self is object or class, accssr is accssr to relationship, params are 
534         # data for relobject, and created is the array ref to store objs we 
535         # create (optional).
536         my ( $self, $accssr, $params, $created )  = @_;
537         $self->_croak ("Can't make related object without a parent $self object") 
538                 unless ref $self;
539         $created      ||= [];
540         my  $rel_meta = $self->related_meta('r',$accssr);
541     if (!$rel_meta) {
542                 $self->_croak("No relationship for $accssr in " . ref($self));
543         }
544         my $rel_type  = $rel_meta->{name};
545         my $fclass    = $rel_meta->{foreign_class};
546         #warn " Dumper of meta is " . Dumper($rel_meta);
547         
548
549         my ($rel, $errs); 
550
551         # Set up params for might_have, has_many, etc
552         if ($rel_type ne 'has_own' and $rel_type ne 'has_a') {
553
554                 # Foreign Key meta data not very standardized in CDBI
555                 my $fkey= $rel_meta->{args}{foreign_key} || $rel_meta->{foreign_column};
556                 unless ($fkey) { die " Could not determine foreign key for $fclass"; }
557                 my %data = (%$params, $fkey => $self->id);
558                 %data = ( %data, %{$rel_meta->{args}->{constraint} || {}} );
559                 #warn "Data is " . Dumper(\%data);
560             ($rel, $errs) =  $fclass->_do_create_all(\%data, $created);
561         }
562         else { 
563             ($rel, $errs) =  $fclass->_do_create_all($params, $created);
564                 unless ($errs) {
565                         $self->$accssr($rel->id);
566                         $self->update;
567                 }
568         }
569         return ($rel, $errs);
570 }
571
572
573
574                 
575 =head2  classify_form_inputs
576
577 $self->classify_form_inputs($params[, $delimiter]);
578
579 Foreign inputs are inputs that have data for a related table.
580 They come named so we can tell which related class they belong to.
581 This assumes the form : $accessor . $delimeter . $column recursively 
582 classifies them into hashes. It returns a hashref.
583
584 =cut
585
586 sub classify_form_inputs {
587         my ($self, $params, $delimiter) = @_;
588         my %hashed = ();
589         my $bottom_level;
590         $delimiter ||= $self->foreign_input_delimiter;
591         foreach my $input_name (keys %$params) {
592                 my @accssrs  = split /$delimiter/, $input_name;
593                 my $col_name = pop @accssrs;    
594                 $bottom_level = \%hashed;
595                 while ( my $a  = shift @accssrs ) {
596                         $bottom_level->{$a} ||= {};
597                         $bottom_level = $bottom_level->{$a};  # point to bottom level
598                 }
599                 # now insert parameter at bottom level keyed on col name
600                 $bottom_level->{$col_name} = $params->{$input_name};
601         }
602         return  \%hashed;
603 }
604
605 sub _untaint_handlers {
606     my ($me, $them) = @_;
607     return () unless $them->can('__untaint_types');
608     my %type = %{ $them->__untaint_types || {} };
609     my %h;
610     @h{ @{ $type{$_} } } = ($_) x @{ $type{$_} } foreach keys %type;
611     return %h;
612 }
613
614 sub _column_type_for {
615     my $type = lc shift;
616     $type =~ s/\(.*//;
617     my %map = (
618         varchar   => 'printable',
619         char      => 'printable',
620         text      => 'printable',
621         tinyint   => 'integer',
622         smallint  => 'integer',
623         mediumint => 'integer',
624         int       => 'integer',
625         integer   => 'integer',
626         bigint    => 'integer',
627         year      => 'integer',
628         date      => 'date',
629     );
630     return $map{$type} || "";
631 }
632
633 =head1 MAINTAINER 
634
635 Maypole Developers
636
637 =head1 AUTHORS
638
639 Peter Speltz, Aaron Trevena 
640
641 =head1 AUTHORS EMERITUS
642
643 Tony Bowden
644
645 =head1 TODO
646
647 * Tests
648 * add_to_from_cgi, search_from_cgi
649 * complete documentation
650 * ensure full backward compatibility with Class::DBI::FromCGI
651
652 =head1 BUGS and QUERIES
653
654 Please direct all correspondence regarding this module to:
655  Maypole list.
656
657 =head1 COPYRIGHT AND LICENSE
658
659 Copyright 2003-2004 by Peter Speltz 
660
661 This library is free software; you can redistribute it and/or modify
662 it under the same terms as Perl itself.
663
664 =head1 SEE ALSO
665
666 L<Class::DBI>, L<Class::DBI::FromCGI>
667
668 =cut
669
670 1;
671
672