The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
package Maypole::Model::CDBI::FromCGI;
use strict;
use warnings;

=head1 NAME

Maypole::Model:CDBI::FromCGI - Validate form input and populate Model objects

=head1 SYNOPSIS

  $obj = $class->create_from_cgi($r);
  $obj = $class->create_from_cgi($r, { params => {data1=>...}, required => [..],
		 ignore => [...], all => [...]);
  $obj = $class->create_from_cgi($h, $options); # CDBI::FromCGI style, see docs

  $obj->update_from_cgi($r);
  $obj->update_from_cgi($h, $options);

  $obj = $obj->add_to_from_cgi($r);
  $obj = $obj->add_to_from_cgi($r, { params => {...} } );

  # This does not work like in CDBI::FromCGI and probably never will :
  # $class->update_from_cgi($h, @columns);


=head1 DESCRIPTION

Provides a way to validate form input and populate Model Objects, based
on Class::DBI::FromCGI.

=cut


# The base base model class for apps 
# provides good search and create functions

use base qw(Exporter); 
use CGI::Untaint;
use Maypole::Constants;
use CGI::Untaint::Maypole;
our $Untainter = 'CGI::Untaint::Maypole';

our @EXPORT = qw/update_from_cgi create_from_cgi untaint_columns add_to_from_cgi
    cgi_update_errors untaint_type validate_inputs validate_all _do_update_all 
    _do_create_all _create_related classify_form_inputs/;



use Data::Dumper; # for debugging

=head1 METHODS

=head2 untaint_columns

Replicates Class::DBI::FromCGI method of same name :

  __PACKAGE__->untaint_columns(
    printable => [qw/Title Director/],
    integer   => [qw/DomesticGross NumExplodingSheep],
    date      => [qw/OpeningDate/],
  );

=cut

sub untaint_columns {
    die "untaint_columns() needs a hash" unless @_ % 2;
    my ($class, %args) = @_;
    $class->mk_classdata('__untaint_types')
        unless $class->can('__untaint_types');
    my %types = %{ $class->__untaint_types || {} };
    while (my ($type, $ref) = each(%args)) {
        $types{$type} = $ref;
    }
    $class->__untaint_types(\%types);
}

=head2 untaint_type

  gets the  untaint type for a column as set in "untaint_types"

=cut

# get/set untaint_type for a column
sub untaint_type {
    my ($class, $field, $new_type) = @_;
    my %handler = __PACKAGE__->_untaint_handlers($class);
    return $handler{$field} if $handler{$field};
    my $handler = eval {
        local $SIG{__WARN__} = sub { };
        my $type = $class->column_type($field) or die;
        _column_type_for($type);
    };
    return $handler || undef;
}

=head2 cgi_update_errors

Returns errors that ocurred during an operation.

=cut

sub cgi_update_errors { %{ shift->{_cgi_update_error} || {} } }

=head2 create_from_cgi

Based on the same method in Class::DBI::FromCGI.

Creates  multiple objects  from a  cgi form. 
Errors are returned in cgi_update_errors

It can be called Maypole style passing the Maypole request object as the
first arg, or Class::DBI::FromCGI style passing the Untaint Handler ($h)
as the first arg. 

A hashref of options can be passed as the second argument. Unlike 
in the CDBI equivalent, you can *not* pass a list as the second argument.
Options can be :
 params -- hashref of cgi data to use instead of $r->params,
 required -- list of fields that are required
 ignore   -- list of fields to ignore
 all      -- list of all fields (defaults to $class->columns)

=cut

sub create_from_cgi {
  my ($self, $r, $opts) = @_;
  $self->_croak( "create_from_cgi can only be called as a class method")
    if ref $self;
  my ($errors, $validated);
  
  
  if ($r->isa('CGI::Untaint')) { # FromCGI interface compatibility
    ($validated, $errors) = $self->validate_inputs($r,$opts); 
  } else {
    my $params = $opts->{params} || $r->params;
    $opts->{params} = $self->classify_form_inputs($params);
    ($validated, $errors) = $self->validate_all($r, $opts);
  }

  if (keys %$errors) {
    return bless { _cgi_update_error => $errors }, $self;
  }

  # Insert all the data
  my ($obj, $err ) = $self->_do_create_all($validated); 
  if ($err) {
    return bless { _cgi_update_error => $err }, $self;
  }
  return $obj;
}


=head2 update_from_cgi

Replicates the Class::DBI::FromCGI method of same name. It updates an object and
returns 1 upon success. It can take the same arguments as create_form_cgi. 
If errors, it sets the cgi_update_errors.

=cut

sub update_from_cgi {
  my ($self, $r, $opts) = @_;
  $self->_croak( "update_from_cgi can only be called as an object method") unless ref $self;
  my ($errors, $validated);
  $self->{_cgi_update_error} = {};
  $opts->{updating} = 1;

  # FromCGI interface compatibility 
  if ($r->isa('CGI::Untaint')) {
    # REHASH the $opts for updating:
    # 1: we ignore any fields we dont have parmeter for. (safe ?)
    # 2: we dont want to update fields unless they change

    my @ignore = @{$opts->{ignore} || []};
    push @ignore, $self->primary_column->name;
    my $raw = $r->raw_data;
    #print "*** raw data ****" . Dumper($raw);
    foreach my $field ($self->columns) {
      #print "*** field is $field ***\n";
      	if (not defined $raw->{$field}) {
			push @ignore, $field->name; 
			#print "*** ignoring $field because it is not present ***\n";
			next;
      	}
      	# stupid inflation , cant get at raw db value easy, must call
      	# deflate ***FIXME****
      	my $cur_val = ref $self->$field ? $self->$field->id : $self->$field;
      	if ($raw->{$field} eq $cur_val) {
			#print "*** ignoring $field because unchanged ***\n";
			push @ignore, "$field"; 
      	}
    }
    $opts->{ignore} = \@ignore;
    ($validated, $errors) = $self->validate_inputs($r,$opts); 
  } else {
    my $params = $opts->{params} || $r->params;
    $opts->{params} = $self->classify_form_inputs($params);
    ($validated, $errors) = $self->validate_all($r, $opts);
    #print "*** errors for validate all   ****" . Dumper($errors);
  }

  if (keys %$errors) {
    #print "*** we have errors   ****" . Dumper($errors);
    $self->{_cgi_update_error} = $errors;
    return;
  }

  # Update all the data
  my ($obj, $err ) = $self->_do_update_all($validated); 
  if ($err) {
    $self->{_cgi_update_error} = $err;
    return; 
  }
  return 1;
}

=head2 add_to_from_cgi

$obj->add_to_from_cgi($r[, $opts]); 

Like add_to_* for has_many relationships but will add nay objects it can 
figure out from the data.  It returns a list of objects it creates or nothing
on error. Call cgi_update_errors with the calling object to get errors.
Fatal errors are in the respective "FATAL" key.

=cut

sub add_to_from_cgi {
  my ($self, $r, $opts) = @_;
  $self->_croak( "add_to_from_cgi can only be called as an object method")
    unless ref $self;
  my ($errors, $validated, @created);
   
  my $params = $opts->{params} || $r->params;
  $opts->{params} = $self->classify_form_inputs($params);
  ($validated, $errors) = $self->validate_all($r, $opts);

  
  if (keys %$errors) {
    $self->{_cgi_update_error} = $errors;
	return;
  }

  # Insert all the data
  foreach my $hm (keys %$validated) { 
	my ($obj, $errs) = $self->_create_related($hm, $validated->{$hm}); 
	if (not $errs) {
		push @created, $obj;
	}else {
		$errors->{$hm} = $errs;
	}
  }
  
  if (keys %$errors) {
    $self->{_cgi_update_error} = $errors;
	return;
  }

  return @created;
}

 


=head2 validate_all

Validates (untaints) a hash of possibly mixed table data. 
Returns validated and errors ($validated, $errors).
If no errors then undef in that spot.

=cut

sub validate_all {
  my ($self, $r, $opts) = @_;
  my $class = ref $self || $self;
  my $classified = $opts->{params};
  my $updating   = $opts->{updating};

  # Base case - validate this classes data
  $opts->{all}   ||= eval{ $r->config->{$self->table}{all_cols} } || [$self->columns('All')];
  $opts->{required} ||= eval { $r->config->{$self->table}{required_cols} || $self->required_columns } || [];
  my $ignore = $opts->{ignore} || eval{ $r->config->{$self->table}{ignore_cols} } || [];
  push @$ignore, $self->primary_column->name if $updating;
  
  # Ignore hashes of foreign inputs. This takes care of required has_a's 
  # for main object that we have foreign inputs for. 
  foreach (keys %$classified) {
    push @$ignore, $_ if  ref $classified->{$_} eq 'HASH'; 
  }
  $opts->{ignore} = $ignore;
  my $h = $Untainter->new($classified);
  my ($validated, $errs) = $self->validate_inputs($h, $opts);

  # Validate all foreign input
	
  #warn "Classified data is " . Dumper($classified); 
  foreach my $field (keys %$classified) {
    if (ref $classified->{$field} eq "HASH") {
      my $data = $classified->{$field};
  	  my $ignore = [];
      my @usr_entered_vals = ();
      foreach ( values %$data ) {
		push @usr_entered_vals, $_  if $_  ne '';
      }

      # filled in values
      # IF we have some inputs for the related
      if ( @usr_entered_vals ) {
		# We need to ignore us if we are a required has_a in this foreign class
		my $rel_meta = $self->related_meta($r, $field);
	    my $fclass   = $rel_meta->{foreign_class};
		my $fmeta    = $fclass->meta_info('has_a');
		for (keys %$fmeta) {
			if ($fmeta->{$_}{foreign_class} eq $class) {
				push @$ignore, $_;
			}
		}
		my ($valid, $ferrs) = $fclass->validate_all($r,
		{params => $data, updating => $updating, ignore => $ignore } ); 	

		$errs->{$field} = $ferrs if $ferrs;
		$validated->{$field} = $valid;

      } else { 
		# Check this foreign object is not requeired
		my %req = map { $_ => 1 } $opts->{required};
		if ($req{$field}) {
	  		$errs->{$field}{FATAL} = "This is required. Please enter the required fields in this section." 
			}
		}
  	}
  }
  #warn "Validated inputs are " . Dumper($validated);
  undef $errs unless keys %$errs;
  return ($validated, $errs);	
}



=head2 validate_inputs

$self->validate_inputs($h, $opts);

This is the main validation method to validate inputs for a single class.
Most of the time you use validate_all.

Returns validated and errors.

If no errors then undef in that slot.

Note: This method is currently experimental (in 2.11) and may be subject to change
without notice.

=cut

sub validate_inputs {
  my ($self, $h, $opts) = @_;
  my $updating = $opts->{updating};
  my %required = map { $_ => 1 } @{$opts->{required}};
  my %seen;
  $seen{$_}++ foreach @{$opts->{ignore}};
  my $errors 	= {}; 
  my $fields 	= {};
  $opts->{all} = [ $self->columns ] unless @{$opts->{all} || [] } ;
  foreach my $field (@{$opts->{required}}, @{$opts->{all}}) {
    next if $seen{$field}++;
    my $type = $self->untaint_type($field) or 
      do { warn "No untaint type for $self 's field $field. Ignoring.";
	   next;
	 };
    my $value = $h->extract("-as_$type" => $field);
    my $err = $h->error;

    # Required field error 
    if ($required{$field} and !ref($value) and $err =~ /^No input for/) {
      $errors->{$field} = "You must supply '$field'" 
    } elsif ($err) {

      # 1: No inupt entered
      if ($err =~ /^No input for/) {
				# A : Updating -- set the field to undef or '' 
	if ($updating) { 
	  $fields->{$field} = eval{$self->column_nullable($field)} ? 
	    undef : ''; 
	}
				# B : Creating -- dont set a value and RDMS will put default
      }

      # 2: A real untaint error -- just set the error 
      elsif ($err !~ /^No parameter for/) {
	$errors->{$field} =  $err;
      }
    } else {
      $fields->{$field} = $value
    }
  }
  undef $errors unless keys %$errors;
  return ($fields, $errors);
}


##################
# _do_create_all #
##################

# Untaints and Creates objects from hashed params.
# Returns parent object and errors ($obj, $errors).  
# If no errors, then undef in that slot.
sub _do_create_all {
  my ($self, $validated) = @_;
  my $class = ref $self  || $self;
  my ($errors, $accssr); 

  # Separate out related objects' data from main hash 
  my %related;
  foreach (keys %$validated) {
    $related{$_}= delete $validated->{$_} if ref $validated->{$_} eq 'HASH';
  }

  # Make main object -- base case
  #warn "\n*** validated data is " . Dumper($validated). "***\n";
  my $me_obj  = eval { $self->create($validated) };
  if ($@) { 
    warn "Just failed making a " . $self. " FATAL Error is $@"
      if (eval{$self->model_debug});  
    $errors->{FATAL} = $@; 
    return (undef, $errors);
  }

  if (eval{$self->model_debug}) {
    if ($me_obj) {
      warn "Just made a $self : $me_obj ( " . $me_obj->id . ")";
    } else {
      warn "Just failed making a " . $self. " FATAL Error is $@" if not $me_obj;
    }
  }

  # Make other related (must_have, might_have, has_many  etc )
  foreach $accssr ( keys %related ) {
    my ($rel_obj, $errs) = 
      $me_obj->_create_related($accssr, $related{$accssr});
    $errors->{$accssr} = $errs if $errs;

  }
  #warn "Errors are " . Dumper($errors);

  undef $errors unless keys %$errors;
  return ($me_obj, $errors);
}


##################
# _do_update_all #
##################

#  Updates objects from hashed untainted data 
# Returns 1 

sub _do_update_all {
	my ($self, $validated) = @_;
	my ($errors, $accssr); 

	#  Separate out related objects' data from main hash 
	my %related;
	foreach (keys %$validated) {
		$related{$_}= delete $validated->{$_} if ref $validated->{$_} eq 'HASH';
	}
	# Update main obj 
	# set does not work with IsA right now so we set each col individually
	#$self->set(%$validated);
	my $old = $self->autoupdate(0); 
	for (keys %$validated) {
		$self->$_($validated->{$_});
	}
	$self->update;
	$self->autoupdate($old);

	# Update related
	foreach $accssr (keys %related) {
		my $fobj = $self->$accssr;
		my $validated = $related{$accssr};
		if ($fobj) {
			my $old = $fobj->autoupdate(0); 
			for (keys %$validated) {
				$fobj->$_($validated->{$_});
			}
			$fobj->update;
			$fobj->autoupdate($old);
		}
		else { 
			$fobj = $self->_create_related($accssr, $related{$accssr});
		}	
	}
	return 1;
}
	

###################
# _create_related #
###################

# Creates and automatically relates newly created object to calling object 
# Returns related object and errors ($obj, $errors).  
# If no errors, then undef in that slot.

sub _create_related {
    # self is object or class, accssr is accssr to relationship, params are 
    # data for relobject, and created is the array ref to store objs we 
    # create (optional).
    my ( $self, $accssr, $params, $created )  = @_;
    $self->_croak ("Can't make related object without a parent $self object") 
	unless ref $self;
    $created      ||= [];
    my  $rel_meta = $self->related_meta('r',$accssr);
    if (!$rel_meta) {
	$self->_carp("[_create_related] No relationship for $accssr in " . ref($self));
	return;
    }
    my $rel_type  = $rel_meta->{name};
    my $fclass    = $rel_meta->{foreign_class};
    #warn " Dumper of meta is " . Dumper($rel_meta);


    my ($rel, $errs); 

    # Set up params for might_have, has_many, etc
    if ($rel_type ne 'has_own' and $rel_type ne 'has_a') {

	# Foreign Key meta data not very standardized in CDBI
	my $fkey= $rel_meta->{args}{foreign_key} || $rel_meta->{foreign_column};
	unless ($fkey) { die " Could not determine foreign key for $fclass"; }
	my %data = (%$params, $fkey => $self->id);
	%data = ( %data, %{$rel_meta->{args}->{constraint} || {}} );
	#warn "Data is " . Dumper(\%data);
	($rel, $errs) =  $fclass->_do_create_all(\%data, $created);
    }
    else { 
	($rel, $errs) =  $fclass->_do_create_all($params, $created);
	unless ($errs) {
	    $self->$accssr($rel->id);
	    $self->update;
	}
    }
    return ($rel, $errs);
}



		
=head2  classify_form_inputs

$self->classify_form_inputs($params[, $delimiter]);

Foreign inputs are inputs that have data for a related table.
They come named so we can tell which related class they belong to.
This assumes the form : $accessor . $delimeter . $column recursively 
classifies them into hashes. It returns a hashref.

=cut

sub classify_form_inputs {
	my ($self, $params, $delimiter) = @_;
	my %hashed = ();
	my $bottom_level;
	$delimiter ||= $self->foreign_input_delimiter;
	foreach my $input_name (keys %$params) {
		my @accssrs  = split /$delimiter/, $input_name;
		my $col_name = pop @accssrs;	
		$bottom_level = \%hashed;
		while ( my $a  = shift @accssrs ) {
			$bottom_level->{$a} ||= {};
			$bottom_level = $bottom_level->{$a};  # point to bottom level
		}
		# now insert parameter at bottom level keyed on col name
		$bottom_level->{$col_name} = $params->{$input_name};
	}
	return  \%hashed;
}

sub _untaint_handlers {
    my ($me, $them) = @_;
    return () unless $them->can('__untaint_types');
    my %type = %{ $them->__untaint_types || {} };
    my %h;
    @h{ @{ $type{$_} } } = ($_) x @{ $type{$_} } foreach keys %type;
    return %h;
}

sub _column_type_for {
    my $type = lc shift;
    $type =~ s/\(.*//;
    my %map = (
        varchar   => 'printable',
        char      => 'printable',
        text      => 'printable',
        tinyint   => 'integer',
        smallint  => 'integer',
        mediumint => 'integer',
        int       => 'integer',
        integer   => 'integer',
        bigint    => 'integer',
        year      => 'integer',
        date      => 'date',
    );
    return $map{$type} || "";
}

=head1 MAINTAINER 

Maypole Developers

=head1 AUTHORS

Peter Speltz, Aaron Trevena 

=head1 AUTHORS EMERITUS

Tony Bowden

=head1 TODO

* Tests
* add_to_from_cgi, search_from_cgi
* complete documentation
* ensure full backward compatibility with Class::DBI::FromCGI

=head1 BUGS and QUERIES

Please direct all correspondence regarding this module to:
 Maypole list.

=head1 COPYRIGHT AND LICENSE

Copyright 2003-2004 by Peter Speltz 

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=head1 SEE ALSO

L<Class::DBI>, L<Class::DBI::FromCGI>

=cut

1;