CGI::Ex::Validate - Yet another form validator - does good javascript too
$Id: pod_cgi_ex_validate.html,v 1.1 2005/03/09 23:30:49 pauls Exp $
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);
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.
new
get_validation
my $ref = $self->get_validation($file);
get_validation_keys
my $key_hashref = $self->get_validation_keys($val_hash);
The values of the hash are the names of the fields.
validate
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 = $@; }
generate_js
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 ->print_js
has been provided in CGI::Ex for printing
js files found in the perl heirchy. See the CGI::Ex manpage 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).
->cgix
->conf
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.
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:
# 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}, }
# 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}, }
# 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}, }
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.
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.
validate_if
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)],
required_if
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.
min_values
and max_values
min_in_set
and max_in_set
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
enum
{ field => 'password_type', enum => 'plaintext||crypt||md5', # OR enum => [qw(plaintext crypt md5)], }
equals
{ field => 'password', equals => 'password_verify', }, { field => 'domain1', equals => '!domain2', # make sure the fields are not the same }
min_len and max_len
{ field => 'site', min_len => 4, max_len => 100, }
match
{ field => 'my_ip', match => 'm/^\d{1,3}(\.\d{1,3})3$/', match_2 => '!/^0\./ || !/^192\./', }
compare
{ field => 'my_number', match => 'm/^\d+$/', compare1 => '> 100', compare2 => '< 255', compare3 => '!= 150', }
sql
{ 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} }
custom
{ field => 'username', custom => sub { my ($key, $val, $type, $field_val_hash) = @_; # do something here return 0; }, }
custom_js
{ 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.', }
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', }
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.
name
delegate_error
{ field => 'zip', match => 'm/^\d{5}/', }, { field => 'zip_plus4', match => 'm/^\d{4}/', delegate_error => 'zip', },
{ field => 'm/^(id_[\d+])_user$/', delegate_error => '$1', },
exclude_js
{ field => 'cgi_var', required => 1, exclude_js => 1, }
exclude_cgi
{ field => 'js_var', required => 1, exclude_cgi => 1, }
do_not_trim
{field => 'foo', do_not_trim => 1}
replace
{field => 'foo', replace => 's/(\d{3})(\d{3})(\d{3})/($1) $2-$3/'}
default
{field => 'country', default => 'EN'}
to_upper_case
and to_lower_case
untaint
This is for use in conjunction with the -T switch.
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.
as_array
### 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']
as_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>'
as_hash
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>'}
Any key in a validation hash matching the pattern m/^group\s+(\w+)$/ is considered a group option. The current know options are:
'group title'
'group order'
'group fields'
'group validate_if'
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.
'general raise_error'
'general no_extra_fields'
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.
'general \w+_error'
'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', });
'general as_array_title'
'general as_array_prefix'
'general as_string_join'
'general as_string_header'
'general as_string_footer'
'general as_hash_suffix'
'general as_hash_join'
'general as_hash_header'
'general as_hash_footer'
'general no_inline'
'general no_confirm'
'general no_alert'
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}, }, ];
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
->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.
Paul Seamons
This module may be distributed under the same terms as Perl itself.