package Validate::Form; use strict; use warnings; =item Validate::Form->new(I) Creates a new Form object with defined I. Fields is a HASH with keys of input names and values of Validate::Form::Fields. Returns a newly created Form object. =cut sub new { my ($class, $fields) = @_; my $self = { _fields => $fields, }; # Make $self an object of $class return bless($self, $class); } =item Validate::Form->validate(I) Validates a HASH of user input data against the form's fields. Inputs is a HASH with keys of input names and values of input data. Returns 0 if input fails validation, otherwise returns 1. Any errors will be available from the form's public errors() method. If the input passed validation, the validated and filtered input data is available from the form's public data() meethod. =cut sub validate { my ($self, $inputs) = @_; # empty data and errors before validating $self->_reset(); # make sure input data is a HASH if (not defined $inputs or ref($inputs) ne 'HASH') { $self->add_error('INVALID_INPUT', 'Invalid input data'); return 0; } # store validated data in $data while validating my $data = {}; # loop through each field on this form for my $key (keys %{$self->fields}) { # setup state for validating this field's input $self->{_current_key} = $key; $self->{_have_errors} = undef; # this field with validators my $field = $self->fields->{$key}; # this field's input my $input = $inputs->{$key}; # filter input before validating if (defined $field->{filters}) { for my $filter (@{$field->{filters}}) { $input = $filter->($input); } } # if this field is optional and field is missing, don't validate it if (defined $field->{optional} and $field->{optional} and not exists $inputs->{$key}) { next; } # if this field is nullable and input is null, then # set this field to null and don't validate it if (defined $field->{nullable} and $field->{nullable} and not defined $input) { $data->{$key} = undef; next; } # loop through this field's validators. the Validate::Form::Fields validator # will always be the first validator to execute for my $validator (@{$field->{validators}}) { # run this validator on the field's input # validators will call $self->add_error() if they fail validation &$validator($self, $key, $input); # don't run this field's other validators if we failed this validator last if defined $self->{_have_errors}; } # save this field's input data if the input passed all validators. # the data will now be available with the public data() method $data->{$key} = $input unless defined $self->{_have_errors}; } # validating finished, so save input data if there were no errors # return 0 if encountered validation errors, return 1 if no errors if (int(@{$self->errors}) == 0) { $self->{_data} = $data; return 1; } else { return 0; } } =item Validate::Form->fields() Returns a form's Fields. =cut sub fields { my ($self) = @_; return $self->{_fields}; } =item Validate::Form->data() Returns a form's validated and filtered input data after calling validate(). By default, will return an empty HASH unless the input data passed all validators. =cut sub data { my ($self) = @_; $self->{_data} = {} unless defined $self->{_data}; return $self->{_data}; } =item Validate::Form->errors() Returns a form's errors after calling the validate() method, if there were errors. This can be passed directly to a JSON encode method to return input validation errors. =cut sub errors { my ($self) = @_; $self->{_errors} = [] unless defined $self->{_errors}; return $self->{_errors}; } =item Validate::Form->add_error(I, I) Called by validators when input fails validation. I is a constant error code string representing the specific validation error. I is a human-readable string describing the validation error. =cut sub add_error { my ($self, $code, $message) = @_; die 'missing error code' unless defined $code and $code; die 'missing error message' unless defined $message and $message; $self->_add_error({ type => 'content_validation', name => $self->{_current_key}, code => $code, message => $message, }); } sub _add_error { my ($self, $error) = @_; $self->{_errors} = [] unless defined $self->{_errors}; push(@{$self->{_errors}}, $error); $self->{_have_errors} = 1; } sub _reset { my ($self) = @_; $self->{_data} = {}; $self->{_errors} = []; } 1; package Validate::Form::Fields; use strict; use warnings; use JSON; =item Validate::Form::Fields::_combine(defaults, original) Adds a Field's type validator and optional filter to the beginning of the original validators and original filters. =cut sub _combine { my ($default, $options) = @_; $options->{filters} = [] unless defined $options->{filters}; unshift(@{$options->{filters}}, $default->{filter}) if defined $default->{filter}; $options->{validators} = [] unless defined $options->{validators}; unshift(@{$options->{validators}}, $default->{validator}) if defined $default->{validator}; return $options; } =item Validate::Form::Fields::Boolean(I, I) Defines a Boolean field. Options can contain: validators => I filters => I nullable => I optional => I I is an optional human-readable error string available from the public errors() method if the input fails validation. If you omit I, a useful default will be provided. A field's validators and filters are always run before user defined validators and filters. =cut sub Boolean { my ($options) = @_; return _combine({ validator => sub { my ($form, $key, $data) = @_; if (not defined $data or ref(\$data) ne 'SCALAR' or $data !~ /^0|1$/) { if (defined $data and not JSON::is_bool($data)) { $form->add_error('INVALID_BOOLEAN', 'Invalid Boolean'); } } }, filter => sub { my ($data) = @_; return $data unless (defined $data and ref(\$data) eq 'SCALAR'); $data = '0' if $data =~ /^false$/i; $data = '1' if $data =~ /^true$/i; return $data; }, }, $options); } =item Validate::Form::Fields::Integer(I, I) Defines an Integer field. Options can contain: validators => I filters => I nullable => I optional => I I is an optional human-readable error string available from the public errors() method if the input fails validation. If you omit I, a useful default will be provided. A field's validators and filters are always run before user defined validators and filters. =cut sub Integer { my ($options) = @_; return _combine({ validator => sub { my ($form, $key, $data) = @_; if (not defined $data or ref(\$data) ne 'SCALAR' or $data !~ /^-?\d+$/) { $form->add_error('INVALID_INTEGER', 'Invalid Integer'); } }, }, $options); } =item Validate::Form::Fields::Float(I, I) Defines a Float field. Options can contain: validators => I filters => I nullable => I optional => I I is an optional human-readable error string available from the public errors() method if the input fails validation. If you omit I, a useful default will be provided. A field's validators and filters are always run before user defined validators and filters. =cut sub Float { my ($options) = @_; return _combine({ validator => sub { my ($form, $key, $data) = @_; if (not defined $data or ref(\$data) ne 'SCALAR' or $data !~ /^-?((\d+)?\.)?\d+$/) { $form->add_error('INVALID_NUMBER', 'Invalid Number'); } }, }, $options); } =item Validate::Form::Fields::String(I, I) Defines a String field. Options can contain: validators => I filters => I nullable => I optional => I I is an optional human-readable error string available from the public errors() method if the input fails validation. If you omit I, a useful default will be provided. A field's validators and filters are always run before user defined validators and filters. =cut sub String { my ($options) = @_; return _combine({ validator => sub { my ($form, $key, $data) = @_; unless (defined $data and ref(\$data) eq 'SCALAR') { $form->add_error('INVALID_STRING', 'Invalid String'); } }, }, $options); } =item Validate::Form::Fields::List(I, I) Defines a List field. Options can contain: form=> I validators => I filters => I min_items => I max_items => I nullable => I optional => I I is an optional human-readable error string available from the public errors() method if the input fails validation. If you omit I, a useful default will be provided. The form object will be used to validate each element of the input array. =cut sub List { my ($options) = @_; die 'Missing form' unless defined $options->{form}; return _combine({ validator => sub { my ($form, $key, $data) = @_; unless (defined $data and ref($data) eq 'ARRAY') { $form->add_error('INVALID_ARRAY', 'Invalid Array'); return; } if (defined $options->{min_items} and int(@$data) < $options->{min_items}) { $form->add_error('MIN_ITEMS', 'At least '. $options->{min_items} .' required.'); return; } if (defined $options->{max_items} and int(@$data) > $options->{max_items}) { $form->add_error('MAX_ITEMS', 'No more than '. $options->{min_items} .' allowed.'); return; } my $count = 0; for my $item (@$data) { if (not $options->{form}->validate($item)) { $form->_add_error({ index => $count, errors => $options->{form}->errors(), }); } $count++; } }, }, $options); } 1; package Validate::Form::Validators; use strict; use warnings; =item Validate::Form::Validators::DataRequired() Built-in validator to require input data exists, is defined, and is not an empty string. Applies to any field. =cut sub DataRequired { my ($value, $params) = @_; $params = {} unless defined $params; return sub { my ($form, $key, $data) = @_; if (not defined $data or $data eq '') { $form->add_error('INPUT_REQUIRED', defined $params->{message} ? $params->{message} : 'This field is required.'); } }; } =item Validate::Form::Validators::MinLength(I) Built-in validator to check a string's minimum length. I is the minimum required length of the input string. Applies to String fields. =cut sub MinLength { my ($value, $params) = @_; $params = {} unless defined $params; return sub { my ($form, $key, $data) = @_; if (not defined $data or length($data) < $value) { $form->add_error('INPUT_MIN_LENGTH', defined $params->{message} ? $params->{message} : 'Must be at least '.$value.' characters long.'); } }; } =item Validate::Form::Validators::MaxLength(I) Built-in validator to check a string's maximum length. I is the maximum allowed length of the input string. Applies to String fields. =cut sub MaxLength { my ($value, $params) = @_; $params = {} unless defined $params; return sub { my ($form, $key, $data) = @_; if (defined $data and length($data) > $value) { $form->add_error('INPUT_MAX_LENGTH', defined $params->{message} ? $params->{message} : 'Cannot be longer than '.$value.' characters.'); } }; } =item Validate::Form::Validators::Min(I) Built-in validator to check a number's minimum value. I is the minimum input value allowed. Applies to Integer and Float fields. =cut sub Min { my ($value, $params) = @_; $params = {} unless defined $params; return sub { my ($form, $key, $data) = @_; if (defined $data and $data < $value) { $form->add_error('INPUT_MIN_VALUE', defined $params->{message} ? $params->{message} : 'Must be greater than '.$value.'.'); } }; } =item Validate::Form::Validators::Max(I) Built-in validator to check a number's maximum value. I is the maximum input value allowed. Applies to Integer and Float fields. =cut sub Max { my ($value, $params) = @_; $params = {} unless defined $params; return sub { my ($form, $key, $data) = @_; if (defined $data and $data > $value) { $form->add_error('INPUT_MAX_VALUE', defined $params->{message} ? $params->{message} : 'Must be less than '.$value.'.'); } }; } 1; __END__ =head1 NAME Validate::Form - validate user input =head1 SYNOPSIS use Validate::Form; # Define user input. This would usually be JSON or Form encoded data. my $user_input = { first_name => 'Alan', last_name => 'Hamlett', age => 21, email => 'alan.hamlett@whitehatsec.com', }; # Define the form. Pretend we are using an SQL table with columns: # first_name TEXT NOT NULL # last_name TEXT NOT NULL # age INTEGER # email TEXT NOT NULL my $form = Validate::Form->new({ first_name => Validate::Form::Fields::String({ validators=>[ Validate::Form::Validators::DataRequired(), Validate::Form::Validators::MinLength(3), Validate::Form::Validators::MaxLength(50), ], }), last_name => Validate::Form::Fields::String({ validators=>[ Validate::Form::Validators::DataRequired(), Validate::Form::Validators::MinLength(3), Validate::Form::Validators::MaxLength(50), ], }), age => Validate::Form::Fields::Integer({ optional=>1, validators=>[ Validate::Form::Validators::Min(13), Validate::Form::Validators::Max(99, {message=>'You must be less than 100 years old.'}), ], }), email => Validate::Form::Fields::String({ validators=>[ Validate::Form::Validators::Required(), sub { my ($form, $key, $data) = @_; use Email::Valid; if (not Email::Valid->address($data)) { $form->add_error('INVALID_EMAIL', 'Not a valid email address.'); } no Email::Valid; }, ], }), }); # Validate user input against the form if ($form->validate($user_input)) { # user input is valid, so create a new user. Always use # the validated input from $form->data() instead of the # original $user_input. } else { # Failed validation, so return the errors return $form->errors(); } =head1 METHODS new(\%validators) validate(\%userinput) errors() data() add_error($code, $message) =head1 AUTHOR 2012, Alan Hamlett