From 6ab8b2e8e8388d1a238148a1ee58e124855f3768 Mon Sep 17 00:00:00 2001 From: Paul Seamons Date: Thu, 9 Feb 2012 00:00:00 +0000 Subject: [PATCH] CGI::Ex 2.37 --- Changes | 30 +++++ MANIFEST | 1 + META.yml | 30 +++-- lib/CGI/Ex.pm | 6 +- lib/CGI/Ex/App.pm | 18 ++- lib/CGI/Ex/App.pod | 24 +++- lib/CGI/Ex/App/Constants.pm | 175 ++++++++++++++++++++++++++++ lib/CGI/Ex/Auth.pm | 30 +++-- lib/CGI/Ex/Conf.pm | 4 +- lib/CGI/Ex/Die.pm | 4 +- lib/CGI/Ex/Dump.pm | 11 +- lib/CGI/Ex/Fill.pm | 4 +- lib/CGI/Ex/JSONDump.pm | 4 +- lib/CGI/Ex/Template.pm | 2 +- lib/CGI/Ex/Validate.pm | 56 ++++++--- lib/CGI/Ex/Validate.pod | 168 +++++++++++++++++++++----- lib/CGI/Ex/validate.js | 99 ++++++++++------ samples/validate_js_0_tests.html | 53 +++++++-- samples/validate_js_2_onchange.html | 40 +++---- t/1_validate_05_types.t | 30 ++++- t/4_app_00_base.t | 22 +++- 21 files changed, 647 insertions(+), 164 deletions(-) create mode 100644 lib/CGI/Ex/App/Constants.pm diff --git a/Changes b/Changes index 708f5ff..08cff5c 100644 --- a/Changes +++ b/Changes @@ -1,3 +1,33 @@ +2.37 + 2012-02-09 + * (Validate) Make match_2 work in the javascript layer (as shown in perldoc) + * (Validate) Add int, uint, and num types with range checking on int and uint + * (Validate) Allow for custom => sub { my ($k, $v) = @_; die "Always fail\n" } to pass the result of the failure via the value of die. + * (Validate) Allow for custom_js:function (args) { throw "Always fail ("+args.value+")" } which passes the error message of the test as the fail message + * (Validate) Allow for type, enum, and equals to short circuit (don't run match, compare, length, or custom checks if type fails) + * (App) Fix the test suite to not require installation of CGI + * (App) Add CGI::Ex::App::Constants module supporting constants as a source of documentation + * (App) Allow for access to constants via use CGI::Ex::App qw(:App); or use MyApp qw(:App); + +2.36 + 2010-06-10 + * (Auth) Make delete_cookie always delete, even for session Apps. + +2.35 + 2010-05-24 + * (Auth) More robust cookie reading and writing and deleting. + * Workaround CGI::Cookie->parse weird empty cookie implementation. + * (Auth) Allow cookie_domain to be modified more gracefully. + * (Auth) Consider case with multiple cookies for the same name. + +2.34 + 2010-04-23 + * (Auth) Don't attempted to decode base64 armor unless use_base64 + +2.33 + 2010-04-01 + * (Auth) Allow for custom form user vs valid cookie check + 2.32 2010-02-25 * (Validate) Allow for default to be an arrayref diff --git a/MANIFEST b/MANIFEST index 430c6df..61e40eb 100644 --- a/MANIFEST +++ b/MANIFEST @@ -1,6 +1,7 @@ Changes lib/CGI/Ex.pm lib/CGI/Ex/App.pm +lib/CGI/Ex/App/Constants.pm lib/CGI/Ex/App.pod lib/CGI/Ex/Auth.pm lib/CGI/Ex/Conf.pm diff --git a/META.yml b/META.yml index 7216ca2..a1f8bb5 100644 --- a/META.yml +++ b/META.yml @@ -1,14 +1,22 @@ --- #YAML:1.0 -name: CGI-Ex -version: 2.32 -abstract: CGI utility suite - makes powerful application writing fun and easy -license: ~ -author: +name: CGI-Ex +version: 2.37 +abstract: CGI utility suite - makes powerful application writing fun and easy +author: - Paul Seamons -generated_by: ExtUtils::MakeMaker version 6.42 -distribution_type: module -requires: - Template::Alloy: 1.004 +license: unknown +distribution_type: module +configure_requires: + ExtUtils::MakeMaker: 0 +build_requires: + ExtUtils::MakeMaker: 0 +requires: + Template::Alloy: 1.004 +no_index: + directory: + - t + - inc +generated_by: ExtUtils::MakeMaker version 6.56 meta-spec: - url: http://module-build.sourceforge.net/META-spec-v1.3.html - version: 1.3 + url: http://module-build.sourceforge.net/META-spec-v1.4.html + version: 1.4 diff --git a/lib/CGI/Ex.pm b/lib/CGI/Ex.pm index fa8d1fe..b18f5ee 100644 --- a/lib/CGI/Ex.pm +++ b/lib/CGI/Ex.pm @@ -7,7 +7,7 @@ CGI::Ex - CGI utility suite - makes powerful application writing fun and easy =cut ###----------------------------------------------------------------### -# Copyright 2007 - Paul Seamons # +# Copyright 2003-2012 - Paul Seamons # # Distributed under the Perl Artistic License without warranty # ###----------------------------------------------------------------### @@ -24,7 +24,7 @@ use vars qw($VERSION use base qw(Exporter); BEGIN { - $VERSION = '2.32'; + $VERSION = '2.37'; $PREFERRED_CGI_MODULE ||= 'CGI'; @EXPORT = (); @EXPORT_OK = qw(get_form @@ -195,7 +195,7 @@ sub get_cookies { my %hash = (); foreach my $key ($obj->cookie) { my @val = $obj->cookie($key); - $hash{$key} = ($#val == -1) ? next : ($#val == 0) ? $val[0] : \@val; + $hash{$key} = ($#val == -1) ? "" : ($#val == 0) ? $val[0] : \@val; } return $self->{'cookies'} = \%hash; } diff --git a/lib/CGI/Ex/App.pm b/lib/CGI/Ex/App.pm index 79720fc..78a9d47 100644 --- a/lib/CGI/Ex/App.pm +++ b/lib/CGI/Ex/App.pm @@ -2,7 +2,7 @@ package CGI::Ex::App; ###---------------------### # See the perldoc in CGI/Ex/App.pod -# Copyright 2008 - Paul Seamons +# Copyright 2004-2012 - Paul Seamons # Distributed under the Perl Artistic License without warranty use strict; @@ -11,7 +11,7 @@ BEGIN { eval { use Time::HiRes qw(time) }; eval { use Scalar::Util }; } -our $VERSION = '2.32'; +our $VERSION = '2.37'; sub new { my $class = shift || croak "Usage: ".__PACKAGE__."->new"; @@ -33,6 +33,15 @@ sub init_from_conf { return; } +sub import { # only ever called with explicit use CGI::Ex::App qw() - not with use base + my $class = shift; + if (@_ = grep { /^:?App($|__)/ } @_) { + require CGI::Ex::App::Constants; + unshift @_, 'CGI::Ex::App::Constants'; + goto &CGI::Ex::App::Constants::import; + } +} + ###---------------------### sub navigate { @@ -926,15 +935,16 @@ sub js_run_step { # step that allows for printing javascript libraries that are return 1; } +sub __forbidden_require_auth { 0 } sub __forbidden_allow_morph { shift->allow_morph(@_) && 1 } sub __forbidden_info_complete { 0 } # step that will be used the path method determines it is forbidden sub __forbidden_hash_common { shift->stash } -sub __forbidden_file_print { \ "

Denied

You do not have access to the step \"[% forbidden_step %]\"" } +sub __forbidden_file_print { \ "

Denied

You do not have access to the step \"[% forbidden_step.html %]\"" } sub __error_allow_morph { shift->allow_morph(@_) && 1 } sub __error_info_complete { 0 } # step that is used by the default handle_error sub __error_hash_common { shift->stash } -sub __error_file_print { \ "

A fatal error occurred

Step: \"[% error_step %]\"
[% TRY; CONFIG DUMP => {header => 0}; DUMP error; END %]" } +sub __error_file_print { \ "

A fatal error occurred

