package CGI::Ex::Validate;

### CGI Extended Validator

###----------------------------------------------------------------###
#  Copyright 2004 - Paul Seamons                                     #
#  Distributed under the Perl Artistic License without warranty      #
###----------------------------------------------------------------###

### See perldoc at bottom

use strict;
use vars qw($VERSION
            $ERROR_PACKAGE
            $DEFAULT_EXT
            %DEFAULT_OPTIONS
            $JS_URI_PATH
            $JS_URI_PATH_YAML
            $JS_URI_PATH_VALIDATE
            $QR_EXTRA
            @UNSUPPORTED_BROWSERS
            );

$VERSION = '1.14';

$ERROR_PACKAGE = 'CGI::Ex::Validate::Error';
$DEFAULT_EXT   = 'val';
$QR_EXTRA      = qr/^(\w+_error|as_(array|string|hash)_\w+|no_\w+)/;
@UNSUPPORTED_BROWSERS = (qr/MSIE\s+5.0\d/i);

use CGI::Ex::Conf ();

###----------------------------------------------------------------###

sub new {
  my $class = shift || __PACKAGE__;
  my $self  = (@_ && ref($_[0])) ? shift : {@_};

  ### allow for global defaults
  foreach (keys %DEFAULT_OPTIONS) {
    $self->{$_} = $DEFAULT_OPTIONS{$_} if ! exists $self->{$_};
  }

  return bless $self, $class;
}

###----------------------------------------------------------------###

sub cgix {
  my $self = shift;
  return $self->{cgix} ||= do {
    require CGI::Ex;
    CGI::Ex->new;
  };
}

sub conf {
  my $self = shift;
  return $self->{conf_obj} ||= CGI::Ex::Conf->new({
    default_ext => $DEFAULT_EXT,
    directive   => 'LAST',
  });
}

