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