Step: \"[% error_step.html %]\"
[% TRY; CONFIG DUMP => {header => 0}; DUMP error; END %]" } sub __login_require_auth { 0 } sub __login_allow_morph { shift->allow_morph(@_) && 1 } diff --git a/lib/CGI/Ex/App.pod b/lib/CGI/Ex/App.pod index 536741b..e9f0888 100644 --- a/lib/CGI/Ex/App.pod +++ b/lib/CGI/Ex/App.pod @@ -20,7 +20,7 @@ A basic example: return \ "Hello World!"; } -Well, you should put your content in an external file... +Properly put content in an external file... -------- File: /cgi-bin/my_cgi -------- @@ -38,7 +38,7 @@ Well, you should put your content in an external file... Hello World! -How about if we want to add substitutions... +Adding substitutions... -------- File: /cgi-bin/my_cgi -------- @@ -65,7 +65,7 @@ How about if we want to add substitutions... [% greeting %] World! ([% date %]) -How about a form with validation (inluding javascript validation)... +Add forms and validation (inluding javascript validation)... -------- File: /cgi-bin/my_cgi -------- @@ -976,6 +976,24 @@ possible to move some of those methods into an external package. See the discussions under the methods named "find_hook" and "run_hook" for more details. +Some hooks expect "magic" values to be replaced. Often they are +intuitive, but sometimes it is easy to forget. For example, the +finalize hook should return true (default) to indicate the step is +complete and false to indicate that it failed and the page should be +redisplayed. You can import a set of constants that allows for human +readible names. + + use CGI::Ex::App qw(:App__finalize); + OR + use MyAppPkg qw(:App__finalize); # if it is a subclass of CGI::Ex::App + +This would import the following constants: +App__finalize__failed_and_show_page (0), +App__finalize__finished_and_move_to_next_step => (1 - default), and +App__finalize__finished_but_show_page ("" - still false). These +constants are provided by CGI::Ex::App::Constants which also contains +more options for usage. + The following is the alphabetical list of methods and hooks. =over 4 diff --git a/lib/CGI/Ex/App/Constants.pm b/lib/CGI/Ex/App/Constants.pm new file mode 100644 index 0000000..a7b6f56 --- /dev/null +++ b/lib/CGI/Ex/App/Constants.pm @@ -0,0 +1,175 @@ +package CGI::Ex::App::Constants; + +=head1 NAME + +CGI::Ex::App::Constants - Easier access to magic App values + +=cut + +use vars qw(%constants @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); +use strict; +use warnings; +use Exporter qw(import); # allow for goto from CGI::Ex::App +use base qw(Exporter); + +$VERSION = '2.37'; + +BEGIN { +my $all = { + App__allow_morph__allow_reblessing => '1 - This will allow changing MyApp to MyApp::MyStep when on step my_step', + App__allow_morph__force_reblessing => '2 - This will force changing MyApp to MyApp::MyStep when on step my_step', + App__allow_morph__no_auto_reblessing => '0 - We will not look for another package to bless into', + App__auth_args => 'should return a hashref of args to pass to auth_obj', + App__auth_obj => 'should return a auth_obj - used when require_auth is true', + App__file_print => 'should file path, or be a scalar reference to the template to print', + App__file_val => 'should return a file path or a hash_validation hashref (default is {})', + App__fill_args => 'should return a hashref of args to pass to CGI::Ex::Fill::fill (default {})', + App__fill_template => 'void context - uses hashes to fill the template', + App__finalize__failed_and_show_page => '0 - additional processing failed so show the page', + App__finalize__finished_and_move_to_next_step => '1 - default', + App__finalize__finished_but_show_page => '', + App__form_name => 'must return a name', + App__generate_form => 'return coderef to form that can generate the form based on hash_validation', + App__get_valid_auth => 'needs to return a CGI::Ex::Auth::Data object (which can be either true or false)', + App__hash_base => 'should return a hashref', + App__hash_common => 'should return a hashref', + App__hash_errors => 'should return a hashref of errors that occurred on the page (normally populated via add_error)', + App__hash_fill => 'should return a hashref of things to get filled in forms in the template', + App__hash_form => 'should return a hashref - default is $self->form - normally not overridden', + App__hash_swap => 'should return a hashref of things to process in the template', + App__hash_validation => 'should return a CGI::Ex::Validate compatible hashref or {} (default empty hashref means all submitted information is always ok to finalize)', + App__info_complete__fail_and_show_page => '0 - we were not ready to finalize the data either because there was not any, or it failed validation', + App__info_complete__succeed_and_run_finalize => '1 - occurs because data is ready and is good or because there was no hash_validation to test against', + App__js_validation => 'return coderef to form that will generate javascript based validation based on hash_validation', + App__morph => 'void context - used to rebless into other package', + App__morph_package => 'return package name derivative for use when allow_morph is true (see perldoc on morph, morph_package, morph_base)', + App__name_module => 'return name of application - used when mapping to file_system (see file_print, file_val, conf_file)', + App__name_step => 'return step of current request (default is $current_step) - used for mapping to file_system', + App__path_info_map => 'return arrayref of matching arrayrefs - first one matching is used - others abort (only applies to current step)', + App__path_info_map_base => 'return arrayref of matching arrayrefs - first one matching is used - others abort (ran before nav_loop)', + App__post_loop__do_not_recurse => '1 - can be used to abort navigation if the loop is about to recurse to default step - no additional headers will be sent', + App__post_loop__recurse_loop => 0, + App__post_navigate => 'void context - called at the end of navigation unless $self->{_no_post_navigate} is true', + App__post_print => 'void context - run code after page is diplayed to user', + App__post_step__abort_navigation_flow => '1 - no additional headers will be sent', + App__post_step__move_to_next_step => 0, + App__pre_loop__begin_loop => 0, + App__pre_loop__do_not_loop => '1 - can be used to abort the nav_loop call early on - no additional headers will be sent', + App__pre_navigate__continue => '0 - go ahead and navigate the request', + App__pre_navigate__stop => '1 - can be used to abort the navigate call early on - no additional headers will be sent', + App__pre_step__abort_navigation_flow => '1 - no additional headers will be sent', + App__pre_step__continue_current_step => 0, + App__prepare__fail_and_show_page => 0, + App__prepare__ok_move_to_info_complete => '1 - default', + App__prepared_print => 'void context - gathers hashes - then calls print', + App__print => 'void context - uses hashes to swap and fill file_print, then calls print_out', + App__print_out => 'void context - prints headers and prepared content', + App__ready_validate__data_not_ready_show_page => '0 - either validate_when_data was alse and it was a GET', + App__ready_validate__ready_to_validate_data => '1 - either validate_when_data was true or we received a POST', + App__refine_path => 'void context - manipulates the path after a step. set_ready_validate(0) if next_step is true', + App__require_auth__needs_authentication => 1, + App__require_auth__no_authentication_needed => 0, + App__run_step__move_to_next_step => 0, + App__run_step__request_completed => '1 - no additional headers will be sent', + App__set_ready_validate => 'void context - sets ready_validate to true (fakes REQUEST_METHOD to POST OR GET)', + App__skip__continue_current_step => 0, + App__skip__move_to_next_step => '1 - make sure the path has a next step or it will default to main', + App__swap_template => 'should return swapped template (passed $step, $file, $swap)', + App__template_args => 'should return a hashref to pass to template_obj', + App__template_obj => 'should return a Template::Alloy type object for swapping the template (passed template_args)', + App__unmorph => 'void context - re-reblesses back to the original class before morph', + App__val_args => 'should return a hashref to pass to val_obj', + App__val_obj => 'should return a CGI::Ex::Validate type object for validating the data', + App__validate__data_was_ok => '1 - request data either passed hash_validation or hash_validation was empty - make info_complete succeed', + App__validate__failed_validation => 0, + App__validate_when_data__succeed_if_data => '1 - will be true if there is no hash_validation, or a key from hash_validation was in the form', + App__validate_when_data__use_ready_validate => 0, +}; + +no strict 'refs'; +while (my ($method, $val) = each %$all) { + my ($prefix, $tag, $name) = split /__/, $method; + if (! $name) { + $constants{$tag} = $val; + next; + } + $constants{$tag}->{$name} = $val; + + my $tags = $EXPORT_TAGS{"App__${tag}"} ||= []; + push @{ $EXPORT_TAGS{"App"} }, $method; + push @$tags, $method; + push @EXPORT, $method; + push @EXPORT_OK, $method; + + $val =~ s/\s+-.*//; + $val *= 1 if $val =~ /^\d+$/; + *{__PACKAGE__."::$method"} = sub () { $val }; +} + +}; # end of BEGIN + +sub constants { + print __PACKAGE__."\n---------------------\n"; + no strict 'refs'; + for (sort @EXPORT_OK) { + print "$_ (".$_->().")\n"; + } +} + +sub tags { + print __PACKAGE__." Tags\n---------------------\n"; + print "$_\n" for sort keys %EXPORT_TAGS; +} + +sub details { + require Data::Dumper; + local $Data::Dumper::SortKeys = 1; + print Data::Dumper::Dumper(\%constants); +} + +1; + +__END__ + +=head1 SYNOPSIS + + use base qw(CGI::Ex::App); + use CGI::Ex::App::Constants; # load all + use CGI::Ex::App::Constants qw(:App); # also load all + use CGI::Ex::App qw(:App); # also load all + + __PACKAGE__->navigate; + + sub main_run_step { + my $self = shift; + + $self->cgix->print_content_type; + print "Hello world\n"; + + return App__run_step__request_completed; + } + + + # you can request only certain tags + use CGI::Ex::App::Constants qw(:App__run_step); + use CGI::Ex::App qw(:App__run_step); + + # you can request only certain constants + use CGI::Ex::App::Constants qw(App__run_step__request_completed); + use CGI::Ex::App qw(App__run_step__request_completed); + +=head1 CONSTANTS + +To see a list of the importable tags type: + + perl -MCGI::Ex::App::Constants -e 'CGI::Ex::App::Constants::tags()' + +To see a list of the importable constants type: + + perl -MCGI::Ex::App::Constants -e 'CGI::Ex::App::Constants::constants()' + +To see a little more discussion about the hooks and other CGI::Ex::App options type: + + perl -MCGI::Ex::App::Constants -e 'CGI::Ex::App::Constants::details()' + +=cut diff --git a/lib/CGI/Ex/Auth.pm b/lib/CGI/Ex/Auth.pm index 149123e..21b82d4 100644 --- a/lib/CGI/Ex/Auth.pm +++ b/lib/CGI/Ex/Auth.pm @@ -7,7 +7,7 @@ CGI::Ex::Auth - Handle logins nicely. =cut ###----------------------------------------------------------------### -# Copyright 2007 - Paul Seamons # +# Copyright 2004-2012 - Paul Seamons # # Distributed under the Perl Artistic License without warranty # ###----------------------------------------------------------------### @@ -19,7 +19,7 @@ use Digest::MD5 qw(md5_hex); use CGI::Ex; use Carp qw(croak); -$VERSION = '2.32'; +$VERSION = '2.37'; ###----------------------------------------------------------------### @@ -98,7 +98,7 @@ sub get_valid_auth { $data = $self->verify_token({token => $cookie, from => 'cookie'}); if (defined $form_user) { # they had form data my $user = $self->cleanup_user($form_user); - if (! $data || $user ne $data->{'user'}) { # but the cookie didn't match + if (! $data || !$self->check_form_user_against_cookie($user, $data->{'user'}, $data)) { # but the cookie didn't match $data = $self->{'_last_auth_data'} = $form_data; # restore old form data failure $data->{'user'} = $user if ! defined $data->{'user'}; } @@ -183,7 +183,7 @@ sub handle_failure { # make sure the cookie is gone my $key_c = $self->key_cookie; - $self->delete_cookie({name => $key_c}) if $self->cookies->{$key_c}; + $self->delete_cookie({name => $key_c}) if exists $self->cookies->{$key_c}; # no valid login and we are checking for cookies - see if they have cookies if (my $value = delete $form->{$self->key_verify}) { @@ -257,7 +257,17 @@ sub delete_cookie { my $args = shift; return $self->{'delete_cookie'}->($self, $args) if $self->{'delete_cookie'}; local $args->{'value'} = ''; - local $args->{'expires'} = '-10y' if ! $self->use_session_cookie($args->{'name'}, ''); + local $args->{'expires'} = '-10y'; + if (my $dom = $ENV{HTTP_HOST}) { + $dom =~ s/:\d+$//; + do { + local $args->{'domain'} = $dom; + $self->set_cookie($args); + local $args->{'domain'} = ".$dom"; + $self->set_cookie($args); + } + while ($dom =~ s/^[\w\-]*\.// and $dom =~ /\./); + } $self->set_cookie($args); delete $self->cookies->{$args->{'name'}}; } @@ -333,9 +343,9 @@ sub js_uri_path { sub no_cookies_print { my $self = shift; + return $self->{'no_cookies_print'}->($self) if $self->{'no_cookies_print'}; $self->cgix->print_content_type; print qq{
You do not appear to have cookies enabled.
}; - return 1; } sub login_print { @@ -510,7 +520,7 @@ sub parse_token { my $bkey; for my $armor ('none', 'base64', 'blowfish') { my $copy = ($armor eq 'none') ? $token - : ($armor eq 'base64') ? eval { local $^W; decode_base64($token) } + : ($armor eq 'base64') ? $self->use_base64 ? eval { local $^W; decode_base64($token) } : next : ($bkey = $self->use_blowfish) ? decrypt_blowfish($token, $bkey) : next; if ($self->complex_plaintext && $copy =~ m|^ ([^/]+) / (\d+) / (-?\d+) / ([^/]*) / (.*) $|x) { @@ -702,6 +712,12 @@ sub cleanup_user { return $user; } +sub check_form_user_against_cookie { + my ($self, $form_user, $cookie_user, $data) = @_; + return if ! defined($form_user) || ! defined($cookie_user); + return $form_user eq $cookie_user; +} + sub get_pass_by_user { my $self = shift; my $user = shift; diff --git a/lib/CGI/Ex/Conf.pm b/lib/CGI/Ex/Conf.pm index 04ff2a3..0fdb2f1 100644 --- a/lib/CGI/Ex/Conf.pm +++ b/lib/CGI/Ex/Conf.pm @@ -7,7 +7,7 @@ CGI::Ex::Conf - Conf Reader/Writer for many different data format types =cut ###----------------------------------------------------------------### -# Copyright 2007 - Paul Seamons # +# Copyright 2003-2012 - Paul Seamons # # Distributed under the Perl Artistic License without warranty # ###----------------------------------------------------------------### @@ -29,7 +29,7 @@ use vars qw($VERSION ); @EXPORT_OK = qw(conf_read conf_write in_cache); -$VERSION = '2.32'; +$VERSION = '2.37'; $DEFAULT_EXT = 'conf'; diff --git a/lib/CGI/Ex/Die.pm b/lib/CGI/Ex/Die.pm index 50aa0fd..e77b699 100644 --- a/lib/CGI/Ex/Die.pm +++ b/lib/CGI/Ex/Die.pm @@ -7,7 +7,7 @@ CGI::Ex::Die - A CGI::Carp::FatalsToBrowser type utility. =cut ###----------------------------------------------------------------### -# Copyright 2007 - Paul Seamons # +# Copyright 2004-2012 - Paul Seamons # # Distributed under the Perl Artistic License without warranty # ###----------------------------------------------------------------### @@ -23,7 +23,7 @@ use CGI::Ex; use CGI::Ex::Dump qw(debug ctrace dex_html); BEGIN { - $VERSION = '2.32'; + $VERSION = '2.37'; $SHOW_TRACE = 0 if ! defined $SHOW_TRACE; $IGNORE_EVAL = 0 if ! defined $IGNORE_EVAL; $EXTENDED_ERRORS = 1 if ! defined $EXTENDED_ERRORS; diff --git a/lib/CGI/Ex/Dump.pm b/lib/CGI/Ex/Dump.pm index 924d493..574eae5 100644 --- a/lib/CGI/Ex/Dump.pm +++ b/lib/CGI/Ex/Dump.pm @@ -7,7 +7,7 @@ CGI::Ex::Dump - A debug utility =cut ###----------------------------------------------------------------### -# Copyright 2007 - Paul Seamons # +# Copyright 2004-2012 - Paul Seamons # # Distributed under the Perl Artistic License without warranty # ###----------------------------------------------------------------### @@ -17,10 +17,10 @@ use vars qw(@ISA @EXPORT @EXPORT_OK $VERSION use strict; use Exporter; -$VERSION = '2.32'; +$VERSION = '2.37'; @ISA = qw(Exporter); @EXPORT = qw(dex dex_warn dex_text dex_html ctrace dex_trace); -@EXPORT_OK = qw(dex dex_warn dex_text dex_html ctrace dex_trace debug); +@EXPORT_OK = qw(dex dex_warn dex_text dex_html ctrace dex_trace debug caller_trace); ### is on or off sub on { $ON = 1 }; @@ -102,7 +102,7 @@ sub _what_is_this { } else { my $html = "
$called: $file line $line_n\n";
     for (0 .. $#dump) {
-      $dump[$_] =~ s/\\n/\n/g;
+      $dump[$_] =~ s/(?$var[$_]|g;
       $html .= $dump[$_];
@@ -113,6 +113,7 @@ sub _what_is_this {
     CGI::Ex::print_content_type();
     print $html;
   }
+  return @_[0..$#_];
 }
 
 ### some aliases
@@ -160,6 +161,8 @@ sub ctrace {
   return \@i;
 }
 
+*caller_trace = \&ctrace;
+
 sub dex_trace {
   _what_is_this(ctrace(1));
 }
diff --git a/lib/CGI/Ex/Fill.pm b/lib/CGI/Ex/Fill.pm
index 1183950..74bff19 100644
--- a/lib/CGI/Ex/Fill.pm
+++ b/lib/CGI/Ex/Fill.pm
@@ -7,7 +7,7 @@ CGI::Ex::Fill - Fast but compliant regex based form filler
 =cut
 
 ###----------------------------------------------------------------###
-#  Copyright 2007 - Paul Seamons                                     #
+#  Copyright 2003-2012 - Paul Seamons                                #
 #  Distributed under the Perl Artistic License without warranty      #
 ###----------------------------------------------------------------###
 
@@ -24,7 +24,7 @@ use vars qw($VERSION
 use base qw(Exporter);
 
 BEGIN {
-    $VERSION   = '2.32';
+    $VERSION   = '2.37';
     @EXPORT    = qw(form_fill);
     @EXPORT_OK = qw(fill form_fill html_escape get_tagval_by_key swap_tagval_by_key);
 };
diff --git a/lib/CGI/Ex/JSONDump.pm b/lib/CGI/Ex/JSONDump.pm
index b45b11b..6a5e825 100644
--- a/lib/CGI/Ex/JSONDump.pm
+++ b/lib/CGI/Ex/JSONDump.pm
@@ -7,7 +7,7 @@ CGI::Ex::JSONDump - Comprehensive data to JSON dump.
 =cut
 
 ###----------------------------------------------------------------###
-#  Copyright 2007 - Paul Seamons                                     #
+#  Copyright 2006-2012 - Paul Seamons                                #
 #  Distributed under the Perl Artistic License without warranty      #
 ###----------------------------------------------------------------###
 
@@ -17,7 +17,7 @@ use strict;
 use base qw(Exporter);
 
 BEGIN {
-    $VERSION  = '2.32';
+    $VERSION  = '2.37';
 
     @EXPORT = qw(JSONDump);
     @EXPORT_OK = @EXPORT;
diff --git a/lib/CGI/Ex/Template.pm b/lib/CGI/Ex/Template.pm
index 1cd650a..457b437 100644
--- a/lib/CGI/Ex/Template.pm
+++ b/lib/CGI/Ex/Template.pm
@@ -25,7 +25,7 @@ use vars qw($VERSION
             $VOBJS
             );
 
-$VERSION = '2.32';
+$VERSION = '2.37';
 
 ### install true symbol table aliases that can be localized
 *QR_PRIVATE        = *Template::Alloy::QR_PRIVATE;
diff --git a/lib/CGI/Ex/Validate.pm b/lib/CGI/Ex/Validate.pm
index d2717cc..450fa2e 100644
--- a/lib/CGI/Ex/Validate.pm
+++ b/lib/CGI/Ex/Validate.pm
@@ -2,13 +2,13 @@ package CGI::Ex::Validate;
 
 ###---------------------###
 #  See the perldoc in CGI/Ex/Validate.pod
-#  Copyright 2008 - Paul Seamons
+#  Copyright 2003-2012 - Paul Seamons
 #  Distributed under the Perl Artistic License without warranty
 
 use strict;
 use Carp qw(croak);
 
-our $VERSION  = '2.32';
+our $VERSION  = '2.37';
 our $QR_EXTRA = qr/^(\w+_error|as_(array|string|hash)_\w+|no_\w+)/;
 our @UNSUPPORTED_BROWSERS = (qr/MSIE\s+5.0\d/i);
 our $JS_URI_PATH;
@@ -372,7 +372,7 @@ sub validate_buddy {
     # at this point @errors should still be empty
     my $content_checked; # allow later for possible untainting (only happens if content was checked)
 
-    foreach my $value (@$values) {
+    OUTER: foreach my $value (@$values) {
 
         if (exists $field_val->{'enum'}) {
             my $ref = ref($field_val->{'enum'}) ? $field_val->{'enum'} : [split(/\s*\|\|\s*/,$field_val->{'enum'})];
@@ -383,6 +383,17 @@ sub validate_buddy {
             if (! $found) {
                 return [] if $self->{'_check_conditional'};
                 push @errors, [$field, 'enum', $field_val, $ifs_match];
+                next OUTER;
+            }
+            $content_checked = 1;
+        }
+
+        # do specific type checks
+        if (exists $field_val->{'type'}) {
+            if (! $self->check_type($value, $field_val->{'type'}, $field, $form)){
+                return [] if $self->{'_check_conditional'};
+                push @errors, [$field, 'type', $field_val, $ifs_match];
+                next OUTER;
             }
             $content_checked = 1;
         }
@@ -403,6 +414,7 @@ sub validate_buddy {
             if ($not ? $success : ! $success) {
                 return [] if $self->{'_check_conditional'};
                 push @errors, [$field, $type, $field_val, $ifs_match];
+                next OUTER;
             }
             $content_checked = 1;
         } }
@@ -513,20 +525,20 @@ sub validate_buddy {
         # server side custom type
         if ($types{'custom'}) { foreach my $type (@{ $types{'custom'} }) {
             my $check = $field_val->{$type};
-            next if UNIVERSAL::isa($check, 'CODE') ? &$check($field, $value, $field_val, $type) : $check;
+            my $err;
+            if (UNIVERSAL::isa($check, 'CODE')) {
+                my $ok;
+                $err = "$@" if ! eval { $ok = $check->($field, $value, $field_val, $type, $form); 1 };
+                next if $ok;
+                chomp($err) if !ref($@) && defined($err);
+            } else {
+                next if $check;
+            }
             return [] if $self->{'_check_conditional'};
-            push @errors, [$field, $type, $field_val, $ifs_match];
+            push @errors, [$field, $type, $field_val, $ifs_match, (defined($err) ? $err : ())];
             $content_checked = 1;
         } }
 
-        # do specific type checks
-        if ($types{'type'}) { foreach my $type (@{ $types{'type'} }) {
-            if (! $self->check_type($value,$field_val->{'type'},$field,$form)){
-                return [] if $self->{'_check_conditional'};
-                push @errors, [$field, $type, $field_val, $ifs_match];
-            }
-            $content_checked = 1;
-        } }
     }
 
     # allow for the data to be "untainted"
@@ -552,7 +564,7 @@ sub validate_buddy {
 ### used to validate specific types
 sub check_type {
     my ($self, $value, $type) = @_;
-
+    $type = lc $type;
     if ($type eq 'email') {
         return 0 if ! $value;
         my ($local_p,$dom) = ($value =~ /^(.+)\@(.+?)$/) ? ($1,$2) : return 0;
@@ -587,11 +599,20 @@ sub check_type {
         return 0 if $value && ! $self->check_type($value,'uri');
 
     # validate a uri - the path portion of a request
-    } elsif ($type eq 'URI') {
+    } elsif ($type eq 'uri') {
         return 0 if ! $value;
         return 0 if $value =~ m/\s+/;
 
-    } elsif ($type eq 'CC') {
+    } elsif ($type eq 'int') {
+        return 0 if $value !~ /^-? (?: 0 | [1-9]\d*) $/x;
+        return 0 if ($value < 0) ? $value < -2**31 : $value > 2**31-1;
+    } elsif ($type eq 'uint') {
+        return 0 if $value !~ /^   (?: 0 | [1-9]\d*) $/x;
+        return 0 if $value > 2**32-1;
+    } elsif ($type eq 'num') {
+        return 0 if $value !~ /^-? (?: 0 | [1-9]\d* (?:\.\d+)? | 0?\.\d+) $/x;
+
+    } elsif ($type eq 'cc') {
         return 0 if ! $value;
         return 0 if $value =~ /[^\d\-\ ]/;
         $value =~ s/\D//g;
@@ -899,7 +920,8 @@ sub get_error_text {
     my $self  = shift;
     my $err   = shift;
     my $extra = $self->{extra} || {};
-    my ($field, $type, $field_val, $ifs_match) = @$err;
+    my ($field, $type, $field_val, $ifs_match, $custom_err) = @$err;
+    return $custom_err if defined($custom_err) && length($custom_err);
     my $dig     = ($type =~ s/(_?\d+)$//) ? $1 : '';
     my $type_lc = lc($type);
 
diff --git a/lib/CGI/Ex/Validate.pod b/lib/CGI/Ex/Validate.pod
index 9311c05..53a24b6 100644
--- a/lib/CGI/Ex/Validate.pod
+++ b/lib/CGI/Ex/Validate.pod
@@ -335,8 +335,8 @@ This section lists the available validation types.  Multiple instances
 of the same type may be used for some validation types by adding a
 number to the type (ie match, match2, match232).  Multiple instances
 are validated in sorted order.  Types that allow multiple values are:
-compare, custom, custom_js, equals, enum, match, required_if, sql,
-type, validate_if, and replace (replace is a MODIFICATION TYPE).
+compare, custom, custom_js, equals, match, required_if, sql, validate_if,
+and replace (replace is a MODIFICATION TYPE).
 
 =over 4
 
@@ -366,22 +366,41 @@ validation and an error is added.
     {
         field => 'username',
         custom => sub {
-            my ($key, $val, $type, $field_val_hash) = @_;
+            my ($key, $val, $field_val_hash, $checktype, $form) = @_;
             # do something here
             return 0;
         },
+        custom_error => '$name was not valid',
+    }
+
+Often it is desirable to specify a different message depending upon
+the code passed to custom.  To use a custom error message simply die
+with the error message.  Note that you will want to add a newline or
+else perl will add the line number and file for you -
+CGI::Ex::Validate will remove the trailing newline.
+
+    {
+        field => 'username',
+        custom => sub {
+            my ($key, $val) = @_;
+            die "Custom error message 1\n" if $val eq '1';
+            die "Custom error message 2\n" if $val eq '2';
+            return 0;
+        },
+        custom_error => '$name default custom error message',
     }
 
 =item C
 
-Custom value - only available in JS.  Allows for extra programming types.
-May be a javascript function (if fully declared in javascript), a string containing
-a javascript function (that will be eval'ed into a real function),
-a boolean value pre-determined before calling validate, or may be
-section of javascript that will be eval'ed (the last value of
-the eval'ed javascript will determine if validation passed).  A false response indicates
-the value did not pass validation.  A true response indicates that it did.  See
-the samples/validate_js_0_tests.html page for a sample of usages.
+Custom value - only available in JS.  Allows for extra programming
+types.  May be a javascript function (if fully declared in
+javascript), a string containing a javascript function (that will be
+eval'ed into a real function), a boolean value pre-determined before
+calling validate, or may be section of javascript that will be eval'ed
+(the last value of the eval'ed javascript will determine if validation
+passed).  A false response indicates the value did not pass
+validation.  A true response indicates that it did.  See the
+samples/validate_js_0_tests.html page for a sample of usages.
 
     {
         field => 'date',
@@ -389,17 +408,31 @@ the samples/validate_js_0_tests.html page for a sample of usages.
         match    => 'm|^\d\d\d\d/\d\d/\d\d$|',
         match_error => 'Please enter date in YYYY/MM/DD format',
         custom_js => "function (args) {
-            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;
+            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;
             (args.value > ''+y+'/'+m+'/'+d) ? 1 : 0;
         }",
         custom_js_error => 'The date was not greater than today.',
     }
 
+Often it is desirable to specify a different message depending upon
+the function passed to custom_js.  To use a custom error message simply throw
+the error message.
+
+    {
+        field => 'username',
+        custom_js => 'function (args) {
+            if (args.value == 1) throw "Custom error message 1";
+            if (args.value == 2) throw "Custom error message 2";
+            return 0;
+        }',
+        custom_js_error => '$name default custom error message',
+    }
+
 =item C
 
 Allows for checking whether an item matches a set of options.  In perl
@@ -483,7 +516,11 @@ to allow more than one item by any given name).
 =item C
 
 Requires the form field to have some value.  If the field is not present,
-no other checks will be run.
+no other checks will be run and an error will be given.
+
+It has been common for code to try C 0> which essentially has
+no effect - instead use C 'fieldname', required => 1>.  This
+results in the fieldname only being required if the fieldname is present.
 
 =item C
 
@@ -496,6 +533,9 @@ as saying:
 
     required_if => 'some_condition',
 
+It is different in that other checks will run - whereas validate_if skips
+all validation if some condition is not met.
+
 If a regex is used for the field name, the required_if
 field will have any match patterns swapped in.
 
@@ -524,15 +564,67 @@ $self->{dbh} is a coderef - they will be called and should return a dbh.
 =item C
 
 Allows for more strict type checking.  Currently supported types
-include CC (credit card), EMAIL, DOMAIN, IP, URL.  Other types will be
-added upon request provided we can add a perl and a javascript
-version.
+include CC (credit card), EMAIL, DOMAIN, IP, URL, INT, UINT, and NUM.
+Other types will be added upon request provided we can add a perl and
+a javascript version (extra types often aren't necessary as the custom and
+custom_js options give arbitrary checking).  If a type checks fails - other
+compare, custom, or length checks will not be ran.
 
     {
         field => 'credit_card',
         type  => 'CC',
     }
 
+=over 4
+
+item C
+
+Simple Luhn-10 check.  Note that spaces and dashes are left in place.
+
+=item C
+
+Very basic check to see if the value looks like an address.  The local part
+must only contain [\w.~!\#\$%\^&*\-=+?] and the domain must be a domain or ip.
+If you want full fledged RFC compliant checking consider something like:
+
+    {
+        field => 'email',
+        custom => sub {
+            my ($key, $val, $fv, $type, $form) = @_;
+            require Mail::Address;
+            my @a = Mail::Address->parse($val);
+            die "Invalid address\n" if @a != 1;
+            return $form->{$key} = $a[0]->address;
+         },
+     }
+
+=item C
+
+Checks for a valid domain name - does no lookup of the domain.  For that use
+a custom sub.
+
+=item C
+
+Basic IPv4 check.
+
+=item C
+
+Basic check that string matches something resembling an http or https url.
+
+=item C
+
+Checks for an integer between -2147483648 and -2147483648
+
+=item C
+
+Checks for an unsigned integer between 0 and 4294967295.
+
+=item C
+
+Checks for something that looks like a number.  Scientic notation is not allowed.  No range enforced.
+
+=back
+
 =item C
 
 If validate_if is specified, the field will only be validated
@@ -1079,19 +1171,36 @@ samples/validate_js_2_onchange.html to highlight the row and set an icon.
         = '!';
       document.getElementById(args.key+'_row').style.background
         = '#ffdddd';
-};
+    };
 
-document.validate_clear_hook = function (args) {
-    if (args.was_valid) {
+    document.validate_clear_hook = function (args) {
+      if (args.was_valid) {
         document.getElementById(args.key+'_img').innerHTML
             = '+';
         document.getElementById(args.key+'_row').style.background
             = '#ddffdd';
-    } else {
+      } else {
         document.getElementById(args.key+'_img').innerHTML = '';
         document.getElementById(args.key+'_row').style.background = '#fff';
-    }
-};
+      }
+    };
+
+If you have jquery that looks like:
+
+    document.validate_set_hook = function (args) {
+      $('#'+args.key+'_img').html('!');
+      $('#'+args.key+'_row').css('backgroundColor', '#ffdddd');
+    };
+
+    document.validate_clear_hook = function (args) {
+      if (args.was_valid) {
+        $('#'+args.key+'_img').html('+');
+        $('#'+args.key+'_row').css('backgroundColor', '#ddffdd');
+      } else {
+        $('#'+args.key+'_img').html('');
+        $('#'+args.key+'_row').css('backgroundColor', '#fff');
+      }
+    };
 
 These hooks can also be set as "group clear_hook" and "group set_hook"
     which are defined further above.
@@ -1104,11 +1213,6 @@ These hooks can also be set as "group clear_hook" and "group set_hook"
     that if the javascript didn't validate correctly, the user can still
 submit the data.
 
-=head1 THANKS
-
-Thanks to Eamon Daly for providing bug fixes for bugs in validate.js
-caused by HTML::Prototype.
-
 =head1 LICENSE
 
 This module may be distributed under the same terms as Perl itself.
diff --git a/lib/CGI/Ex/validate.js b/lib/CGI/Ex/validate.js
index 7b46911..a3c0db0 100644
--- a/lib/CGI/Ex/validate.js
+++ b/lib/CGI/Ex/validate.js
@@ -1,4 +1,4 @@
-// Copyright 2008 - Paul Seamons - $Revision: 1.18 $
+// Copyright 2003-2012 - Paul Seamons - ver 2.37
 // Distributed under the Perl Artistic License without warranty
 // See perldoc CGI::Ex::Validate for usage
 
@@ -25,7 +25,7 @@ function v_get_ordered_fields (val_hash) {
  var field_keys = [];
  var m;
  for (var key in val_hash) {
-  if (key == 'extend') continue; // Protoype Array()
+  if (!val_hash.hasOwnProperty(key)) continue;
   if (m = key.match(/^(general|group)\s+(\w+)/)) {
     ARGS[m[2]] = val_hash[key];
     continue;
@@ -97,16 +97,16 @@ function v_clean_field_val (field_val, N_level) {
   var k = field_val.order[i];
   var v = field_val[k];
   if (typeof(v) == 'undefined') return {error:v_error('No matching validation found on field '+field+' for type '+k)};
-  if (k.match(/^(min|max)_in_set(\d*)$/)) {
+  if (k.match(/^(min|max)_in_set_?(\d*)$/)) {
    if (typeof(v) == 'string') {
     if (! (m = v.match(/^\s*(\d+)(?:\s*[oO][fF])?\s+(.+)\s*$/))) return {error:v_error("Invalid "+k+" check "+v)};
     field_val[k] = m[2].split(/[\s,]+/);
     field_val[k].unshift(m[1]);
    }
    for (var j = 1; j < field_val[k].length; j++) if (field_val[k][j] != field_val.field) field_val.deps[field_val[k][j]] = 1;
-  } else if (k.match(/^(enum|compare)\d*$/)) {
+  } else if (k.match(/^(enum|compare)_?\d*$/)) {
    if (typeof(v) == 'string') field_val[k] = v.split(/\s*\|\|\s*/);
-  } else if (k.match(/^match\d*$/)) {
+  } else if (k.match(/^match_?\d*$/)) {
    if (typeof(v) == 'string') v = field_val[k] = v.split(/\s*\|\|\s*/);
    for (var j = 0; j < v.length; j++) {
     if (typeof(v[j]) != 'string' || v[j] == '!') continue;
@@ -120,9 +120,9 @@ function v_clean_field_val (field_val, N_level) {
     v[j] = new RegExp(pat, opt);
     if (not) v.splice(j, 0, '!');
    }
-  } else if (k.match(/^custom_js\d*$/)) {
+  } else if (k.match(/^custom_js_?\d*$/)) {
    if (typeof(v) == 'string' && v.match(/^\s*function\s*\(/)) eval("field_val[k] = "+v);
-  } else if (k.match(/^(validate|required)_if\d*$/)) {
+  } else if (k.match(/^(validate|required)_if_?\d*$/)) {
     if (typeof(v) == 'string' || ! v.length) v = field_val[k] = [v];
     var deps = v_clean_cond(v, N_level);
     for (var k in deps) field_val.deps[k] = 2;
@@ -248,8 +248,8 @@ function v_filter_types (type, types) {
  return values;
 }
 
-function v_add_error (errors,field,type,field_val,ifs_match,form) {
- errors.push([field, type, field_val, ifs_match]);
+function v_add_error (errors,field,type,field_val,ifs_match,form,custom_err) {
+ errors.push([field, type, field_val, ifs_match, custom_err]);
  if (field_val.clear_on_error) {
   var el = form[field];
   if (el && el.type && el.type.match(/(hidden|password|text|textarea|submit)/)) el.value = '';
@@ -260,7 +260,7 @@ function v_add_error (errors,field,type,field_val,ifs_match,form) {
 function v_field_order (field_val) {
  var o = [];
  for (var k in field_val)
-   if (! k.match(/^(extend|field|name|required|was_valid|was_checked|had_error)$/) && ! k.match(/_error$/)) o.push(k);
+   if (field_val.hasOwnProperty(k) && ! k.match(/^(field|name|required|was_valid|was_checked|had_error)$/) && ! k.match(/_error$/)) o.push(k);
  return o.sort();
 }
 
@@ -408,19 +408,29 @@ function v_validate_buddy (form, field, field_val, val_hash, ifs_match) {
   }
  }
 
- for (var i = 0; i < types.length; i++) {
-  var type = types[i];
-  var _fv  = field_val[type];
-  for (var n = 0; n < values.length; n++) {
-   var value = values[n];
+ for (var n = 0; n < values.length; n++) {
+  var value = values[n];
+
+  if (typeof field_val['enum'] != 'undefined') {
+   var is_found = 0;
+   for (var j = 0; j < field_val['enum'].length; j++) if (value == field_val['enum'][j]) { is_found = 1; break }
+   if (! is_found) {
+    v_add_error(errors, field, 'enum', field_val, ifs_match, form);
+    continue;
+   }
+  }
 
-   if (type.match(/^enum\d*$/)) {
-    var is_found = 0;
-    for (var j = 0; j < _fv.length; j++) if (value == _fv[j]) { is_found = 1; break }
-    if (! is_found) v_add_error(errors, field, type, field_val, ifs_match, form);
+  if (typeof field_val['type'] != 'undefined')
+   if (! v_check_type(value, field_val['type'], field, form)) {
+    v_add_error(errors, field, 'type', field_val, ifs_match, form);
+    continue;
    }
 
-   if (type.match(/^equals\d*$/)) {
+  for (var i = 0; i < types.length; i++) {
+   var type = types[i];
+   var _fv  = field_val[type];
+
+   if (type.match(/^equals_?\d*$/)) {
     var not = _fv.match(/^!\s*/);
     if (not) _fv = _fv.substring(not[0].length);
     var success = 0;
@@ -431,14 +441,16 @@ function v_validate_buddy (form, field, field_val, val_hash, ifs_match) {
      if (typeof(value2) == 'undefined') value2 = '';
      if (value == value2) success = 1;
     }
-    if (not && success || ! not && ! success)
+    if (not && success || ! not && ! success) {
      v_add_error(errors, field, type, field_val, ifs_match, form);
+     break;
+    }
    }
 
    if (type == 'min_len' && value.length < _fv) v_add_error(errors, field, 'min_len', field_val, ifs_match, form);
    if (type == 'max_len' && value.length > _fv) v_add_error(errors, field, 'max_len', field_val, ifs_match, form);
 
-   if (type.match(/^match\d*$/)) {
+   if (type.match(/^match_?\d*$/)) {
     for (var j = 0; j < _fv.length; j++) {
      if (typeof(_fv[j]) == 'string') continue;
      var not = (j > 0 && typeof(_fv[j-1]) == 'string' && _fv[j-1] == '!') ? 1 : 0;
@@ -447,7 +459,7 @@ function v_validate_buddy (form, field, field_val, val_hash, ifs_match) {
     }
    }
 
-   if (type.match(/^compare\d*$/)) {
+   if (type.match(/^compare_?\d*$/)) {
     for (var j = 0; j < _fv.length; j++) {
      var comp = _fv[j];
      if (! comp) continue;
@@ -478,19 +490,20 @@ function v_validate_buddy (form, field, field_val, val_hash, ifs_match) {
      if (! hold) v_add_error(errors, field, type, field_val, ifs_match, form);
     }
    }
-
-   if (type.match(/^type\d*$/))
-    if (! v_check_type(value, _fv, field, form))
-     v_add_error(errors, field, type, field_val, ifs_match, form);
   }
+ }
 
+ for (var i = 0; i < types.length; i++) {
+  var type = types[i];
+  var _fv  = field_val[type];
   // the js is evaluated and should return 1 for success
   // or 0 for failure - the variables field, value, and field_val (the hash) are available
-  if (type.match(/^custom_js\d*$/)) {
+  if (type.match(/^custom_js_?\d*$/)) {
    var value = values.length == 1 ? values[0] : values;
-   if (typeof(_fv) == 'function'
-       ? ! _fv({'value':value, 'field_val':field_val, 'form':form, 'key':field_val.field, 'errors':errors, 'event':v_event})
-       : ! eval(_fv)) v_add_error(errors, field, type, field_val, ifs_match, form);
+   var err;
+   var ok;
+   try { ok = (typeof _fv == 'function') ? _fv({'value':value, 'field_val':field_val, 'form':form, 'key':field_val.field, 'errors':errors, 'event':v_event}) : eval(_fv) } catch (e) { err = e }
+   if (!ok) v_add_error(errors, field, type, field_val, ifs_match, form, err);
   }
  }
 
@@ -542,6 +555,15 @@ function v_check_type (value, type, field, form) {
   if (! value) return 0;
   if (value.match(/\s/)) return 0;
 
+ } else if (type == 'INT') {
+  if (!value.match(/^-?(?:0|[1-9]\d*)$/)) return 0;
+  if ((value < 0) ? value < -Math.pow(2,31) : value > Math.pow(2,31)-1) return 0;
+ } else if (type == 'UINT') {
+  if (!value.match(/^(?:0|[1-9]\d*)$/)) return 0;
+  if (value > Math.pow(2,32)-1) return 0;
+ } else if (type == 'NUM') {
+  if (!value.match(/^-?(?:0|[1-9]\d*(?:\.\d+)?|0?\.\d+)$/)) return 0;
+
  } else if (type == 'CC') {
   if (! value) return 0;
   if (value.match(/[^\d\- ]/)) return 0;
@@ -644,10 +666,11 @@ function v_get_error_text (err, extra1, extra2) {
  var type      = err[1];
  var field_val = err[2];
  var ifs_match = err[3];
- var m;
+ if (err.length == 5 && typeof err[4] != 'undefined' && err[4].length) return err[4]; // custom error from throw in custom_js
 
+ var m;
  var dig = '';
- if (m = type.match(/^(.+?)(\d+)$/)) { type = m[1] ; dig = m[2] }
+ if (m = type.match(/^(.+?)(_?\d+)$/)) { type = m[1] ; dig = m[2] }
  var type_lc = type.toLowerCase();
  var v = field_val[type + dig];
 
@@ -795,7 +818,7 @@ function eob_as_hash (extra) {
   var header = v_find_val('as_hash_header', extra, this.extra, '');
   var footer = v_find_val('as_hash_footer', extra, this.extra, '');
   for (var key in ret) {
-   if (key == 'extend') continue; // Protoype Array()
+   if (!ret.hasOwnProperty(key)) continue;
    ret[key] = header + ret[key].join(joiner) + footer;
   }
  }
@@ -820,7 +843,7 @@ document.validate = function (form, val_hash) {
 
  if (v_event != 'load') {
   for (var key in v_did_inline) {
-   if (key == 'extend') continue; // Protoype Array()
+   if (!v_did_inline.hasOwnProperty(key)) continue;
    v_inline_error_clear(key, val_hash, form);
   }
  }
@@ -844,7 +867,7 @@ document.validate = function (form, val_hash) {
  if (! val_hash['group no_inline']) {
   var hash = err_obj.as_hash({as_hash_suffix:""});
   for (var key in hash) {
-   if (key == 'extend') continue; // Protoype Array()
+   if (!hash.hasOwnProperty(key)) continue;
    v_inline_error_set(key, hash[key], val_hash, form);
   }
  }
@@ -946,7 +969,7 @@ document.check_form = function (form, val_hash) {
    for (var j in clean.fields[i].deps) if (j != clean.fields[i].field) _add(j, clean.fields[i]);
   }
   for (var k in h) {
-   if (k == 'extend') continue; // Protoype Array()
+   if (!h.hasOwnProperty(k)) continue;
    var el = form[k];
    if (! el) return v_error("No form element by the name "+k);
    var _change = !types.change ? 0 : typeof(types.change) == 'object' ? types.change[k] : 1;
@@ -997,7 +1020,7 @@ function v_el_attach (el, fvs, form, val_hash, _change, _blur) {
   e = new ValidateError(e, {});
   e = e.as_hash({as_hash_suffix:"", first_only:(val_hash['group first_only']?1:0)});
   for (var k in e) {
-   if (k == 'extend') continue; // Protoype Array()
+   if (!e.hasOwnProperty(k)) continue;
    v_inline_error_set(k, e[k], val_hash, form);
   }
  };
diff --git a/samples/validate_js_0_tests.html b/samples/validate_js_0_tests.html
index 5471ec8..90f4228 100644
--- a/samples/validate_js_0_tests.html
+++ b/samples/validate_js_0_tests.html
@@ -135,8 +135,15 @@ function run_tests () {
   ok(e.text1_error == "The field text1 is not in the given list.", "Got the right enum error");
   e = validate({text1:1}, v);
   ok(! e, "No enum error");
+
+  v = {text1:{'enum':[1, 2, 3],match:'m/3/'}};
+  e = validate({text1:1}, v);
+  ok(e, 'enum');
+  ok(e.text1_error == "The field text1 contains invalid characters.", 'enum shortcircuit');
+
   e = validate({text1:4}, v);
-  ok(e, "Got enum error");
+  ok(e, 'enum');
+  ok(e.text1_error == "The field text1 is not in the given list.", 'enum shortcircuit2');
 
   v = {text2:{'enum':"1 || 2||3"}};
   e = validate({text2:1}, v);
@@ -200,6 +207,13 @@ function run_tests () {
   ok(e, "Got a match error");
   ok(e.text1_error == "The field text1 contains invalid characters.", "Got the right match error");
 
+  v = {text1:{match_2:'m/^\\w+$/',match_2_error:'Bad'}};
+  e = validate({text1:"abc"}, v);
+  ok(! e, "No match error");
+  e = validate({text1:"abc."}, v);
+  ok(e, "Got a match error");
+  ok(e.text1_error == "Bad", "Got the right match error");
+
   v = {text1:{match:['m/^\\w+$/', 'm/^[a-z]+$/']}};
   e = validate({text1:"abc"}, v);
   ok(! e, "No match error with multiple");
@@ -323,13 +337,26 @@ function run_tests () {
   e = validate({text1:"str"}, v);
   ok(! e, "No custom_js error for function type");
 
+  e = validate({text1: "str"}, {text1: {custom_js:"throw 'Always fail ('+value+')'"}});
+  ok(e, 'Got an error');
+  ok(e.text1_error == "Always fail (str)", "Passed along the message from throw");
+  e = validate({text1: "str2"}, {text1: {custom_js:function (args) { throw "Always fail2 ("+args.value+")" }}});
+  ok(e, 'Got an error');
+  ok(e.text1_error == "Always fail2 (str2)", "Passed along the message from throw");
+
+
+
   // type checks
-  v = {text1:{type:'ip'}};
-  e = validate({text1:'209.108.25'}, v);
-  ok(e, "Got type ip error");
-  ok(e.text1_error == "The field text1 did not match type ip.", "Got the right type ip error");
-  e = validate({text1:'209.108.25.111'}, v);
-  ok(! e, "No type ip error");
+  v = {text1: {type: 'ip', match: 'm/^203\./'}};
+  e = validate({text1: '209.108.25'}, v);
+  ok(e, 'type ip - with short circuit');
+  ok(e.text1_error == 'The field text1 did not match type ip.', 'type ip - was: '+e.text1_error); // make sure they short circuit
+  e = validate({text1: '209.108.25.111'}, v);
+  ok(e, 'type ip - but had match error');
+  ok(e.text1_error == 'The field text1 contains invalid characters.', 'type ip');
+  e = validate({text1: '203.108.25.111'}, v);
+  ok(! e, 'type ip');
+
   v = {text1:{type:'email'}};
   e = validate({text1:'foo.bar.com'}, v)
   ok(e, "Got an email error");
@@ -369,6 +396,18 @@ function run_tests () {
   ok(e, "Got cc error");
   e = validate({text1:'4241-4242-4242'}, {text1:{type:'cc'}});
   ok(e, "Got cc error");
+  for (var $_ in {"0":1, "2":1, "23":1, "-0":1, "-2":1, "-23":1, "0.0":1, ".1":1, "0.1":1, "0.10":1, "1.0":1, "1.01":1})
+    ok(!validate({text1: $_}, {text1: {type: 'num'}}),  "Type num "+$_)
+  for (var $_ in {"0":1, "2":1, "23":1, "-0":1, "-2":1, "-23":1, "2147483647":1, "-2147483648":1})
+    ok(!validate({text1: $_}, {text1: {type: 'int'}}),  "Type int "+$_);
+  for (var $_ in {"0":1, "2":1, "23":1, "4294967295":1})
+    ok(!validate({text1: $_}, {text1: {type: 'uint'}}), "Type uint "+$_);
+  for (var $_ in {"0a":1, "a2":1, "-0a":1, "0..0":1, "00":1, "001":1, "1.":1})
+    ok(validate({text1: $_},  {text1: {type: 'num'}}),  "Type num invalid "+$_);
+  for (var $_ in {"1.1":1, "0.1":1, "0.0":1, "-1.1":1, "0a":1, "a2":1, "a":1, "00":1, "001":1, "2147483648":1, "-2147483649":1})
+    ok(validate({text1: $_},  {text1: {type: 'int'}}),  "Type int invalid "+$_);
+  for (var $_ in {"-1":1, "-0":1, "1.1":1, "0.1":1, "0.0":1, "-1.1":1, "0a":1, "a2":1, "a":1, "00":1, "001":1, "4294967296":1})
+    ok(validate({text1: $_},  {text1: {type: 'uint'}}), "Type uint invalid "+$_);
 
   // min_in_set checks
   v = {text1:{min_in_set:'2 of text1 text2 password1', max_values:5}};
diff --git a/samples/validate_js_2_onchange.html b/samples/validate_js_2_onchange.html
index 95a405e..7d6e23a 100644
--- a/samples/validate_js_2_onchange.html
+++ b/samples/validate_js_2_onchange.html
@@ -145,7 +145,23 @@ document.validation = {
     min_len: 6,
     max_len: 30,
     match: ["m/\\d/", "m/[a-z]/"],
-    match_error: "$name must contain both a letter and a number."
+    match_error: "$name must contain both a letter and a number.",
+    custom_js: function (args) {
+      var v = args.value;
+      var n = 0;
+      if (v.match(/[a-z]/)) n++;
+      if (v.match(/[A-Z]/)) n++;
+      if (v.match(/[0-9]/)) n++;
+      var sym = v.match(/[ ~!@#$%^&*()_,.?{}\[\]]/) ? 1 : 0;
+      var s = (! v.length)   ? ''
+            : (v.length < 6) ? 'weak'
+            : (v.length < 7) ? (sym || n == 3)  ? 'ok' : 'weak'
+            : (v.length < 10) ? (n < 3 && ! sym) ? 'ok' : 'good'
+            : sym ? 'excellent' : 'good';
+      document.getElementById('password_strength').innerHTML = s;
+      if (s === 'weak') throw "Cannot use a weak password.  Try increasing the length or adding variation.";
+      return 1;
+    }
   },
   password2: {
     validate_if: 'password was_valid',
@@ -195,7 +211,8 @@ document.validation = {
   compare: {
     required: 1,
     required_error: "Please type a number",
-    replace: "s/\\D//g",
+    type: 'num',
+    type_error: 'Please type a valid number',
     name: "Compare check",
     compare: ['> 99', '< 1000'],
     compare_error: '$name must be greater than 99 and less than 1000.'
@@ -213,25 +230,6 @@ document.validation = {
   foo: {
     min_in_set: "2 of foo bar baz",
     max_in_set: "2 of foo bar baz"
-  },
-  pw_strength: { // separate from the pw validation
-    field: "password",
-    custom_js: function (args) {
-      var v = args.value;
-      var n = 0;
-      if (v.match(/[a-z]/)) n++;
-      if (v.match(/[A-Z]/)) n++;
-      if (v.match(/[0-9]/)) n++;
-      var sym = v.match(/[ ~!@#$%^&*()_,.?{}\[\]]/) ? 1 : 0;
-      var s = (! v.length)   ? ''
-            : (v.length < 3) ? 'weak'
-            : (v.length < 5) ? (sym || n == 3)  ? 'ok' : 'weak'
-            : (v.length < 9) ? (n < 3 && ! sym) ? 'ok' : 'good'
-            : sym ? 'excellent' : 'good';
-      document.getElementById('password_strength').innerHTML = s;
-      // we could return false to indicate the strength wasn't good enough
-      return 1;
-    }
   }
 };
 if (document.check_form) document.check_form('a');
diff --git a/t/1_validate_05_types.t b/t/1_validate_05_types.t
index 3456803..f0a4914 100644
--- a/t/1_validate_05_types.t
+++ b/t/1_validate_05_types.t
@@ -7,14 +7,14 @@
 =cut
 
 use strict;
-use Test::More tests => 120;
+use Test::More tests => 181;
 
 use_ok('CGI::Ex::Validate');
 
 my $v;
 my $e;
 
-sub validate { scalar CGI::Ex::Validate::validate(@_) }
+sub validate { scalar CGI::Ex::Validate->new({as_array_title=>'',as_string_join=>"\n"})->validate(@_) }
 
 ### required
 $v = {foo => {required => 1}};
@@ -110,11 +110,14 @@ ok(! $e, 'enum');
 $e = validate({foo => 1, bar => 2}, $v);
 ok(! $e, 'enum');
 
-$e = validate({foo => 1, bar => 3}, $v);
-ok(! $e, 'enum');
+$v->{'foo'}->{'match'} = 'm/3/';
+$e = validate({foo => 1, bar => 2}, $v);
+ok($e, 'enum');
+is($e, "Foo contains invalid characters.", 'enum shortcircuit');
 
-$e = validate({foo => 1, bar => 4}, $v);
+$e = validate({foo => 4, bar => 1}, $v);
 ok($e, 'enum');
+is($e, "Foo is not in the given list.", 'enum shortcircuit');
 
 # equals
 $v = {foo => {equals => 'bar'}};
@@ -331,11 +334,19 @@ ok($e, 'custom');
 $e = validate({foo => "str"}, $v);
 ok(! $e, 'custom');
 
+$e = validate({foo => "str"}, {foo => {custom => sub { my ($k, $v) = @_; die "Always fail ($v)\n" }}});
+ok($e, 'Got an error');
+is($e->as_hash->{'foo_error'}, "Always fail (str)", "Passed along the message from die");
+
 ### type checks
-$v = {foo => {type => 'ip'}};
+$v = {foo => {type => 'ip', match => 'm/^203\./'}};
 $e = validate({foo => '209.108.25'}, $v);
 ok($e, 'type ip');
+is($e, 'Foo did not match type ip.', 'type ip'); # make sure they short circuit
 $e = validate({foo => '209.108.25.111'}, $v);
+ok($e, 'type ip - but had match error');
+is($e, 'Foo contains invalid characters.', 'type ip');
+$e = validate({foo => '203.108.25.111'}, $v);
 ok(! $e, 'type ip');
 
 $v = {foo => {type => 'domain'}};
@@ -357,6 +368,13 @@ ok($e, 'type domain');
 $e = validate({foo => '1234567890123456789012345678901234567890123456789012345678901234.com'}, $v);
 ok($e, 'type domain');
 
+ok(!validate({n => $_}, {n => {type => 'num'}}),  "Type num $_")  for qw(0 2 23 -0 -2 -23 0.0 .1 0.1 0.10 1.0 1.01);
+ok(!validate({n => $_}, {n => {type => 'int'}}),  "Type int $_")  for qw(0 2 23 -0 -2 -23 2147483647 -2147483648);
+ok(!validate({n => $_}, {n => {type => 'uint'}}), "Type uint $_") for qw(0 2 23 4294967295);
+ok(validate({n => $_}, {n => {type  => 'num'}}),  "Type num invalid $_")  for qw(0a a2 -0a 0..0 00 001 1.);
+ok(validate({n => $_}, {n => {type  => 'int'}}),  "Type int invalid $_")  for qw(1.1 0.1 0.0 -1.1 0a a2 a 00 001 2147483648 -2147483649);
+ok(validate({n => $_}, {n => {type  => 'uint'}}), "Type uint invalid $_") for qw(-1 -0 1.1 0.1 0.0 -1.1 0a a2 a 00 001 4294967296);
+
 ### min_in_set checks
 $v = {foo => {min_in_set => '2 of foo bar baz', max_values => 5}};
 $e = validate({foo => 1}, $v);
diff --git a/t/4_app_00_base.t b/t/4_app_00_base.t
index 5d86aca..ef63648 100644
--- a/t/4_app_00_base.t
+++ b/t/4_app_00_base.t
@@ -16,13 +16,31 @@ we do try to put it through most paces.
 use Test::More tests => 234;
 use strict;
 use warnings;
-use CGI::Ex::Dump qw(debug);
+use CGI::Ex::Dump qw(debug caller_trace);
 
+{
+    package CGIXFail;
+    use vars qw($AUTOLOAD);
+    sub new { bless {}, __PACKAGE__ }
+    sub DESTROY {}
+    sub AUTOLOAD {
+        my $self = shift;
+        my $meth = ($AUTOLOAD =~ /::(\w+$)/) ? $1 : die "Invalid method $AUTOLOAD";
+        die "Not calling CGI::Ex method $meth while testing App";
+    }
+}
 {
     package Foo;
 
     use base qw(CGI::Ex::App);
     use vars qw($test_stdout);
+    use CGI::Ex::Dump qw(debug caller_trace);
+
+    sub cgix { shift->{'cgix'} ||= CGIXFail->new } # for our tests try not to access external
+
+    sub form { shift->{'form'} ||= {} }
+
+    sub cookies { shift->{'cookies'} ||= {} }
 
     sub init { $test_stdout = '' }
 
@@ -693,7 +711,7 @@ foreach my $type (qw(base
         if $type ne 'form';
 
     my $meth2 = "add_to_$type";
-    my $c = CGI::Ex::App->new;
+    my $c = CGI::Ex::App->new({cgix => CGI::Ex->new({form=>{}})});
     $c->$meth2({bing => 'bang'});
     $c->$meth2(bong => 'beng');
 
-- 
2.43.0