### the main validation routine
sub validate {
  my $self = (! ref($_[0])) ? shift->new                    # $class->validate
              : UNIVERSAL::isa($_[0], __PACKAGE__) ? shift  # $self->validate
              : __PACKAGE__->new;                           # &validate
  my $form     = shift || die "Missing form hash";
  my $val_hash = shift || die "Missing validation hash";
  my $what_was_validated = shift; # allow for extra arrayref that stores what was validated

  ### turn the form into a form if it is really a CGI object
  if (! ref($form)) {
    die "Invalid form hash or cgi object";
  } elsif(! UNIVERSAL::isa($form,'HASH')) {
    local $self->{cgi_object} = $form;
    $form = $self->cgix->get_form($form);
  }

  ### get the validation - let get_validation deal with types
  ### if a ref is not passed - assume it is a filename
  $val_hash = $self->get_validation($val_hash);

  ### allow for validation passed as single group hash, single group array,
  ### or array of group hashes or group arrays
  my @ERRORS = ();
  my %EXTRA  = ();
  my @USED_GROUPS = ();
  my $group_order = (UNIVERSAL::isa($val_hash,'HASH')) ? [$val_hash] : $val_hash;
  foreach my $group_val (@$group_order) {
    die "Validation groups must be a hashref" if ! UNIVERSAL::isa($group_val,'HASH');
    my $title       = $group_val->{'group title'};
    my $validate_if = $group_val->{'group validate_if'};

    ### only validate this group if it is supposed to be checked
    next if $validate_if && ! $self->check_conditional($form, $validate_if);
    push @USED_GROUPS, $group_val;

    ### If the validation items were not passed as an arrayref.
    ### Look for a group order and then fail back to the keys of the group.
    ### We will keep track of what was added using %found - the keys will
    ###   be the hash signatures of the field_val hashes (ignore the hash internals).
    my @order  = sort keys %$group_val;
    my $fields = $group_val->{'group fields'};
    my %found = (); # attempt to keep track of what field_vals have been added
    if ($fields) { # if I passed group fields array - use it
      die "'group fields' must be an arrayref" if ! UNIVERSAL::isa($fields,'ARRAY');
    } else { # other wise - create our own array
      my @fields = ();
      if (my $order = $group_val->{'group order'} || \@order) {
        die "Validation 'group order' must be an arrayref" if ! UNIVERSAL::isa($order,'ARRAY');
        foreach my $field (@$order) {
          next if $field =~ /^(group|general)\s/;
          my $field_val = exists($group_val->{$field}) ? $group_val->{$field}
            : ($field eq 'OR') ? 'OR' : die "No element found in group for $field";
          $found{"$field_val"} = 1; # do this before modifying on the next line
          if (ref $field_val && ! $field_val->{'field'}) {
            $field_val = { %$field_val, 'field' => $field }; # copy the values to add the key
          }
          push @fields, $field_val;
        }
      }
      $fields = \@fields;
    }

    ### double check which field_vals have been used so far
    foreach my $field_val (@$fields) {
      my $field = $field_val->{'field'} || die "Missing field key in validation";
      $found{"$field_val"} = 1;
    }

    ### add any remaining field_vals from the order
    ### this is necessary for items that weren't in group fields or group order
    foreach my $field (@order) {
      next if $field =~ /^(group|general)\s/;
      my $field_val = $group_val->{$field};
      die "Found a nonhashref value on field $field" if ! UNIVERSAL::isa($field_val, 'HASH');
      next if $found{"$field_val"}; # do before modifying ref on next line
      $field_val = { %$field_val, 'field' => $field } if ! $field_val->{'field'}; # copy the values
      push @$fields, $field_val;
    }

    ### Finally we have our arrayref of hashrefs that each have their 'field' key
    ### now lets do the validation
    my $found  = 1;
    my @errors = ();
    my $hold_error; # hold the error for a moment - to allow for an "Or" operation
    foreach (my $i = 0; $i <= $#$fields; $i ++) {
      my $ref = $fields->[$i];
      if (! ref($ref) && $ref eq 'OR') {
        $i ++ if $found; # if found skip the OR altogether
        $found = 1; # reset
        next;
      }
      $found = 1;
      die "Missing field key during normal validation" if ! $ref->{'field'};
      local $ref->{'was_validated'} = 1;
      my @err = $self->validate_buddy($form, $ref->{'field'}, $ref);
      if (delete($ref->{'was_validated'}) && $what_was_validated) {
        push @$what_was_validated, $ref;
      }

      ### test the error - if errors occur allow for OR - if OR fails use errors from first fail
      if (scalar @err) {
        if ($i < $#$fields && ! ref($fields->[$i + 1]) && $fields->[$i + 1] eq 'OR') {
          $hold_error = \@err;
        } else {
          push @errors, $hold_error ? @$hold_error : @err;
          $hold_error = undef;
        }
      } else {
        $hold_error = undef;
      }
    }
    push(@errors, @$hold_error) if $hold_error; # allow for final OR to work

    ### add on errors as requested
    if ($#errors != -1) {
      push @ERRORS, $title if $title;
      push @ERRORS, @errors;
    }

    ### add on general options, and group options if errors in group occurred
    foreach my $field (@order) {
      next if $field !~ /^(general|group)\s+(\w+)$/;
      my $key = $2;
      next if $1 eq 'group' && ($#errors == -1 || $key =~ /^(field|order|title)$/);
      $EXTRA{$key} = $group_val->{$field};
    }
  }

  ### store any extra items from self
  foreach my $key (keys %$self) {
    next if $key !~ $QR_EXTRA;
    $EXTRA{$key} = $self->{$key};
  }

  ### allow for checking for unused keys
  if ($EXTRA{no_extra_fields}) {
    my $which = ($EXTRA{no_extra_fields} =~ /used/i) ? 'used' : 'all';
    my $ref   = ($which eq 'all') ? $val_hash : \@USED_GROUPS;
    my $keys  = $self->get_validation_keys($ref);
    foreach my $key (sort keys %$form) {
      next if $keys->{$key};
      $self->add_error(\@ERRORS, $key, 'no_extra_fields', {}, undef);
    }
  }

  ### return what they want
  if ($#ERRORS != -1) {
    my $err_obj = $ERROR_PACKAGE->new(\@ERRORS, \%EXTRA);
    die    $err_obj if $EXTRA{raise_error};
    return $err_obj;
  } else {
    return wantarray ? () : undef;
  }
}


### allow for optional validation on groups and on individual items
sub check_conditional {
  my ($self, $form, $ifs, $N_level, $ifs_match) = @_;

  $N_level ||= 0;
  $N_level ++; # prevent too many recursive checks

  ### can pass a single hash - or an array ref of hashes
  if (! $ifs) {
    die "Need reference passed to check_conditional";
  } elsif (! ref($ifs)) {
    $ifs = [$ifs];
  } elsif (UNIVERSAL::isa($ifs,'HASH')) {
    $ifs = [$ifs];
  }

  ### run the if options here
  ### multiple items can be passed - all are required unless OR is used to separate
  my $found = 1;
  foreach (my $i = 0; $i <= $#$ifs; $i ++) {
    my $ref = $ifs->[$i];
    if (! ref $ref) {
      if ($ref eq 'OR') {
        $i ++ if $found; # if found skip the OR altogether
        $found = 1; # reset
        next;
      } else {
        if ($ref =~ s/^\s*!\s*//) {
          $ref = {field => $ref, max_in_set => "0 of $ref"};
        } else {
          $ref = {field => $ref, required => 1};
        }
      }
    }
    last if ! $found;

    ### get the field - allow for custom variables based upon a match
    my $field = $ref->{'field'} || die "Missing field key during validate_if (possibly used a reference to a main hash *foo -> &foo)";
    $field =~ s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg if $ifs_match;

    my @err = $self->validate_buddy($form, $field, $ref, $N_level);
    $found = 0 if scalar @err;
  }
  return $found;
}


### this is where the main checking goes on
sub validate_buddy {
  my $self = shift;
  my ($form, $field, $field_val, $N_level, $ifs_match) = @_;
  $N_level ||= 0;
  $N_level ++; # prevent too many recursive checks
  die "Max dependency level reached $N_level" if $N_level > 10;

  my @errors = ();
  my $types  = [sort keys %$field_val];

  ### allow for not running some tests in the cgi
  if (scalar $self->filter_type('exclude_cgi',$types)) {
    delete $field_val->{'was_validated'};
    return wantarray ? @errors : $#errors + 1;
  }

  ### allow for field names that contain regular expressions
  if ($field =~ m/^(!\s*|)m([^\s\w])(.*)\2([eigsmx]*)$/s) {
    my ($not,$pat,$opt) = ($1,$3,$4);
    $opt =~ tr/g//d;
    die "The e option cannot be used on validation keys on field $field" if $opt =~ /e/;
    foreach my $_field (sort keys %$form) {
      next if ($not && $_field =~ m/(?$opt:$pat)/) || (! $not && $_field !~ m/(?$opt:$pat)/);
      my @match = (undef,$1,$2,$3,$4,$5); # limit to the matches
      push @errors, $self->validate_buddy($form, $_field, $field_val, $N_level, \@match);
    }
    return wantarray ? @errors : $#errors + 1;
  }

  ### allow for default value
  foreach my $type ($self->filter_type('default', $types)) {
    if (! defined($form->{$field}) || (! ref($form->{$field}) && ! length($form->{$field}))) {
      $form->{$field} = $field_val->{$type};
    }
  }

  my $n_values = UNIVERSAL::isa($form->{$field},'ARRAY') ? $#{ $form->{$field} } + 1 : 1;
  my $values = ($n_values > 1) ? $form->{$field} : [$form->{$field}];

  ### allow for a few form modifiers
  my $modified = 0;
  foreach my $value (@$values) {
    next if ! defined $value;
    if (! scalar $self->filter_type('do_not_trim',$types)) { # whitespace
      $value =~ s/^\s+//;
      $value =~ s/\s+$//;
      $modified = 1;
    }
    if (scalar $self->filter_type('to_upper_case',$types)) { # uppercase
      $value = uc($value);
      $modified = 1;
    } elsif (scalar $self->filter_type('to_lower_case',$types)) { # lowercase
      $value = lc($value);
      $modified = 1;
    }
  }
  # allow for inline specified modifications (ie s/foo/bar/)
  foreach my $type ($self->filter_type('replace',$types)) {
    my $ref = UNIVERSAL::isa($field_val->{$type},'ARRAY') ? $field_val->{$type}
      : [split(/\s*\|\|\s*/,$field_val->{$type})];
    foreach my $rx (@$ref) {
      if ($rx !~ m/^\s*s([^\s\w])(.+)\1(.*)\1([eigsmx]*)$/s) {
        die "Not sure how to parse that match ($rx)";
      }
      my ($pat,$swap,$opt) = ($2,$3,$4);
      die "The e option cannot be used in swap on field $field" if $opt =~ /e/;
      my $global = $opt =~ s/g//g;
      $swap =~ s/\\n/\n/g;
      if ($global) {
        foreach my $value (@$values) {
          $value =~ s{(?$opt:$pat)}{
            my @match = (undef,$1,$2,$3,$4,$5,$6); # limit on the number of matches
            my $copy = $swap;
            $copy =~ s/\$(\d+)/defined($match[$1]) ? $match[$1] : ""/ge;
            $modified = 1;
            $copy; # return of the swap
          }eg;
        }
      }else{
        foreach my $value (@$values) {
          $value =~ s{(?$opt:$pat)}{
            my @match = (undef,$1,$2,$3,$4,$5,$6); # limit on the number of matches
            my $copy = $swap;
            $copy =~ s/\$(\d+)/defined($match[$1]) ? $match[$1] : ""/ge;
            $modified = 1;
            $copy; # return of the swap
          }e;
        }
      }
    }
  }
  ### put them back into the form if we have modified it
  if ($modified) {
    if ($n_values == 1) {
      $form->{$field} = $values->[0];
      $self->{cgi_object}->param(-name => $field, -value => $values->[0])
        if $self->{cgi_object};
    } else {
      ### values in @{ $form->{$field} } were modified directly
      $self->{cgi_object}->param(-name => $field, -value => $values)
        if $self->{cgi_object};
    }
  }

  ### only continue if a validate_if is not present or passes test
  my $needs_val = 0;
  my $n_vif = 0;
  foreach my $type ($self->filter_type('validate_if',$types)) {
    $n_vif ++;
    my $ifs = $field_val->{$type};
    my $ret = $self->check_conditional($form, $ifs, $N_level, $ifs_match);
    $needs_val ++ if $ret;
  }
  if (! $needs_val && $n_vif) {
    delete $field_val->{'was_validated'};
    return wantarray ? @errors : $#errors + 1;
  }

  ### check for simple existence
  ### optionally check only if another condition is met
  my $is_required = '';
  foreach my $type ($self->filter_type('required',$types)) {
    next if ! $field_val->{$type};
    $is_required = $type;
    last;
  }
  if (! $is_required) {
    foreach my $type ($self->filter_type('required_if',$types)) {
      my $ifs = $field_val->{$type};
      next if ! $self->check_conditional($form, $ifs, $N_level, $ifs_match);
      $is_required = $type;
      last;
    }
  }
  if ($is_required && (! defined($form->{$field})
                       || ((UNIVERSAL::isa($form->{$field},'ARRAY') && $#{ $form->{$field} } == -1)
                           || ! length($form->{$field})))) {
    return 1 if ! wantarray;
    $self->add_error(\@errors, $field, $is_required, $field_val, $ifs_match);
    return @errors;
  }

  ### min values check
  foreach my $type ($self->filter_type('min_values',$types)) {
    my $n = $field_val->{$type} || 0;
    if ($n_values < $n) {
      return 1 if ! wantarray;
      $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
      return @errors;
    }
  }

  ### max values check
  my @keys = $self->filter_type('max_values',$types);
  if ($#keys == -1) {
    push @keys, 'max_values';
    $field_val->{'max_values'} = 1;
  }
  foreach my $type (@keys) {
    my $n = $field_val->{$type} || 0;
    if ($n_values > $n) {
      return 1 if ! wantarray;
      $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
      return @errors;
    }
  }

  ### max_in_set and min_in_set checks
  foreach my $minmax (qw(min max)) {
    my @keys = $self->filter_type("${minmax}_in_set",$types);
    foreach my $type (@keys) {
      $field_val->{$type} =~ m/^\s*(\d+)(?i:\s*of)?\s+(.+)\s*$/
        || die "Invalid in_set check $field_val->{$type}";
      my $n = $1;
      foreach my $_field (split /[\s,]+/, $2) {
        my $ref = UNIVERSAL::isa($form->{$_field},'ARRAY') ? $form->{$_field} : [$form->{$_field}];
        foreach my $_value (@$ref) {
          $n -- if defined($_value) && length($_value);
        }
      }
      if (   ($minmax eq 'min' && $n > 0)
          || ($minmax eq 'max' && $n < 0)) {
        return 1 if ! wantarray;
        $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
        return @errors;
      }
    }
  }

  ### at this point @errors should still be empty
  my $content_checked; # allow later for possible untainting (only happens if content was checked)

  ### loop on values of field
  foreach my $value (@$values) {

    ### allow for enum types
    foreach my $type ($self->filter_type('enum',$types)) {
      my $ref = ref($field_val->{$type}) ? $field_val->{$type} : [split(/\s*\|\|\s*/,$field_val->{$type})];
      my $found = 0;
      foreach (@$ref) {
        $found = 1 if defined($value) && $_ eq $value;
      }
      if (! $found) {
        return 1 if ! wantarray;
        $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
      }
      $content_checked = 1;
    }

    ### field equality test
    foreach my $type ($self->filter_type('equals',$types)) {
      my $field2  = $field_val->{$type};
      my $not     = ($field2 =~ s/^!\s*//) ? 1 : 0;
      my $success = 0;
      if ($field2 =~ m/^([\"\'])(.*)\1$/) {
        my $test = $2;
        $success = (defined($value) && $value eq $test);
      } elsif (exists($form->{$field2}) && defined($form->{$field2})) {
        $success = (defined($value) && $value eq $form->{$field2});
      } elsif (! defined($value)) {
        $success = 1; # occurs if they are both undefined
      }
      if ($not ? $success : ! $success) {
        return 1 if ! wantarray;
        $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
      }
      $content_checked = 1;
    }

    ### length min check
    foreach my $type ($self->filter_type('min_len',$types)) {
      my $n = $field_val->{$type};
      if (! defined($value) || length($value) < $n) {
        return 1 if ! wantarray;
        $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
      }
    }

    ### length max check
    foreach my $type ($self->filter_type('max_len',$types)) {
      my $n = $field_val->{$type};
      if (defined($value) && length($value) > $n) {
        return 1 if ! wantarray;
        $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
      }
    }

    ### now do match types
    foreach my $type ($self->filter_type('match',$types)) {
      my $ref = UNIVERSAL::isa($field_val->{$type},'ARRAY') ? $field_val->{$type}
         : UNIVERSAL::isa($field_val->{$type}, 'Regexp') ? [$field_val->{$type}]
         : [split(/\s*\|\|\s*/,$field_val->{$type})];
      foreach my $rx (@$ref) {
        if (UNIVERSAL::isa($rx,'Regexp')) {
          if (! defined($value) || $value !~ $rx) {
            $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
          }
        } else {
          if ($rx !~ m/^(!\s*|)m([^\s\w])(.*)\2([eigsmx]*)$/s) {
            die "Not sure how to parse that match ($rx)";
          }
          my ($not,$pat,$opt) = ($1,$3,$4);
          $opt =~ tr/g//d;
          die "The e option cannot be used on validation keys on field $field" if $opt =~ /e/;
          if ( (     $not && (  defined($value) && $value =~ m/(?$opt:$pat)/))
               || (! $not && (! defined($value) || $value !~ m/(?$opt:$pat)/))
               ) {
            return 1 if ! wantarray;
            $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
          }
        }
      }
      $content_checked = 1;
    }

    ### allow for comparison checks
    foreach my $type ($self->filter_type('compare',$types)) {
      my $ref = UNIVERSAL::isa($field_val->{$type},'ARRAY') ? $field_val->{$type}
        : [split(/\s*\|\|\s*/,$field_val->{$type})];
      foreach my $comp (@$ref) {
        next if ! $comp;
        my $test  = 0;
        if ($comp =~ /^\s*(>|<|[><!=]=)\s*([\d\.\-]+)\s*$/) {
          my $val = $value || 0;
          $val *= 1;
          if    ($1 eq '>' ) { $test = ($val >  $2) }
          elsif ($1 eq '<' ) { $test = ($val <  $2) }
          elsif ($1 eq '>=') { $test = ($val >= $2) }
          elsif ($1 eq '<=') { $test = ($val <= $2) }
          elsif ($1 eq '!=') { $test = ($val != $2) }
          elsif ($1 eq '==') { $test = ($val == $2) }

        } elsif ($comp =~ /^\s*(eq|ne|gt|ge|lt|le)\s+(.+?)\s*$/) {
          my $val = defined($value) ? $value : '';
          my ($op, $value2) = ($1, $2);
          $value2 =~ s/^([\"\'])(.*)\1$/$2/;
          if    ($op eq 'gt') { $test = ($val gt $value2) }
          elsif ($op eq 'lt') { $test = ($val lt $value2) }
          elsif ($op eq 'ge') { $test = ($val ge $value2) }
          elsif ($op eq 'le') { $test = ($val le $value2) }
          elsif ($op eq 'ne') { $test = ($val ne $value2) }
          elsif ($op eq 'eq') { $test = ($val eq $value2) }

        } else {
          die "Not sure how to compare \"$comp\"";
        }
        if (! $test) {
          return 1 if ! wantarray;
          $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
        }
      }
      $content_checked = 1;
    }

    ### server side sql type
    foreach my $type ($self->filter_type('sql',$types)) {
      my $db_type = $field_val->{"${type}_db_type"};
      my $dbh = ($db_type) ? $self->{dbhs}->{$db_type} : $self->{dbh};
      if (! $dbh) {
        die "Missing dbh for $type type on field $field" . ($db_type ? " and db_type $db_type" : "");
      } elsif (UNIVERSAL::isa($dbh,'CODE')) {
        $dbh = &$dbh($field, $self) || die "SQL Coderef did not return a dbh";
      }
      my $sql  = $field_val->{$type};
      my @args = ($value) x $sql =~ tr/?//;
      my $return = $dbh->selectrow_array($sql, {}, @args); # is this right - copied from O::FORMS
      $field_val->{"${type}_error_if"} = 1 if ! defined $field_val->{"${type}_error_if"};
      if ( (! $return && $field_val->{"${type}_error_if"})
           || ($return && ! $field_val->{"${type}_error_if"}) ) {
        return 1 if ! wantarray;
        $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
      }
      $content_checked = 1;
    }

    ### server side custom type
    foreach my $type ($self->filter_type('custom',$types)) {
      my $check = $field_val->{$type};
      next if UNIVERSAL::isa($check, 'CODE') ? &$check($field, $value, $field_val, $type) : $check;
      return 1 if ! wantarray;
      $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
      $content_checked = 1;
    }

    ### do specific type checks
    foreach my $type ($self->filter_type('type',$types)) {
      if (! $self->check_type($value,$field_val->{'type'},$field,$form)){
        return 1 if ! wantarray;
        $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
      }
      $content_checked = 1;
    }
  }

  ### allow for the data to be "untainted"
  ### this is only allowable if the user ran some other check for the datatype
  foreach my $type ($self->filter_type('untaint',$types)) {
    last if $#errors != -1;
    if (! $content_checked) {
      $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
    } else {
      ### generic untainter - assuming the other required content_checks did good validation
      $_ = /(.*)/ ? $1 : die "Couldn't match?" foreach @$values;
      if ($n_values == 1) {
        $form->{$field} = $values->[0];
        $self->{cgi_object}->param(-name => $field, -value => $values->[0])
          if $self->{cgi_object};
      } else {
        ### values in @{ $form->{$field} } were modified directly
        $self->{cgi_object}->param(-name => $field, -value => $values)
          if $self->{cgi_object};
      }
    }
  }

  ### all done - time to return
  return wantarray ? @errors : $#errors + 1;
}

### simple error adder abstraction
sub add_error {
  my $self = shift;
  my $errors = shift;
  push @$errors, \@_;
}

### allow for multiple validations in the same hash
### ie Match, Match1, Match2, Match234
sub filter_type {
  my $self  = shift;
  my $type  = shift;
  my $order = shift || die "Missing order array";
  my @array = ();
  foreach (@$order) {
    push @array, $_ if /^\Q$type\E_?\d*$/;
  }
  return wantarray ? @array : $#array + 1;
}

###----------------------------------------------------------------###

### used to validate specific types
sub check_type {
  my $self  = shift;
  my $value = shift;
  my $type  = uc(shift);

  ### do valid email address for our system
  if ($type eq 'EMAIL') {
    return 0 if ! $value;
    my($local_p,$dom) = ($value =~ /^(.+)\@(.+?)$/) ? ($1,$2) : return 0;

    return 0 if length($local_p) > 60;
    return 0 if length($dom) > 100;
    return 0 if ! $self->check_type($dom,'DOMAIN') && ! $self->check_type($dom,'IP');
    return 0 if ! $self->check_type($local_p,'LOCAL_PART');

  ### the "username" portion of an email address
  } elsif ($type eq 'LOCAL_PART') {
    return 0 if ! defined($value) || ! length($value);
    return 0 if $value =~ m/[^a-z0-9.\-\!\&]/;
    return 0 if $value =~ m/^[\.\-]/;
    return 0 if $value =~ m/[\.\-\&]$/;
    return 0 if $value =~ m/(\.\-|\-\.|\.\.)/;

  ### standard IP address
  } elsif ($type eq 'IP') {
    return 0 if ! $value;
    return (4 == grep {!/\D/ && $_ < 256} split /\./, $value, 4);

  ### domain name - including tld and subdomains (which are all domains)    
  } elsif ($type eq 'DOMAIN') {
    return 0 if ! $value;
    return 0 if $value =~ m/[^a-z0-9.\-]/;
    return 0 if $value =~ m/^[\.\-]/;
    return 0 if $value =~ m/(\.\-|\-\.|\.\.)/;
    return 0 if length($value) > 255;
    return 0 if $value !~ s/\.([a-z]+)$//;

    my $ext = $1;
    if ($ext eq 'name') { # .name domains
      return 0 if $value !~ /^[a-z0-9][a-z0-9\-]{0,62} \. [a-z0-9][a-z0-9\-]{0,62}$/x;
    } else {              # any other domains
      return 0 if $value !~ /^([a-z0-9][a-z0-9\-]{0,62} \.)* [a-z0-9][a-z0-9\-]{0,62}$/x;
    }

  ### validate a url
  } elsif ($type eq 'URL') {
    return 0 if ! $value;
    $value =~ s|^https?://([^/]+)||i || return 0;
    my $dom = $1;
    return 0 if ! $self->check_type($dom,'DOMAIN') && ! $self->check_type($dom,'IP');
    return 0 if $value && ! $self->check_type($value,'URI');

  ### validate a uri - the path portion of a request
  } elsif ($type eq 'URI') {
    return 0 if ! $value;
    return 0 if $value =~ m/\s+/;

  } elsif ($type eq 'CC') {
    return 0 if ! $value;
    ### validate the number
    return 0 if $value =~ /[^\d\-\ ]/
      || length($value) > 16
      || length($value) < 13;

    ### simple mod10 check
    $value =~ s/\D//g;
    my $sum    = 0;
    my $switch = 0;
    foreach my $digit ( reverse split //, $value ){
      $switch = 1 if ++ $switch > 2;
      my $y = $digit * $switch;
      $y -= 9 if $y > 9;
      $sum += $y;
    }
    return 0 if $sum % 10;

  }

  return 1;
}

###----------------------------------------------------------------###

sub get_validation {
  my $self = shift;
  my $val  = shift;
  return $self->conf->read($val, {html_key => 'validation'});
}

### returns all keys from all groups - even if group has validate_if
sub get_validation_keys {
  my $self     = shift;
  my $val_hash = shift;
  my $form     = shift; # with optional form - will only return keys in validated groups
  my %keys     = ();

  ### if a form was passed - make sure it is a hashref
  if ($form) {
    if (! ref($form)) {
      die "Invalid form hash or cgi object";
    } elsif(! UNIVERSAL::isa($form,'HASH')) {
      require CGI::Ex;
      $form = CGI::Ex->new->get_form($form);
    }
  }

  my $refs     = $self->get_validation($val_hash);
  $refs = [$refs] if ! UNIVERSAL::isa($refs,'ARRAY');
  foreach my $group_val (@$refs) {
    die "Group found that was not a hashref" if ! UNIVERSAL::isa($group_val, 'HASH');

    ### if form is passed, check to see if the group passed validation
    if ($form) {
      my $validate_if = $group_val->{'group validate_if'};
      next if $validate_if && ! $self->check_conditional($form, $validate_if);
    }

    if ($group_val->{"group fields"}) {
      die "Group fields must be an arrayref" if ! UNIVERSAL::isa($group_val->{"group fields"}, 'ARRAY');
      foreach my $field_val (@{ $group_val->{"group fields"} }) {
        next if ! ref($field_val) && $field_val eq 'OR';
        die "Field_val must be a hashref" if ! UNIVERSAL::isa($field_val, 'HASH');
        my $key = $field_val->{'field'} || die "Missing field key in field_val hashref";
        $keys{$key} = $field_val->{'name'} || 1;
      }
    } elsif ($group_val->{"group order"}) {
      die "Group order must be an arrayref" if ! UNIVERSAL::isa($group_val->{"group order"}, 'ARRAY');
      foreach my $key (@{ $group_val->{"group order"} }) {
        my $field_val = $group_val->{$key};
        next if ! $field_val && $key eq 'OR';
        die "Field_val for $key must be a hashref" if ! UNIVERSAL::isa($field_val, 'HASH');
        $key = $field_val->{'field'} if $field_val->{'field'};
        $keys{$key} = $field_val->{'name'} || 1;
      }
    }

    ### get all others
    foreach my $key (keys %$group_val) {
      next if $key =~ /^(general|group)\s/;
      my $field_val = $group_val->{$key};
      next if ! UNIVERSAL::isa($field_val, 'HASH');
      $keys{$key} = $field_val->{'name'} || 1;
    }
  }

  return \%keys;
}

###----------------------------------------------------------------###

### spit out a chunk that will do the validation
sub generate_js {
  ### allow for some browsers to not receive the validation
  if ($ENV{HTTP_USER_AGENT}) {
    foreach (@UNSUPPORTED_BROWSERS) {
      next if $ENV{HTTP_USER_AGENT} !~ $_;
      return "<!-- JS Validation not supported in this browser $_ -->"
    }
  }

  my $self        = shift;
  my $val_hash    = shift || die "Missing validation";
  my $form_name   = shift || die "Missing form name";
  my $js_uri_path = shift || $JS_URI_PATH;
  $val_hash = $self->get_validation($val_hash);
  require YAML;

  ### store any extra items from self
  my %EXTRA = ();
  foreach my $key (keys %$self) {
    next if $key !~ $QR_EXTRA;
    $EXTRA{"general $key"} = $self->{$key};
  }

  my $str = &YAML::Dump((scalar keys %EXTRA) ? (\%EXTRA) : () , $val_hash);
  $str =~ s/(?<!\\)\\(?=[sSdDwWbB0-9?.*+|\-\^\${}()\[\]])/\\\\/g;
  $str =~ s/\n/\\n\\\n/g; # allow for one big string
  $str =~ s/\"/\\\"/g; # quotify it

  ### get the paths
  my $js_uri_path_yaml = $JS_URI_PATH_YAML || do {
    die "Missing \$js_uri_path" if ! $js_uri_path;
    "$js_uri_path/CGI/Ex/yaml_load.js";
  };
  my $js_uri_path_validate = $JS_URI_PATH_VALIDATE || do {
    die "Missing \$js_uri_path" if ! $js_uri_path;
    "$js_uri_path/CGI/Ex/validate.js";
  };

  ### return the string
  return qq{<script src="$js_uri_path_yaml"></script>
<script src="$js_uri_path_validate"></script>
<script><!--
document.validation = "$str";
if (document.check_form) document.check_form("$form_name");
//--></script>
};

}

###----------------------------------------------------------------###
### How to handle errors

package CGI::Ex::Validate::Error;

use strict;
use overload '""' => \&as_string;

sub new {
  my $class  = shift || __PACKAGE__;
  my $errors = shift;
  my $extra  = shift || {};
  die "Missing or invalid arrayref" if ! UNIVERSAL::isa($errors, 'ARRAY');
  die "Missing or invalid hashref"  if ! UNIVERSAL::isa($extra,  'HASH');
  return bless {errors => $errors, extra => $extra}, $class;
}

sub as_string {
  my $self = shift;
  my $extra  = $self->{extra} || {};
  my $extra2 = shift || {};

  ### allow for formatting
  my $join = defined($extra2->{as_string_join}) ? $extra2->{as_string_join}
    : defined($extra->{as_string_join}) ? $extra->{as_string_join}
    : "\n";
  my $header = defined($extra2->{as_string_header}) ? $extra2->{as_string_header}
    : defined($extra->{as_string_header}) ? $extra->{as_string_header} : "";
  my $footer = defined($extra2->{as_string_footer}) ? $extra2->{as_string_footer}
    : defined($extra->{as_string_footer}) ? $extra->{as_string_footer} : "";

  return $header . join($join, @{ $self->as_array($extra2) }) . $footer;
}

### return an array of applicable errors
sub as_array {
  my $self = shift;
  my $errors = $self->{errors} || die "Missing errors";
  my $extra  = $self->{extra}  || {};
  my $extra2 = shift || {};

  my $title = defined($extra2->{as_array_title}) ? $extra2->{as_array_title}
    : defined($extra->{as_array_title}) ? $extra->{as_array_title}
    : "Please correct the following items:";

  ### if there are heading items then we may end up needing a prefix
  my $has_headings;
  if ($title) {
    $has_headings = 1;
  } else {
    foreach (@$errors) {
      next if ref;
      $has_headings = 1;
      last;
    }
  }

  my $prefix = defined($extra2->{as_array_prefix}) ? $extra2->{as_array_prefix}
    : defined($extra->{as_array_prefix}) ? $extra->{as_array_prefix}
    : $has_headings ? '  ' : '';

  ### get the array ready
  my @array = ();
  push @array, $title if length $title;

  ### add the errors
  my %found = ();
  foreach my $err (@$errors) {
    if (! ref $err) {
      push @array, $err;
      %found = ();
    } else {
      my $text = $self->get_error_text($err);
      next if $found{$text};
      $found{$text} = 1;
      push @array, "$prefix$text";
    }
  }

  return \@array;
}

### return a hash of applicable errors
sub as_hash {
  my $self = shift;
  my $errors = $self->{errors} || die "Missing errors";
  my $extra  = $self->{extra}  || {};
  my $extra2 = shift || {};

  my $suffix = defined($extra2->{as_hash_suffix}) ? $extra2->{as_hash_suffix}
    : defined($extra->{as_hash_suffix}) ? $extra->{as_hash_suffix} : '_error';
  my $join   = defined($extra2->{as_hash_join}) ? $extra2->{as_hash_join}
    : defined($extra->{as_hash_join}) ? $extra->{as_hash_join} : '<br />';

  ### now add to the hash
  my %found  = ();
  my %return = ();
  foreach my $err (@$errors) {
    next if ! ref $err;

    my ($field, $type, $field_val, $ifs_match) = @$err;
    die "Missing field name" if ! $field;
    if ($field_val->{delegate_error}) {
      $field = $field_val->{delegate_error};
      $field =~ s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg if $ifs_match;
    }

    my $text = $self->get_error_text($err);
    next if $found{$field}->{$text};
    $found{$field}->{$text} = 1;

    $field .= $suffix;
    $return{$field} ||= [];
    $return{$field} = [$return{$field}] if ! ref($return{$field});
    push @{ $return{$field} }, $text;
  }

  ### allow for elements returned as
  if ($join) {
    my $header = defined($extra2->{as_hash_header}) ? $extra2->{as_hash_header}
      : defined($extra->{as_hash_header}) ? $extra->{as_hash_header} : "";
    my $footer = defined($extra2->{as_hash_footer}) ? $extra2->{as_hash_footer}
      : defined($extra->{as_hash_footer}) ? $extra->{as_hash_footer} : "";
    foreach my $key (keys %return) {
      $return{$key} = $header . join($join,@{ $return{$key} }) . $footer;
    }
  }

  return \%return;
}

### return a user friendly error message
sub get_error_text {
  my $self  = shift;
  my $err   = shift;
  my $extra = $self->{extra} || {};
  my ($field, $type, $field_val, $ifs_match) = @$err;
  my $dig     = ($type =~ s/(_?\d+)$//) ? $1 : '';
  my $type_lc = lc($type);

  ### allow for delegated field names - only used for defaults
  if ($field_val->{delegate_error}) {
    $field = $field_val->{delegate_error};
    $field =~ s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg if $ifs_match;
  }

  ### the the name of this thing
  my $name = $field_val->{'name'} || "The field $field";
  $name =~ s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg if $ifs_match;

  ### type can look like "required" or "required2" or "required100023"
  ### allow for fallback from required100023_error through required_error
  my @possible_error_keys = ("${type}_error");
  unshift @possible_error_keys, "${type}${dig}_error" if length($dig);

  ### look in the passed hash or self first
  my $return;
  foreach my $key (@possible_error_keys){
    $return = $field_val->{$key} || $extra->{$key} || next;
    $return =~ s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg if $ifs_match;
    $return =~ s/\$field/$field/g;
    $return =~ s/\$name/$name/g;
    if (my $value = $field_val->{"$type$dig"}) {
      $return =~ s/\$value/$value/g if ! ref $value;
    }
    last;
  }

  ### set default messages
  if (! $return) {
    if ($type eq 'required' || $type eq 'required_if') {
      $return = "$name is required.";

    } elsif ($type eq 'min_values') {
      my $n = $field_val->{"min_values${dig}"};
      my $values = ($n == 1) ? 'value' : 'values';
      $return = "$name had less than $n $values.";

    } elsif ($type eq 'max_values') {
      my $n = $field_val->{"max_values${dig}"};
      my $values = ($n == 1) ? 'value' : 'values';
      $return = "$name had more than $n $values.";

    } elsif ($type eq 'enum') {
      $return = "$name is not in the given list.";

    } elsif ($type eq 'equals') {
      my $field2 = $field_val->{"equals${dig}"};
      my $name2  = $field_val->{"equals${dig}_name"} || "the field $field2";
      $name2 =~ s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg if $ifs_match;
      $return = "$name did not equal $name2.";

    } elsif ($type eq 'min_len') {
      my $n = $field_val->{"min_len${dig}"};
      my $char = ($n == 1) ? 'character' : 'characters';
      $return = "$name was less than $n $char.";

    } elsif ($type eq 'max_len') {
      my $n = $field_val->{"max_len${dig}"};
      my $char = ($n == 1) ? 'character' : 'characters';
      $return = "$name was more than $n $char.";

    } elsif ($type eq 'max_in_set') {
      my $set = $field_val->{"max_in_set${dig}"};
      $return = "Too many fields were chosen from the set ($set)";

    } elsif ($type eq 'min_in_set') {
      my $set = $field_val->{"min_in_set${dig}"};
      $return = "Not enough fields were chosen from the set ($set)";

    } elsif ($type eq 'match') {
      $return = "$name contains invalid characters.";

    } elsif ($type eq 'compare') {
      $return = "$name did not fit comparison.";

    } elsif ($type eq 'sql') {
      $return = "$name did not match sql test.";

    } elsif ($type eq 'custom') {
      $return = "$name did not match custom test.";

    } elsif ($type eq 'type') {
      my $_type = $field_val->{"type${dig}"};
      $return = "$name did not match type $_type.";

    } elsif ($type eq 'untaint') {
      $return = "$name cannot be untainted without one of the following checks: enum, equals, match, compare, sql, type, custom";

    } elsif ($type eq 'no_extra_fields') {
      $return = "$name should not be passed to validate.";
    }
  }

  die "Missing error on field $field for type $type$dig" if ! $return;
  return $return;

}

###----------------------------------------------------------------###

1;


__END__

=head1 NAME

CGI::Ex::Validate - Yet another form validator - does good javascript too

$Id: Validate.pm,v 1.79 2005/02/23 21:28:11 pauls Exp $

=head1 SYNOPSIS

  use CGI::Ex::Validate;

  ### THE SHORT

  my $errobj = CGI::Ex::Validate->new->validate($form, $val_hash);

  ### THE LONG

  my $form = CGI->new;
   # OR #
  my $form = CGI::Ex->new; # OR CGI::Ex->get_form;
   # OR #
  my $form = {key1 => 'val1', key2 => 'val2'};


  ### simplest
  my $val_hash = {
    username => {required => 1,
                 max_len  => 30
                 field    => 'username',
                 # field is optional in this case - will use key name
                },
    email    => {required => 1,
                 max_len  => 100
                },
    email2   => {validate_if => 'email'
                 equals      => 'email'
                },
  };

  ### ordered
  my $val_hash = {
    'group order' => [qw(username email email2)],
    username => {required => 1, max_len => 30},
    email    => ...,
    email2   => ...,
  };

  ### ordered again
  my $val_hash = {
    'group fields' => [
      {field    => 'username', # field is not optional in this case
       required => 1,
       max_len  => 30,
      },
      {field    => 'email',
       required => 1,
       max_len  => 100,
      }
      {field       => 'email2',
       validate_if => 'email',
       equals      => 'email',
      }
    ],
  };

  
  my $vob    = CGI::Ex::Validate->new;
  my $errobj = $vob->validate($form, $val_hash);
    # OR #
  my $errobj = $vob->validate($form, "/somefile/somewhere.val"); # import config using yaml file
    # OR #
  my $errobj = $vob->validate($form, "/somefile/somewhere.pl");  # import config using perl file
    # OR #
  my $errobj = $vob->validate($form, "--- # a yaml document\n"); # import config using yaml str


  if ($errobj) {
    my $error_heading = $errobj->as_string; # OR "$errobj";
    my $error_list    = $errobj->as_array;  # ordered list of what when wrong
    my $error_hash    = $errobj->as_hash;   # hash of arrayrefs of errors
  } else {
    # form passed validation
  }

  ### will add an error for any form key not found in $val_hash
  my $vob = CGI::Ex::Validate->new({no_extra_keys => 1});
  my $errobj = $vob->validate($form, $val_hash);  

=head1 DESCRIPTION

CGI::Ex::Validate is yet another module used for validating input.  It
aims to have all of the power of former modules, while advancing them
with more flexibility, external validation files, and identical
javascript validation.  CGI::Ex::Validate can work in a simple way
like all of the other validators do.  However, it also allows for
grouping of validation items and conditional validaion of groups or
individual items.  This is more in line with the normal validation
procedures for a website.

=head1 METHODS

=over 4

=item C<new>

Used to instantiate the object.  Arguments are either a hash, or hashref,
or nothing at all.  Keys of the hash become the keys of the object.

=item C<get_validation>

Given a filename or YAML string will return perl hash.  If more than one
group is contained in the file, it will return an arrayref of hashrefs.

  my $ref = $self->get_validation($file);

=item C<get_validation_keys>

Given a filename or YAML string or a validation hashref, will return all
of the possible keys found in the validation hash.  This can be used to
check to see if extra items have been passed to validate.  If a second
argument contains a form hash is passed, get_validation_keys will only
return the keys of groups that were validated.

  my $key_hashref = $self->get_validation_keys($val_hash);

The values of the hash are the names of the fields.

=item C<validate>

Arguments are a form hashref or cgi object, a validation hashref or filename, and
an optional what_was_validated arrayref.
If a CGI object is passed, CGI::Ex::get_form will be called on that object
to turn it into a hashref.  If a filename is given for the validation, get_validation
will be called on that filename.  If the what_was_validated_arrayref is passed - it
will be populated (pushed) with the field hashes that were actually validated (anything
that was skipped because of validate_if will not be in the array).

If the form passes validation, validate will return undef.  If it fails validation, it
will return a CGI::Ex::Validate::Error object.  If the 'raise_error' general option
has been set, validate will die with a CGI::Ex::validate::Error object as the value.

  my $err_obj = $self->validate($form, $val_hash);

    # OR #

  $self->{raise_error} = 1; # raise error can also be listed in the val_hash
  eval { $self->validate($form, $val_hash) };
  if ($@) {
    my $err_obj = $@;
  }

=item C<generate_js>

Requires YAML to work properly (see L<YAML>).

Takes a validation hash, a form name, and an optional javascript uri
path and returns Javascript that can be embedded on a page and will
perform identical validations as the server side.  The validation can
be any validation hash (or arrayref of hashes.  The form name must be
the name of the form that the validation will act upon - the name is
used to register an onsubmit function.  The javascript uri path is
used to embed the locations two external javascript source files.


The javascript uri path is highly dependent upon the server
implementation and therefore must be configured manually.  It may be
passed to generate_js, or it may be specified in $JS_URI_PATH.  There
are two files included with this module that are needed -
CGI/Ex/yaml_load.js and CGI/Ex/validate.js.  When generating the js
code, generate_js will look in $JS_URI_PATH_YAML and
$JS_URI_PATH_VALIDATE.  If either of these are not set, generate_js
will default to "$JS_URI_PATH/CGI/Ex/yaml_load.js" and
"$JS_URI_PATH/CGI/Ex/validate.js".

  $self->generate_js($val_hash, 'my_form', "/cgi-bin/js")
  # would generate something like the following...
  # <script src="/cgi-bin/js/CGI/Ex/yaml_load.js"></script>
  # <script src="/cgi-bin/js/CGI/Ex/validate.js"></script>
  # ... more js follows ...

  $CGI::Ex::Validate::JS_URI_PATH      = "/stock/js";
  $CGI::Ex::Validate::JS_URI_PATH_YAML = "/js/yaml_load.js";
  $self->generate_js($val_hash, 'my_form')  
  # would generate something like the following...
  # <script src="/js/yaml_load.js"></script>
  # <script src="/stock/js/CGI/Ex/validate.js"></script>
  # ... more js follows ...

Referencing yaml_load.js and validate.js can be done in any of
several ways.  They can be copied to or symlinked to a fixed location
in the servers html directory.  They can also be printed out by a cgi.
The method C<-E<gt>print_js> has been provided in CGI::Ex for printing
js files found in the perl heirchy.  See L<CGI::Ex> for more details.
The $JS_URI_PATH of "/cgi-bin/js" could contain the following:

  #!/usr/bin/perl -w

  use strict;
  use CGI::Ex;

  ### path_info should contain something like /CGI/Ex/yaml_load.js
  my $info = $ENV{PATH_INFO} || '';
  die "Invalid path" if $info !~ m|^(/\w+)+.js$|;
  $info =~ s|^/+||;

  CGI::Ex->new->print_js($info);
  exit;

The print_js method in CGI::Ex is designed to cache the javascript in
the browser (caching is suggested as they are medium sized files).

=item C<-E<gt>cgix>

Returns a CGI::Ex object.  Used internally.

=item C<-E<gt>conf>

Returns a CGI::Ex::Conf object.  Used internally.

=back

=head1 VALIDATION HASH

The validation hash may be passed as a perl a hashref or 
as a filename, or as a YAML document string.  If it is a filename,
it will be translated into a hash using the %EXT_HANDLER for the
extension on the file.  If there is no extension, it will use $DEFAULT_EXT
as a default.

The validation hash may also be an arrayref of hashrefs.  In this
case, each arrayref is treated as a group and is validated separately.

=head1 GROUPS

Each hashref that is passed as a validation hash is treated as a
group.  Keys matching the regex m/^group\s+(\w+)$/ are reserved and
are counted as GROUP OPTIONS.  Keys matching the regex m/^general\s+(\w+)$/
are reserved and are counted as GENERAL OPTIONS.  Other keys (if
any, should be keys that need validation).

If the GROUP OPTION 'group validate_if' is set, the group will only
be validated if the conditions are met.  Any group with out a validate_if
fill be automatically validated.

Each of the items listed in the group will be validated.  The
validation order is determined in one of three ways:

=over 4

=item Specify 'group fields' arrayref.

  # order will be (username, password, 'm/\w+_foo/', somethingelse)
  {
    'group title' => "User Information",
    'group fields' => [
      {field => 'username',   required => 1},
      {field => 'password',   required => 1},
      {field => 'm/\w+_foo/', required => 1},
    ],
    somethingelse => {required => 1},
  }

=item Specify 'group order' arrayref.

  # order will be (username, password, 'm/\w+_foo/', somethingelse)
  {
    'group title' => "User Information",
    'group order' => [qw(username password), 'm/\w+_foo/'],
    username      => {required => 1},
    password      => {required => 1},
    'm/\w+_foo/'  => {required => 1},
    somethingelse => {required => 1},
  }

=item Do nothing - use sorted order.

  # order will be ('m/\w+_foo/', password, somethingelse, username)
  {
    'group title' => "User Information",
    username      => {required => 1},
    password      => {required => 1},
    'm/\w+_foo/'  => {required => 1},
    somethingelse => {required => 1},
  }

=back

Each of the individual field validation hashrefs should contain
the types listed in VALIDATION TYPES.

Optionally the 'group fields' or the 'group order' may contain the word
'OR' as a special keyword.  If the item preceding 'OR' fails validation
the item after 'OR' will be tested instead.  If the item preceding 'OR'
passes validation the item after 'OR' will not be tested.

  'group order' => [qw(zip OR postalcode state OR region)],

Each individual validation hashref will operate on the field contained
in the 'field' key.  This key may also be a regular expression in the
form of 'm/somepattern/'.  If a regular expression is used, all keys
matching that pattern will be validated.

=head1 VALIDATION TYPES

The following are the available validation types.  Multiple instances of
the same type may be used by adding a number to the type (ie match, match2,
match232, match_94).  Multiple instances are validated in sorted order.

=over 4

=item C<validate_if>

If validate_if is specified, the field will only be validated
if the conditions are met.  Works in JS.

  validate_if => {field => 'name', required => 1, max_len => 30}
  # Will only validate if the field "name" is present and is less than 30 chars.

  validate_if => 'name',
  # SAME as
  validate_if => {field => 'name', required => 1},

  validate_if => '! name',
  # SAME as
  validate_if => {field => 'name', max_in_set => '0 of name'},

  validate_if => {field => 'country', compare => "eq US"},
  # only if country's value is equal to US

  validate_if => {field => 'country', compare => "ne US"},
  # if country doesn't equal US

  validate_if => {field => 'password', match => 'm/^md5\([a-z0-9]{20}\)$/'},
  # if password looks like md5(12345678901234567890)

  {
    field       => 'm/^(\w+)_pass/',
    validate_if => '$1_user',
    required    => 1,
  }
  # will validate foo_pass only if foo_user was present.

The validate_if may also contain an arrayref of validation items.  So that
multiple checks can be run.  They will be run in order.  validate_if will
return true only if all options returned true.

  validate_if => ['email', 'phone', 'fax']

Optionally, if validate_if is an arrayref, it may contain the word
'OR' as a special keyword.  If the item preceding 'OR' fails validation
the item after 'OR' will be tested instead.  If the item preceding 'OR'
passes validation the item after 'OR' will not be tested.

  validate_if => [qw(zip OR postalcode)],

=item C<required_if>

Requires the form field if the condition is satisfied.  The conditions
available are the same as for validate_if.  This is somewhat the same
as saying:

  validate_if => 'some_condition',
  required    => 1

  required_if => 'some_condition',

  {
    field       => 'm/^(\w+)_pass/',
    required_if => '$1_user',
  }
  
=item C<required>

Requires the form field to have some value.  If the field is not present,
no other checks will be run.

=item C<min_values> and C<max_values>

Allows for specifying the maximum number of form elements passed.
max_values defaults to 1 (You must explicitly set it higher
to allow more than one item by any given name).

=item C<min_in_set> and C<max_in_set>

Somewhat like min_values and max_values except that you specify the
fields that participate in the count.  Also - entries that are not
defined or do not have length are not counted.  An optional "of" can
be placed after the number for human readibility.

  min_in_set => "2 of foo bar baz",
    # two of the fields foo, bar or baz must be set
    # same as
  min_in_set => "2 foo bar baz",
    # same as 
  min_in_set => "2 OF foo bar baz",

  validate_if => {field => 'whatever', max_in_set => '0 of whatever'},
    # only run validation if there were zero occurances of whatever

=item C<enum>

Allows for checking whether an item matches a set of options.  In perl
the value may be passed as an arrayref.  In the conf or in perl the
value may be passed of the options joined with ||.

  {
    field => 'password_type',
    enum  => 'plaintext||crypt||md5', # OR enum => [qw(plaintext crypt md5)],
  }

=item C<equals>

Allows for comparison of two form elements.  Can have an optional !.

  {
    field  => 'password',
    equals => 'password_verify',
  },
  {
    field  => 'domain1',
    equals => '!domain2', # make sure the fields are not the same
  }

=item C<min_len and max_len>

Allows for check on the length of fields

  {
    field   => 'site',
    min_len => 4,
    max_len => 100,
  }

=item C<match>

Allows for regular expression comparison.  Multiple matches may
be concatenated with ||.  Available in JS.

  {
    field   => 'my_ip',
    match   => 'm/^\d{1,3}(\.\d{1,3})3$/',
    match_2 => '!/^0\./ || !/^192\./',
  }

=item C<compare>

Allows for custom comparisons.  Available types are
>, <, >=, <=, !=, ==, gt, lt, ge, le, ne, and eq.  Comparisons
also work in the JS.

  {
    field    => 'my_number',
    match    => 'm/^\d+$/',
    compare1 => '> 100',
    compare2 => '< 255',
    compare3 => '!= 150',
  }

=item C<sql>

SQL query based - not available in JS.  The database handle will be looked
for in the value $self->{dbhs}->{foo} if sql_db_type is set to 'foo',
otherwise it will default to $self->{dbh}.  If $self->{dbhs}->{foo} or
$self->{dbh} is a coderef - they will be called and should return a dbh.

  {
    field => 'username',
    sql   => 'SELECT COUNT(*) FROM users WHERE username = ?',
    sql_error_if => 1, # default is 1 - set to 0 to negate result
    # sql_db_type  => 'foo', # will look for a dbh under $self->{dbhs}->{foo}
  }

=item C<custom>

Custom value - not available in JS.  Allows for extra programming types.
May be either a boolean value predetermined before calling validate, or may be
a coderef that will be called during validation.  If coderef is called, it will
be passed the field name, the form value for that name, and a reference to the
field validation hash.  If the custom type returns false the element fails
validation and an error is added.

  {
    field => 'username',
    custom => sub {
      my ($key, $val, $type, $field_val_hash) = @_;
      # do something here
      return 0;
    },
  }

=item C<custom_js>

Custom value - only available in JS.  Allows for extra programming types.
May be either a boolean value predermined before calling validate, or may be
section of javascript that will be eval'ed.  The last value (return value) of
the eval'ed javascript will determine if validation passed.  A false value indicates
the value did not pass validation.  A true value indicates that it did.  See
the t/samples/js_validate_3.html page for a sample of usage.

  {
    field => 'date',
    required => 1,
    match    => 'm|^\d\d\d\d/\d\d/\d\d$|',
    match_error => 'Please enter date in YYYY/MM/DD format',
    custom_js => "
      var t=new Date();
      var y=t.getYear()+1900;
      var m=t.getMonth() + 1;
      var d=t.getDate();
      if (m<10) m = '0'+m;
      if (d<10) d = '0'+d;
      (value > ''+y+'/'+m+'/'+d) ? 1 : 0;
    ",
    custom_js_error => 'The date was not greater than today.',
  }

=item C<type>
 
Allows for more strict type checking.  Many types will be added and
will be available from javascript as well.  Currently support types
are CC.

  {
    field => 'credit_card',
    type  => 'CC',
  }

=back

=head1 SPECIAL VALIDATION TYPES

=over 4

=item C<field>

Specify which field to work on.  Key may be a regex in the form 'm/\w+_user/'.
This key is required if 'group fields' is used or if validate_if or required_if
are used.  It can optionally be used with other types to specify a different form
element to operate on.  On errors, if a non-default error is found, $field
will be swapped with the value found in field.

The field name may also be a regular expression in the
form of 'm/somepattern/'.  If a regular expression is used, all keys
matching that pattern will be validated.

=item C<name>

Name to use for errors.  If a name is not specified, default errors will use
"The field $field" as the name.  If a non-default error is found, $name
will be swapped with this name.

=item C<delegate_error>

This option allows for any errors generated on a field to delegate to
a different field.  If the field name was a regex, any patterns will
be swapped into the delegate_error value. This option is generally only
useful with the as_hash method of the error object (for inline errors).

  {
    field => 'zip',
    match => 'm/^\d{5}/',
  },
  {
    field => 'zip_plus4',
    match => 'm/^\d{4}/',
    delegate_error => 'zip',
  },

  {
    field => 'm/^(id_[\d+])_user$/',
    delegate_error => '$1',
  },

=item C<exclude_js>

This allows the cgi to do checking while keeping the checks from
being run in JavaScript

  {
    field      => 'cgi_var',
    required   => 1,
    exclude_js => 1,
  }

=item C<exclude_cgi>

This allows the js to do checking while keeping the checks from
being run in the cgi

  {
    field       => 'js_var',
    required    => 1,
    exclude_cgi => 1,
  }

=back

=head1 MODIFYING VALIDATION TYPES

=over 4

=item C<do_not_trim>

By default, validate will trim leading and trailing whitespace
from submitted values.  Set do_not_trim to 1 to allow it to
not trim.

  {field => 'foo', do_not_trim => 1}

=item C<replace>

Pass a swap pattern to change the actual value of the form.
Any perl regex can be passed.

  {field => 'foo', replace => 's/(\d{3})(\d{3})(\d{3})/($1) $2-$3/'}

=item C<default>

Set item to default value if there is no existing value (undefined
or zero length string).  Maybe someday well add default_if (but that
would require some odd syntax for both the conditional and the default).

  {field => 'country', default => 'EN'}

=item C<to_upper_case> and C<to_lower_case>

Do what they say they do.

=item C<untaint>

Requires that the validated field has been also checked with
an enum, equals, match, compare, custom, or type check.  If the
field has been checked and there are no errors - the field is "untainted."

This is for use in conjunction with the -T switch.

=back

=head1 ERROR OBJECT

Failed validation results in an error object blessed into the class found in
$ERROR_PACKAGE - which defaults to CGI::Ex::Validate::Error.

The error object has several methods for determining what the errors were.

=over 4

=item C<as_array>

Returns an array or arrayref (depending on scalar context) of errors that
occurred in the order that they occured.  Individual groups may have a heading
and the entire validation will have a heading (the default heading can be changed
via the 'as_array_title' general option).  Each error that occured is a separate
item and are prepended with 'as_array_prefix' (which is a general option - default
is '  ').  The as_array_ options may also be set via a hashref passed to as_array.
as_array_title defaults to 'Please correct the following items:'.

  ### if this returns the following
  my $array = $err_obj->as_array;
  # $array looks like
  # ['Please correct the following items:', '  error1', '  error2']

  ### then this would return the following
  my $array = $err_obj->as_array({
    as_array_prefix => '  - ',
    as_array_title  => 'Something went wrong:',
  });
  # $array looks like
  # ['Something went wrong:', '  - error1', '  - error2']

=item C<as_string>

Returns values of as_array joined with a newline.  This method is used as
the stringification for the error object.  Values of as_array are joined with
'as_string_join' which defaults to "\n".  If 'as_string_header' is set, it will
be prepended onto the error string.  If 'as_string_footer' is set, it will be
postpended onto the error string.

  ### if this returns the following
  my $string = $err_obj->as_string;
  # $string looks like
  # "Please correct the following items:\n  error1\n  error2"

  ### then this would return the following
  my $string = $err_obj->as_string({
    as_array_prefix  => '  - ',
    as_array_title   => 'Something went wrong:',
    as_string_join   => '<br />',
    as_string_header => '<span class="error">',
    as_string_footer => '</span>',
  });
  # $string looks like
  # '<span class="error">Something went wrong:<br />  - error1<br />  - error2</span>'

=item C<as_hash>

Returns a hash or hashref (depending on scalar context) of errors that
occurred.   Each key is the field name of the form that failed validation with
'as_hash_suffix' added on as a suffix.  as_hash_suffix is available as a general option
and may also be passed in via a hashref as the only argument to as_hash.
The default value is '_error'.  The values of the hash are arrayrefs of errors
that occured to that form element.

By default as_hash will return the values of the hash as arrayrefs (a list of the errors
that occured to that key).  It is possible to also return the values as strings.
Three options are available for formatting: 'as_hash_header' which will be prepended
onto the error string, 'as_hash_footer' which will be postpended, and 'as_hash_join' which
will be used to join the arrayref.  The only argument required to force the
stringification is 'as_hash_join'.

  ### if this returns the following
  my $hash = $err_obj->as_hash;
  # $hash looks like
  # {key1_error => ['error1', 'error2']}

  ### then this would return the following
  my $hash = $err_obj->as_hash({
    as_hash_suffix => '_foo',
    as_hash_join   => '<br />',
    as_hash_header => '<span class="error">'
    as_hash_footer => '</span>'
  });
  # $hash looks like
  # {key1_foo => '<span class="error">error1<br />error2</span>'}

=back

=head1 GROUP OPTIONS

Any key in a validation hash matching the pattern m/^group\s+(\w+)$/
is considered a group option.  The current know options are:

=over 4

=item C<'group title'>

Used as a group section heading when as_array or as_string is called
by the error object.

=item C<'group order'>

Order in which to validate key/value pairs of group.

=item C<'group fields'>

Arrayref of validation items to validate.

=item C<'group validate_if'>

Conditions that will be checked to see if the group should be validated.
If no validate_if option is found, the group will be validated.

=back

=head1 GENERAL OPTIONS

Any key in a validation hash matching the pattern m/^general\s+(\w+)$/
is considered a general option.  General options will also be looked
for in the Validate object ($self) and can be set when instantiating
the object ($self->{raise_error} is equivalent to
$valhash->{'general raise_error'}).  The current know options are:

General options may be set in any group using the syntax:

  'general general_option_name' => 'general_option_value'

They will only be set if the group's validate_if is successful or
if the group does not have a validate_if.  It is also possible to set
a "group general" option using the following syntax:

  'group general_option_name' => 'general_option_value'

These items will only be set if the group fails validation.
If a group has a validate_if block and passes validation, the group
items will not be used.  This is so that a failed section can have
its own settings.  Note though that the last option found will be
used and that items set in $self override those set in the validation
hash.

Options may also be set globally before calling validate by
populating the %DEFAULT_OPTIONS global hash.

=over 4

=item C<'general raise_error'>

If raise_error is true, any call to validate that fails validation
will die with an error object as the value.

=item C<'general no_extra_fields'>

If no_extra_fields is true, validate will add errors for any field found
in form that does not have a field_val hashref in the validation hash.
Default is false.  If no_extra_fields is set to 'used', it will check for
any keys that were not in a group that was validated.

An important exception to this is that field_val hashrefs or field names listed
in a validate_if or required_if statement will not be included.  You must
have an explicit entry for each key.

=item C<'general \w+_error'>

These items allow for an override of the default errors.

  'general required_error' => '$name is really required',
  'general max_len_error'  => '$name must be shorter than $value characters',
    # OR #
  my $self = CGI::Ex::Validate->new({
    max_len_error => '$name must be shorter than $value characters',
  });

=item C<'general as_array_title'>

Used as the section title for all errors that occur, when as_array
or as_string is called by the error object.

=item C<'general as_array_prefix'>

Used as prefix to individual errors that occur, when as_array
or as_string is called by the error object.  Each individual error
will be prefixed with this string.  Headings will not be prefixed.
Default is '  '.

=item C<'general as_string_join'>

When as_string is called, the values from as_array will be joined with
as_string_join.  Default value is "\n".

=item C<'general as_string_header'>

If set, will be prepended onto the string when as_string is called.

=item C<'general as_string_footer'>

If set, will be prepended onto the string when as_string is called.

=item C<'general as_hash_suffix'>

Added on to key names during the call to as_hash.  Default is '_error'.

=item C<'general as_hash_join'>

By default, as_hash will return hashref values that are errors joined with
the default as_hash_join value of <br />.  It can also return values that are
arrayrefs of the errors.  This can be done by setting as_hash_join to a non-true value
(for example '')

=item C<'general as_hash_header'>

If as_hash_join has been set to a true value, as_hash_header may be set to
a string that will be prepended on to the error string.

=item C<'general as_hash_footer'>

If as_hash_join has been set to a true value, as_hash_footer may be set to
a string that will be postpended on to the error string.

=item C<'general no_inline'>

If set to true, the javascript validation will not attempt to generate inline
errors.  Default is true.  Inline errors are independent of confirm and alert
errors.

=item C<'general no_confirm'>

If set to true, the javascript validation will try to use an alert instead
of a confirm to inform the user of errors.  Alert and confirm are independent
or inline errors.  Default is false.

=item C<'general no_alert'>

If set to true, the javascript validation will not show an alert box
when errors occur.  Default is false.  This option only comes into
play if no_confirm is also set.  This option is independent of inline
errors.  Although it is possible to turn off all errors by setting
no_inline, no_confirm, and no_alert all to 1, it is suggested that at
least one of the error reporting facilities is left on.

=back

It is possible to have a group that contains nothing but general options.

  my $val_hash = [
    {'general error_title'    => 'The following things went wrong',
     'general error_prefix'   => '  - ',
     'general raise_error'    => 1,
     'general name_suffix'    => '_foo_error',
     'general required_error' => '$name is required',
    },
    {'group title' => 'User Information',
     username => {required => 1},
     email    => {required => 1},
     password => {required => 1},
    },
  ];

=head1 JAVASCRIPT

CGI::Ex::Validate provides for having duplicate validation on the
client side as on the server side.  Errors can be shown in any
combination of inline and confirm, inline and alert, inline only,
confirm only, alert only, and none.  These combinations are controlled
by the general options no_inline, no_confirm, and no_alert.
Javascript validation can be generated for a page using the
C<-E<gt>generate_js> Method of CGI::Ex::Validate.  It is also possible
to store the validation inline with the html.  This can be done by
giving each of the elements to be validated an attribute called
"validation", or by setting a global javascript variable called
"document.validation" or "var validation".  An html file containing this
validation will be read in using CGI::Ex::Conf::read_handler_html.

All inline html validation must be written in yaml.

It is anticipated that the html will contain something like either of the
following examples:

  <script src="/cgi-bin/js/CGI/Ex/yaml_load.js"></script>
  <script src="/cgi-bin/js/CGI/Ex/validate.js"></script>
  <script>
  // \n\ allows all browsers to view this as a single string
  document.validation = "\n\
  general no_confirm: 1\n\
  general no_alert: 1\n\
  group order: [username, password]\n\
  username:\n\
    required: 1\n\
    max_len: 20\n\
  password:\n\
    required: 1\n\
    max_len: 30\n\
  ";
  if (document.check_form) document.check_form('my_form_name');
  </script>

Alternately we can use element attributes:

  <form name="my_form_name">

  Username: <input type=text size=20 name=username validation="
    required: 1
    max_len: 20
  "><br>
  <span class=error id=username_error>[% username_error %]</span><br>

  Password: <input type=text size=20 name=password validation="
    required: 1
    max_len: 30
  "><br>
  <span class=error id=password_error>[% password_error %]</span><br>

  <input type=submit>

  </form>

  <script src="/cgi-bin/js/CGI/Ex/yaml_load.js"></script>
  <script src="/cgi-bin/js/CGI/Ex/validate.js"></script>
  <script>
  if (document.check_form) document.check_form('my_form_name');
  </script>

The read_handler_html from CGI::Ex::Conf will find either of these
types of validation.

If inline errors are asked for, each error that occurs will attempt
to find an html element with its name as the id.  For example, if
the field "username" failed validation and created a "username_error",
the javascript would set the html of <span id="username_error"></span>
to the error message.

It is suggested to use something like the following so that you can
have inline javascript validation as well as report validation errors
from the server side as well.

   <span class=error id=password_error>[% password_error %]</span><br>

If the javascript fails for some reason, the form should still be able
to submit as normal (fail gracefully).

If the confirm option is used, the errors will be displayed to the user.
If they choose OK they will be able to try and fix the errors.  If they
choose cancel, the form will submit anyway and will rely on the server
to do the validation.  This is for fail safety to make sure that if the
javascript didn't validate correctly, the user can still submit the data.

=head1 AUTHOR

Paul Seamons

=head1 LICENSE

This module may be distributed under the same terms as Perl itself.

=cut


