]> Dogcows Code - chaz/p5-CGI-Ex/commitdiff
CGI::Ex 1.14 v1.14
authorPaul Seamons <perl@seamons.com>
Mon, 28 Feb 2005 00:00:00 +0000 (00:00 +0000)
committerCharles McGarvey <chazmcgarvey@brokenzipper.com>
Fri, 9 May 2014 23:46:39 +0000 (17:46 -0600)
89 files changed:
CGI-Ex.spec [new file with mode: 0644]
Changes [new file with mode: 0644]
MANIFEST [new file with mode: 0644]
MANIFEST.SKIP [new file with mode: 0644]
META.yml [new file with mode: 0644]
Makefile.PL [new file with mode: 0644]
README [new file with mode: 0644]
lib/CGI/Ex.pm [new file with mode: 0644]
lib/CGI/Ex/App.pm [new file with mode: 0644]
lib/CGI/Ex/Auth.pm [new file with mode: 0644]
lib/CGI/Ex/Conf.pm [new file with mode: 0644]
lib/CGI/Ex/Die.pm [new file with mode: 0644]
lib/CGI/Ex/Dump.pm [new file with mode: 0644]
lib/CGI/Ex/Fill.pm [new file with mode: 0644]
lib/CGI/Ex/Template.pm [new file with mode: 0644]
lib/CGI/Ex/Validate.pm [new file with mode: 0644]
lib/CGI/Ex/md5.js [new file with mode: 0644]
lib/CGI/Ex/sha1.js [new file with mode: 0644]
lib/CGI/Ex/validate.js [new file with mode: 0644]
lib/CGI/Ex/yaml_load.js [new file with mode: 0644]
t/0_ex_00_base.t [new file with mode: 0644]
t/0_ex_01_swap.t [new file with mode: 0644]
t/1_validate_00_base.t [new file with mode: 0644]
t/1_validate_01_form.t [new file with mode: 0644]
t/1_validate_02_form_fail.t [new file with mode: 0644]
t/1_validate_03_cgi.t [new file with mode: 0644]
t/1_validate_04_cgi_fail.t [new file with mode: 0644]
t/1_validate_05_types.t [new file with mode: 0644]
t/1_validate_06_groups.t [new file with mode: 0644]
t/1_validate_07_yaml.t [new file with mode: 0644]
t/1_validate_08_yaml_file.t [new file with mode: 0644]
t/1_validate_09_perl_file.t [new file with mode: 0644]
t/1_validate_10_storable_file.t [new file with mode: 0644]
t/1_validate_11_no_extra.t [new file with mode: 0644]
t/1_validate_12_change.t [new file with mode: 0644]
t/1_validate_13_html_file.t [new file with mode: 0644]
t/1_validate_14_untaint.t [new file with mode: 0644]
t/2_fill_00_base.t [new file with mode: 0644]
t/2_fill_01_form.t [new file with mode: 0644]
t/2_fill_02_hidden.t [new file with mode: 0644]
t/2_fill_03_checkbox.t [new file with mode: 0644]
t/2_fill_04_select.t [new file with mode: 0644]
t/2_fill_05_textarea.t [new file with mode: 0644]
t/2_fill_06_radio.t [new file with mode: 0644]
t/2_fill_07_reuse.t [new file with mode: 0644]
t/2_fill_08_multiple_objects.t [new file with mode: 0644]
t/2_fill_09_default_type.t [new file with mode: 0644]
t/2_fill_10_escape.t [new file with mode: 0644]
t/2_fill_11_target.t [new file with mode: 0644]
t/2_fill_12_mult.t [new file with mode: 0644]
t/2_fill_13_warning.t [new file with mode: 0644]
t/2_fill_14_password.t [new file with mode: 0644]
t/2_fill_15_multiple_fields.t [new file with mode: 0644]
t/2_fill_16_ignore_fields.t [new file with mode: 0644]
t/2_fill_17_xhtml.t [new file with mode: 0644]
t/2_fill_18_coderef.t [new file with mode: 0644]
t/2_fill_19_complex.t [new file with mode: 0644]
t/2_fill_20_switcharoo.t [new file with mode: 0644]
t/3_conf_00_base.t [new file with mode: 0644]
t/3_conf_01_write.t [new file with mode: 0644]
t/4_app_00_base.t [new file with mode: 0644]
t/5_dump_00_base.t [new file with mode: 0644]
t/6_die_00_base.t [new file with mode: 0644]
t/samples/bench_cgix_hfif.pl [new file with mode: 0755]
t/samples/bench_conf_readers.pl [new file with mode: 0644]
t/samples/bench_conf_writers.pl [new file with mode: 0644]
t/samples/bench_method_calling.pl [new file with mode: 0755]
t/samples/cgi_ex_1.cgi [new file with mode: 0755]
t/samples/cgi_ex_2.cgi [new file with mode: 0755]
t/samples/conf_path_1/apples.pl [new file with mode: 0644]
t/samples/conf_path_1/oranges.pl [new file with mode: 0644]
t/samples/conf_path_3/apples.pl [new file with mode: 0644]
t/samples/conf_path_3/oranges.pl [new file with mode: 0644]
t/samples/generate_js.pl [new file with mode: 0644]
t/samples/html1.htm [new file with mode: 0644]
t/samples/html2.htm [new file with mode: 0644]
t/samples/js_validate_1.html [new file with mode: 0644]
t/samples/js_validate_2.html [new file with mode: 0644]
t/samples/js_validate_3.html [new file with mode: 0644]
t/samples/perl1.pl [new file with mode: 0644]
t/samples/perl2.pl [new file with mode: 0644]
t/samples/storable1.storable [new file with mode: 0644]
t/samples/yaml1.val [new file with mode: 0644]
t/samples/yaml2.val [new file with mode: 0644]
t/samples/yaml3.val [new file with mode: 0644]
t/samples/yaml_js_1.html [new file with mode: 0644]
t/samples/yaml_js_2.html [new file with mode: 0644]
t/samples/yaml_js_3.html [new file with mode: 0644]
t/samples/yaml_js_4.html [new file with mode: 0644]

diff --git a/CGI-Ex.spec b/CGI-Ex.spec
new file mode 100644 (file)
index 0000000..95d516b
--- /dev/null
@@ -0,0 +1,64 @@
+%define name CGI-Ex
+%define version 1.14
+
+%define __find_provides %( echo -n /usr/lib/rpm/find-provides && [ -x /usr/lib/rpm/find-provides.perl ] && echo .perl )
+%define __find_requires %( echo -n /usr/lib/rpm/find-requires && [ -x /usr/lib/rpm/find-requires.perl ] && echo .perl )
+
+Summary:        @SUMMARY@
+Name:           %{name}
+Version:        %{version}
+Release:        1
+Source0:        http://seamons.com/cgi_ex/%{name}-%{version}.tar.gz
+Group:          Development/Perl
+License:        Perl Artistic
+Vendor:         Paul Seamons
+Packager:       Paul Seamons
+BuildRequires:  perl
+BuildArch:      noarch
+BuildRoot:      %{_tmppath}/%{name}-%{version}-buildroot
+Provides:       %{name} = %{version}
+
+%description
+CGI::Ex is a Perl module that offers an extended suite of
+functionality, over and above that offered by CGI, HTML::FillInForm,
+and the host of Validator scripts on CPAN.  CGI::Ex tries to use the
+best functions from existing modules and extend them with rich
+functionality.  Particularly of interest is CGI::Ex::App which
+provides extremely easy yet robust CGI developement.
+
+%prep
+%setup -q -n %{name}-%{version}
+
+%build
+%{__perl} Makefile.PL
+%{__make} OPTIMIZE="$RPM_OPT_FLAGS"
+
+%install
+rm -rf $RPM_BUILD_ROOT
+
+# do the build
+%{makeinstall} PREFIX=$RPM_BUILD_ROOT%{_prefix}
+#if [ -x /usr/lib/rpm/brp-mandrake ] ; then
+#  /usr/lib/rpm/brp-mandrake
+#elif [ -x /usr/lib/brp-compress ] ; then
+#  /usr/lib/rpm/brp-compress
+#fi
+
+# Clean up some files we don't want/need
+find $RPM_BUILD_ROOT%{_prefix} -type d | tac | xargs rmdir --ign
+find $RPM_BUILD_ROOT%{_prefix} | grep i386 | tac | xargs rm -rf
+
+%clean
+rm -rf $RPM_BUILD_ROOT
+HERE=`pwd`
+cd ..
+rm -rf $HERE
+
+%files
+%defattr(-,root,root)
+#%doc README Changes
+%{_prefix}
+
+%changelog
+* Sat Nov 11 2003 Paul Seamons <>
+- first try
diff --git a/Changes b/Changes
new file mode 100644 (file)
index 0000000..0f19564
--- /dev/null
+++ b/Changes
@@ -0,0 +1,101 @@
+2005-02-28  Paul Seamons  <cgi_ex@seamons.com>
+
+        * Version 1.14 is done
+        * Bug fix in validate (OR's were not working)
+        * Allow for checking for package existence without require in App
+        * Add hash_swap
+        * Add hash_base
+        * Add add_to_ methods for swap, fill, form, common, and errors
+        * Swap the run_hook order of step and hookname
+        * Allow for untaint in CGI::Ex::Validate
+        * Allow for !field in equals of CGI::Ex::Validate
+        * Allow for return of names in CGI::Ex::Validate
+        * All CGI::Ex to work better with CGI/mod_perl1/mod_perl2
+        * Fix required => 0 in javascript
+
+2004-12-02  Paul Seamons  <cgi_ex@seamons.com>
+
+        * Version 1.13 is done
+        * Show more App perldoc examples
+        * Fix some App path bugs
+        * Add more app hooks
+        * Cleanup app code
+        * Allow for Conf to write to each of the types
+
+2004-11-010  Paul Seamons  <cgi_ex@seamons.com>
+
+        * Version 1.12 is done
+        * Show more App perldoc examples
+        * Fix some App path bugs
+        * Change some App method names
+        * Allow for App js_validation
+        * Allow for App validation redirection
+
+2004-11-08  Paul Seamons  <cgi_ex@seamons.com>
+
+        * Version 1.11 is done
+        * Let file path dependent tests succeed
+        * Allow for next current and previous steps in App
+        * Couple of warn cleans in App
+
+2004-11-05  Paul Seamons  <cgi_ex@seamons.com>
+
+        * Version 1.10 is done
+        * Make CGI::Ex::App->print really work with Template
+        * Fix very large javascript variable swapping bug
+        * Numerous upgrades to App
+        * Fix module access in CGI::Ex
+        * Allow validate to populate what_was_validated array
+        * Allow for App to cleanup crossed references
+
+2004-04-23  Paul Seamons  <cgi_ex@seamons.com>
+
+        * Version 1.00 is done
+        * Added set_path method
+        * Added Auth module
+        * Fix validate.js for select-multiple
+        * Fix validate.js for max_values
+        * Add min_in_set and max_in_set types for validate
+        * Add default for validate (sets default value)
+
+2004-03-22  Paul Seamons  <cgi_ex@seamons.com>
+
+        * Version 0.99 is done
+        * Allow swap_template to be called with one argument
+        * Added extended examples in t/samples/cgi_ex_*
+        * Remove dependency on CGI::Util (doesn't exists on some perls)
+        * A few fixes on App
+        * Added set_form
+        * Updated tests
+
+2004-03-19  Paul Seamons  <cgi_ex@seamons.com>
+
+        * Version 0.98 is done
+        * Multiple fixes in Fill module
+        * Updates on perldocs (thanks to Simon Bellwood for bug reports)
+        * Addition of Dump (debug) module
+        * Addition of Die module
+        * Addition of App module
+
+2004-03-05  Paul Seamons  <cgi_ex@seamons.com>
+
+        * Allow for custom_js type
+        * Fix unshift, shift, and push in ie 5.0
+        * Fix type CC in validate.js
+        * Allow for duplicate field definitions
+
+2003-11-26  Paul Seamons  <cgi_ex@seamons.com>
+
+       * Version 0.96 is done
+       * Fix for pos not resetting in CGI::Ex::Fill
+        * Fix for general items set in self not being passed to generate_js
+        * Workaround for yaml_load.js |- not properly trimming whitespace
+
+2003-11-26  Paul Seamons  <cgi_ex@seamons.com>
+
+       * Version 0.94 is done
+       * Javascript functionality is in.
+
+2003-11-01  Paul Seamons  <cgi_ex@seamons.com>
+
+       * Version 0.0 checked in
diff --git a/MANIFEST b/MANIFEST
new file mode 100644 (file)
index 0000000..177b5f4
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,89 @@
+CGI-Ex.spec
+Changes
+lib/CGI/Ex.pm
+lib/CGI/Ex/App.pm
+lib/CGI/Ex/Auth.pm
+lib/CGI/Ex/Conf.pm
+lib/CGI/Ex/Die.pm
+lib/CGI/Ex/Dump.pm
+lib/CGI/Ex/Fill.pm
+lib/CGI/Ex/md5.js
+lib/CGI/Ex/sha1.js
+lib/CGI/Ex/Template.pm
+lib/CGI/Ex/validate.js
+lib/CGI/Ex/Validate.pm
+lib/CGI/Ex/yaml_load.js
+Makefile.PL
+MANIFEST                       This list of files
+MANIFEST.SKIP
+META.yml                       Module meta-data (added by MakeMaker)
+README
+t/0_ex_00_base.t
+t/0_ex_01_swap.t
+t/1_validate_00_base.t
+t/1_validate_01_form.t
+t/1_validate_02_form_fail.t
+t/1_validate_03_cgi.t
+t/1_validate_04_cgi_fail.t
+t/1_validate_05_types.t
+t/1_validate_06_groups.t
+t/1_validate_07_yaml.t
+t/1_validate_08_yaml_file.t
+t/1_validate_09_perl_file.t
+t/1_validate_10_storable_file.t
+t/1_validate_11_no_extra.t
+t/1_validate_12_change.t
+t/1_validate_13_html_file.t
+t/1_validate_14_untaint.t
+t/2_fill_00_base.t
+t/2_fill_01_form.t
+t/2_fill_02_hidden.t
+t/2_fill_03_checkbox.t
+t/2_fill_04_select.t
+t/2_fill_05_textarea.t
+t/2_fill_06_radio.t
+t/2_fill_07_reuse.t
+t/2_fill_08_multiple_objects.t
+t/2_fill_09_default_type.t
+t/2_fill_10_escape.t
+t/2_fill_11_target.t
+t/2_fill_12_mult.t
+t/2_fill_13_warning.t
+t/2_fill_14_password.t
+t/2_fill_15_multiple_fields.t
+t/2_fill_16_ignore_fields.t
+t/2_fill_17_xhtml.t
+t/2_fill_18_coderef.t
+t/2_fill_19_complex.t
+t/2_fill_20_switcharoo.t
+t/3_conf_00_base.t
+t/3_conf_01_write.t
+t/4_app_00_base.t
+t/5_dump_00_base.t
+t/6_die_00_base.t
+t/samples/bench_cgix_hfif.pl
+t/samples/bench_conf_readers.pl
+t/samples/bench_conf_writers.pl
+t/samples/bench_method_calling.pl
+t/samples/cgi_ex_1.cgi
+t/samples/cgi_ex_2.cgi
+t/samples/conf_path_1/apples.pl
+t/samples/conf_path_1/oranges.pl
+t/samples/conf_path_3/apples.pl
+t/samples/conf_path_3/oranges.pl
+t/samples/generate_js.pl
+t/samples/html1.htm
+t/samples/html2.htm
+t/samples/js_validate_1.html
+t/samples/js_validate_2.html
+t/samples/js_validate_3.html
+t/samples/perl1.pl
+t/samples/perl2.pl
+t/samples/storable1.storable
+t/samples/yaml1.val
+t/samples/yaml2.val
+t/samples/yaml3.val
+t/samples/yaml_js_1.html
+t/samples/yaml_js_2.html
+t/samples/yaml_js_3.html
+t/samples/yaml_js_4.html
diff --git a/MANIFEST.SKIP b/MANIFEST.SKIP
new file mode 100644 (file)
index 0000000..94bd9c4
--- /dev/null
@@ -0,0 +1,14 @@
+CVS/
+^tgz/
+\.~$
+\.#
+\w#$
+\.bak$
+Makefile$
+Makefile\.old$
+blib
+\.gz$
+.cvsignore
+tmon\.out
+t/samples/template
+wrap
\ No newline at end of file
diff --git a/META.yml b/META.yml
new file mode 100644 (file)
index 0000000..0bb9c37
--- /dev/null
+++ b/META.yml
@@ -0,0 +1,10 @@
+# http://module-build.sourceforge.net/META-spec.html
+#XXXXXXX This is a prototype!!!  It will change in the future!!! XXXXX#
+name:         CGI-Ex
+version:      1.14
+version_from: lib/CGI/Ex.pm
+installdirs:  site
+requires:
+
+distribution_type: module
+generated_by: ExtUtils::MakeMaker version 6.17
diff --git a/Makefile.PL b/Makefile.PL
new file mode 100644 (file)
index 0000000..8ac2254
--- /dev/null
@@ -0,0 +1,42 @@
+use ExtUtils::MakeMaker;
+
+###----------------------------------------------------------------###
+#  Copyright 2003 - Paul Seamons                                     #
+#  Distributed under the GNU General Public License without warranty #
+###----------------------------------------------------------------###
+
+WriteMakefile(
+              NAME          => "CGI::Ex",
+              AUTHOR        => "Paul Seamons",
+              ABSTRACT_FROM => "lib/CGI/Ex.pm",
+              VERSION_FROM  => "lib/CGI/Ex.pm",
+              INSTALLDIRS   => 'site',
+              
+              dist          => {
+                DIST_DEFAULT => 'all tardist',
+                COMPRESS     => 'gzip -vf',
+                SUFFIX       => '.gz',
+              },
+              
+              clean          => {
+                FILES        => '*~',
+              },
+              
+              realclean         => {
+                FILES        => '*~',
+              },
+              );
+
+package MY;
+
+sub postamble {
+  return qq^
+    
+pm_to_blib: README
+
+README: \$(VERSION_FROM)
+       pod2text \$(VERSION_FROM) > README
+^;
+}
+
+1;
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..8591b87
--- /dev/null
+++ b/README
@@ -0,0 +1,370 @@
+NAME
+    CGI::Ex - CGI utility suite (form getter/filler/validator/app builder)
+
+SYNOPSIS
+      ### CGI Module Extensions
+
+      my $cgix = CGI::Ex->new;
+      my $hashref = $cgix->get_form; # uses CGI by default
+
+      ### send the Content-type header - whether or not we are mod_perl
+      $cgix->print_content_type;
+
+      my $val_hash = $cgix->conf_read($pathtovalidation);
+
+      my $err_obj = $cgix->validate($hashref, $val_hash);
+      if ($err_obj) {
+        my $errors  = $err_obj->as_hash;
+        my $input   = "Some content";
+        my $content = "";
+        SomeTemplateObject->process($input, $errors, $content);
+        $cgix->fill({text => \$content, form => $hashref});
+        print $content;
+        exit;
+      }
+
+      print "Success\n";
+
+      ### Filling functionality
+
+      $cgix->fill({text => \$text, form    => \%hash});
+      $cgix->fill({text => \$text, fdat    => \%hash});
+      $cgix->fill({text => \$text, fobject => $cgiobject});
+      $cgix->fill({text => \$text, form    => [\%hash1, $cgiobject]});
+      $cgix->fill({text => \$text); # uses $self->object as the form
+      $cgix->fill({text          => \$text,
+                     form          => \%hash,
+                     target        => 'formname',
+                     fill_password => 0,
+                     ignore_fields => ['one','two']});
+      $cgix->fill(\$text); # uses $self->object as the form
+      $cgix->fill(\$text, \%hash, 'formname', 0, ['one','two']);
+      my $copy = $cgix->fill({scalarref => \$text,    fdat => \%hash});
+      my $copy = $cgix->fill({arrayref  => \@lines,   fdat => \%hash});
+      my $copy = $cgix->fill({file      => $filename, fdat => \%hash});
+
+      ### Validation functionality
+
+      my $err_obj = $cgix->validate($form, $val_hash);
+      my $err_obj = $cgix->validate($form, $path_to_validation);
+      my $err_obj = $cgix->validate($form, $yaml_string);
+
+      ### get errors separated by key name
+      ### useful for inline errors
+      my $hash = $err_obj->as_hash;
+      my %hash = $err_obj->as_hash;
+
+      ### get aggregate list of errors
+      ### useful for central error description
+      my $array = $err_obj->as_array;
+      my @array = $err_obj->as_array;
+
+      ### get a string
+      ### useful for central error description
+      my $string = $err_obj->as_string;
+      my $string = "$err_obj";
+
+      $cgix->{raise_error} = 1;
+      $cgix->validate($form, $val_hash);
+        # SAME AS #
+      my $err_obj = $cgix->validate($form, $val_hash);
+      die $err_obj if $err_obj;
+
+      ### Settings functionality
+
+      ### read file via yaml
+      my $ref = $cgix->conf_read('/full/path/to/conf.yaml');
+
+      ### merge all found settings.pl files together
+      @CGI::Ex::Conf::DEFAULT_PATHS = qw(/tmp /my/data/dir /home/foo);
+      @CGI::Ex::Conf::DIRECTIVE     = 'MERGE';
+      @CGI::Ex::Conf::DEFAULT_EXT   = 'pl';
+      my $ref = $cgix->conf_read('settings');
+
+DESCRIPTION
+    CGI::Ex provides a suite of utilities to make writing CGI scripts more
+    enjoyable. Although they can all be used separately, the main
+    functionality of each of the modules is best represented in the
+    CGI::Ex::App module. CGI::Ex::App takes CGI application building to the
+    next step. CGI::Ex::App is not a framework (which normally includes
+    prebuilt html) instead CGI::Ex::App is an extended application flow that
+    normally dramatically reduces CGI build time. See CGI::Ex::App.
+
+    CGI::Ex is another form filler / validator / conf reader / template
+    interface. Its goal is to take the wide scope of validators and other
+    useful CGI application modules out there and merge them into one utility
+    that has all of the necessary features of them all, as well as several
+    extended methods that I have found useful in working on the web.
+
+    The main functionality is provided by several other modules that may be
+    used separately, or together through the CGI::Ex interface.
+
+    "CGI::Ex::Fill"
+        A regular expression based form filler inner (accessed through
+        ->fill or directly via its own functions). Can be a drop in
+        replacement for HTML::FillInForm. See CGI::Ex::Fill for more
+        information.
+
+    "CGI::Ex::Validate"
+        A form field / cgi parameter / any parameter validator (accessed
+        through ->validate or directly via its own methods). Not quite a
+        drop in for most validators, although it has most of the
+        functionality of most of the validators but with the key additions
+        of conditional validation. Has a tightly integrated JavaScript
+        portion that allows for duplicate client side validation. See
+        CGI::Ex::Validate for more information.
+
+    "CGI::Ex::Conf"
+        A general use configuration, or settings, or key / value file
+        reader. Has ability for providing key fallback as well as immutable
+        key definitions. Has default support for yaml, storable, perl, ini,
+        and xml and open architecture for definition of others. See
+        CGI::Ex::Conf for more information.
+
+METHODS
+    "->fill"
+        fill is used for filling hash or cgi object values into an existing
+        html document (it doesn't deal at all with how you got the
+        document). Arguments may be given as a hash, or a hashref or
+        positional. Some of the following arguments will only work using
+        CGI::Ex::Fill - most will work with either CGI::Ex::Fill or
+        HTML::FillInForm (assume they are available unless specified
+        otherwise). (See CGI::Ex::Fill for a full explanation of
+        functionality). The arguments to fill are as follows (and in order
+        of position):
+
+        "text"
+            Text should be a reference to a scalar string containing the
+            html to be modified (actually it could be any reference or
+            object reference that can be modfied as a string). It will be
+            modified in place. Another named argument scalarref is available
+            if you would like to copy rather than modify.
+
+        "form"
+            Form may be a hashref, a cgi style object, a coderef, or an
+            array of multiple hashrefs, cgi objects, and coderefs. Hashes
+            should be key value pairs. CGI objects should be able to call
+            the method param (This can be overrided). Coderefs should expect
+            expect the field name as an argument and should return a value.
+            Values returned by form may be undef, scalar, arrayref, or
+            coderef (coderef values should expect an argument of field name
+            and should return a value). The code ref options are available
+            to delay or add options to the bringing in of form informatin -
+            without having to tie the hash. Coderefs are not available in
+            HTML::FillInForm. Also HTML::FillInForm only allows CGI objects
+            if an arrayref is used.
+
+            NOTE: Only one of the form, fdat, and fobject arguments are
+            allowed at a time.
+
+        "target"
+            The name of the form that the fields should be filled to. The
+            default value of undef, means to fill in all forms in the html.
+
+        "fill_passwords"
+            Boolean value defaults to 1. If set to zero - password fields
+            will not be filled.
+
+        "ignore_fields"
+            Specify which fields to not fill in. It takes either array ref
+            of names, or a hashref with the names as keys. The hashref
+            option is not available in CGI::Ex::Fill.
+
+        Other named arguments are available for compatiblity with
+        HTML::FillInForm. They may only be used as named arguments.
+
+        "scalarref"
+            Almost the same as the argument text. If scalarref is used, the
+            filled html will be returned. If text is used the html passed is
+            filled in place.
+
+        "arrayref"
+            An array ref of lines of the document. Forces a returned filled
+            html document.
+
+        "file"
+            An filename that will be opened, filled, and returned.
+
+        "fdat"
+            A hashref of key value pairs.
+
+        "fobject"
+            A cgi style object or arrayref of cgi style objects used for
+            getting the key value pairs. Should be capable of the ->param
+            method and ->cookie method as document in CGI.
+
+        See CGI::Ex::Fill for more information about the filling process.
+
+    "->object"
+        Returns the CGI object that is currently being used by CGI::Ex. If
+        none has been set it will automatically generate an object of type
+        $PREFERRED_CGI_MODULE which defaults to CGI.
+
+    "->validate"
+        Validate has a wide range of options available. (See
+        CGI::Ex::Validate for a full explanation of functionality). Validate
+        has two arguments:
+
+        "form"
+            Can be either a hashref to be validated, or a CGI style object
+            (which has the param method).
+
+        "val_hash"
+            The val_hash can be one of three items. First, it can be a
+            straight perl hashref containing the validation to be done.
+            Second, it can be a YAML document string. Third, it can be the
+            path to a file containing the validation. The validation in a
+            validation file will be read in depending upon file extension.
+
+    "->get_form"
+        Very similar to CGI->new->Vars except that arrays are returned as
+        arrays. Not sure why CGI::Val didn't do this anyway (well - yes -
+        legacy Perl 4 - but at some point things need to be updated).
+
+    "->set_form"
+        Allow for setting a custom form hash. Useful for testing, or other
+        purposes.
+
+    "->get_cookies"
+        Returns a hash of all cookies.
+
+    "->make_form"
+        Takes a hash and returns a query_string. A second optional argument
+        may contain an arrayref of keys to use from the hash in building the
+        query_string. First argument is undef, it will use the form stored
+        in itself as the hash.
+
+    "->content_type"
+        Can be called multiple times during the same session. Will only
+        print content-type once. (Useful if you don't know if something else
+        already printed content-type). Calling this sends the Content-type
+        header. Trying to print ->content_type is an error. For clarity, the
+        method ->print_content_type is available.
+
+    "->set_cookie"
+        Arguments are the same as those to CGI->new->cookie({}). Uses CGI's
+        cookie method to create a cookie, but then, depending on if content
+        has already been sent to the browser will either print a Set-cookie
+        header, or will add a <meta http-equiv='set-cookie'> tag (this is
+        supported on most major browsers). This is useful if you don't know
+        if something else already printed content-type.
+
+    "->location_bounce"
+        Depending on if content has already been sent to the browser will
+        either print a Location header, or will add a <meta
+        http-equiv='refresh'> tag (this is supported on all major browsers).
+        This is useful if you don't know if something else already printed
+        content-type. Takes single argument of a url.
+
+    "->last_modified"
+        Depending on if content has already been sent to the browser will
+        either print a Last-Modified header, or will add a <meta
+        http-equiv='Last-Modified'> tag (this is supported on most major
+        browsers). This is useful if you don't know if something else
+        already printed content-type. Takes an argument of either a time
+        (may be a CGI -expires style time) or a filename.
+
+    "->expires"
+        Depending on if content has already been sent to the browser will
+        either print a Expires header, or will add a <meta
+        http-equiv='Expires'> tag (this is supported on most major
+        browsers). This is useful if you don't know if something else
+        already printed content-type. Takes an argument of a time (may be a
+        CGI -expires style time).
+
+    "->send_status"
+        Send a custom status. Works in both CGI and mod_perl. Arguments are
+        a status code and the content (optional).
+
+    "->send_header"
+        Send a http header. Works in both CGI and mod_perl. Arguments are a
+        header name and the value for that header.
+
+    "->print_js"
+        Prints out a javascript file. Does everything it can to make sure
+        that the javascript will cache. Takes either a full filename, or a
+        shortened name which will be looked for in @INC. (ie
+        /full/path/to/my.js or CGI/Ex/validate.js or CGI::Ex::validate)
+
+    "->swap_template"
+        This is intended as a simple yet strong subroutine to swap in tags
+        to a document. It is intended to be very basic for those who may not
+        want the full features of a Templating system such as
+        Template::Toolkit (even though they should investigate them because
+        they are pretty nice). The default allows for basic template toolkit
+        variable swapping. There are two arguments. First is a string or a
+        reference to a string. If a string is passed, a copy of that string
+        is swapped and returned. If a reference to a string is passed, it is
+        modified in place. The second argument is a form, or a CGI object,
+        or a cgiex object, or a coderef (if the second argument is missing,
+        the cgiex object which called the method will be used). If it is a
+        coderef, it should accept key as its only argument and return the
+        proper value.
+
+          my $cgix = CGI::Ex->new;
+          my $form = {foo  => 'bar',
+                      this => {is => {nested => ['wow', 'wee']}}
+                     };
+
+          my $str =  $cgix->swap_template("<html>[% foo %]<br>[% foo %]</html>", $form));
+          # $str eq '<html>bar<br>bar</html>'
+
+          $str = $cgix->swap_template("[% this.is.nested.1 %]", $form));
+          # $str eq 'wee'
+
+          $str = "[% this.is.nested.0 %]";
+          $cgix->swap_template(\$str, $form);
+          # $str eq 'wow'
+
+          # may also be called with only one argument as follows:
+          # assuming $cgix had a query string of ?foo=bar&baz=wow&this=wee
+          $str = "<html>([% foo %]) <br>
+                  ([% baz %]) <br>
+                  ([% this %]) </html>";
+          $cgix->swap_template(\$str);
+          #$str eq "<html>(bar) <br>
+          #        (wow) <br>
+          #        (wee) </html>";
+  
+        For further examples, please see the code contained in
+        t/samples/cgi_ex_* of this distribution.
+
+        If at a later date, the developer upgrades to Template::Toolkit, the
+        templates that were being swapped by CGI::Ex::swap_template should
+        be compatible with Template::Toolkit.
+
+EXISTING MODULES
+    The following is a list of existing validator and formfiller modules at
+    the time of this writing (I'm sure this probably isn't exaustive).
+
+    "Email::Valid" - Validator
+    "SSN::Validate" - Validator
+    "Embperl::Form::Validate" - Validator
+    "Data::CGIForm" - Validator
+    "HTML::FillInForm" - Form filler-iner
+    "CGI" - CGI Getter. Form filler-iner
+
+TODO
+        Add an integrated template toolkit interface.
+
+        Add an integrated debug module.
+
+MODULES
+        See also CGI::Ex::Fill.
+
+        See also CGI::Ex::Validate.
+
+        See also CGI::Ex::Conf.
+
+        See also CGI::Ex::Die.
+
+        See also CGI::Ex::App.
+
+        See also CGI::Ex::Dump.
+
+AUTHOR
+        Paul Seamons
+
+LICENSE
+        This module may be distributed under the same terms as Perl itself.
+
diff --git a/lib/CGI/Ex.pm b/lib/CGI/Ex.pm
new file mode 100644 (file)
index 0000000..cbc5a34
--- /dev/null
@@ -0,0 +1,1176 @@
+package CGI::Ex;
+
+### CGI Extended
+
+###----------------------------------------------------------------###
+#  Copyright 2003 - Paul Seamons                                     #
+#  Distributed under the Perl Artistic License without warranty      #
+###----------------------------------------------------------------###
+
+### See perldoc at bottom
+
+use strict;
+use vars qw($VERSION
+            $PREFERRED_FILL_MODULE
+            $PREFERRED_CGI_MODULE
+            $PREFERRED_CGI_REQUIRED
+            $TEMPLATE_OPEN
+            $TEMPLATE_CLOSE
+            $AUTOLOAD
+            $DEBUG_LOCATION_BOUNCE
+            @EXPORT @EXPORT_OK
+            );
+use base qw(Exporter);
+
+$VERSION               = '1.14';
+$PREFERRED_FILL_MODULE ||= '';
+$PREFERRED_CGI_MODULE  ||= 'CGI';
+$TEMPLATE_OPEN         ||= qr/\[%\s*/;
+$TEMPLATE_CLOSE        ||= qr/\s*%\]/;
+@EXPORT = ();
+@EXPORT_OK = qw(get_form
+                get_cookies
+                print_content_type
+                content_type
+                content_typed
+                set_cookie
+                );
+
+###----------------------------------------------------------------###
+
+#   my $cgix = CGI::Ex->new;
+sub new {
+  my $class = shift || die "Missing class name";
+  my $self  = ref($_[0]) ? shift : {@_};
+  return bless $self, $class;
+}
+
+### allow for holding another classed CGI style object
+#   my $query = $cgix->object;
+#   $cgix->object(CGI->new);
+sub object {
+  my $self = shift;
+  die 'Usage: my $query = $cgix_obj->object' if ! ref $self;
+  return $self->{'object'} = shift if $#_ != -1;
+  return $self->{'object'} ||= do {
+    $PREFERRED_CGI_REQUIRED ||= do {
+      my $file = $self->{'cgi_module'} || $PREFERRED_CGI_MODULE;
+      $file .= ".pm";
+      $file =~ s|::|/|g;
+      eval { require $file };
+      die "Couldn't require $PREFERRED_CGI_MODULE: $@" if $@;
+      1; # return of inner do
+    };
+    $PREFERRED_CGI_MODULE->new; # return of the do
+  };
+}
+
+### allow for calling their methods
+sub AUTOLOAD {
+  my $self = shift;
+  my $meth = ($AUTOLOAD =~ /(\w+)$/) ? $1 : die "Invalid method $AUTOLOAD";
+  return wantarray # does wantarray propogate up ?
+    ? ($self->object->$meth(@_))
+    :  $self->object->$meth(@_);
+}
+
+sub DESTROY {}
+
+###----------------------------------------------------------------###
+
+### Form getter that will act like CGI->new->Vars only it will return arrayrefs
+### for values that are arrays
+#   my $hash = $cgix->get_form;
+#   my $hash = $cgix->get_form(CGI->new);
+#   my $hash = get_form();
+#   my $hash = get_form(CGI->new);
+sub get_form {
+  my $self = shift;
+  $self = __PACKAGE__->new if ! $self;
+  die 'Usage: $cgix_obj->get_form' if ! ref $self;
+  if (! UNIVERSAL::isa($self, __PACKAGE__)) { # get_form(CGI->new) syntax
+    my $obj = $self;
+    $self = __PACKAGE__->new;
+    $self->object($obj);
+  }
+  return $self->{'form'} if $self->{'form'};
+
+  ### get the info out of the object
+  my $obj  = shift || $self->object;
+  my %hash = ();
+  foreach my $key ($obj->param) {
+    my @val = $obj->param($key);
+    $hash{$key} = ($#val == -1) ? die : ($#val == 0) ? $val[0] : \@val;
+  }
+  return $self->{'form'} = \%hash;
+}
+
+### allow for a setter
+### $cgix->set_form(\%form);
+sub set_form {
+  my $self = shift;
+  die 'Usage: $cgix_obj->set_form(\%form)' if ! ref $self;
+  $self->{'form'} = shift || {};
+}
+
+### Combined get and set form
+#   my $hash = $cgix->form;
+#   $cgix->form(\%form);
+sub form {
+  my $self = shift;
+  die (defined wantarray
+       ? 'Usage: my $form = $cgix_obj->form' : 'Usage: $cgix_obj->form(\%form)')
+    if ! UNIVERSAL::isa($self, __PACKAGE__);
+  return $self->set_form(shift) if $#_ != -1;
+  return $self->get_form;
+}
+
+### allow for creating a url encoded key value sequence
+#   my $str = $cgix->make_form(\%form);
+#   my $str = $cgix->make_form(\%form, \@keys_to_include);
+sub make_form {
+  my $self = shift;
+  die 'Usage: $cgix_obj->make_form(\%form)' if ! ref $self;
+  my $form = shift || $self->get_form;
+  my $keys = ref($_[0]) ? shift : [sort keys %$form];
+  my $str = '';
+  foreach (@$keys) {
+    my $key = $_; # make a copy
+    my $val = $form->{$key};
+    $key =~ s/([^\w.\-\ ])/sprintf('%%%02X', ord $1)/eg;
+    $key =~ y/ /+/;
+    foreach (ref($val) ? @$val : $val) {
+      my $_val = $_; # make a copy
+      $_val =~ s/([^\w.\-\ ])/sprintf('%%%02X', ord $1)/eg;
+      $_val =~ y/ /+/;
+      $str .= "$key=$_val&"; # intentionally not using join
+    }
+  }
+  chop $str;
+  return $str;
+}
+
+###----------------------------------------------------------------###
+
+### like get_form - but a hashref of cookies
+### cookies are parsed depending upon the functionality of ->cookie
+#   my $hash = $cgix->get_cookies;
+#   my $hash = $cgix->get_cookies(CGI->new);
+#   my $hash = get_cookies();
+#   my $hash = get_cookies(CGI->new);
+sub get_cookies {
+  my $self = shift;
+  $self = __PACKAGE__->new if ! $self;
+  die 'Usage: $cgix_obj->get_cookies' if ! ref $self;
+  if (! UNIVERSAL::isa($self, __PACKAGE__)) { # get_cookies(CGI->new) syntax
+    my $obj = $self;
+    $self = __PACKAGE__->new;
+    $self->object($obj);
+  }
+  return $self->{'cookies'} if $self->{'cookies'};
+
+  my $obj  = shift || $self->object;
+  use CGI::Ex::Dump qw(debug);
+  my %hash = ();
+  foreach my $key ($obj->cookie) {
+    my @val = $obj->cookie($key);
+    $hash{$key} = ($#val == -1) ? next : ($#val == 0) ? $val[0] : \@val;
+  }
+  return $self->{'cookies'} = \%hash;
+}
+
+### Allow for a setter
+### $cgix->set_cookies(\%cookies);
+sub set_cookies {
+  my $self = shift;
+  die 'Usage: $cgix_obj->set_cookies(\%cookies)' if ! ref $self;
+  $self->{'cookies'} = shift || {};
+}
+
+### Combined get and set cookies
+#   my $hash = $cgix->cookies;
+#   $cgix->cookies(\%cookies);
+sub cookies {
+  my $self = shift;
+  die (defined wantarray
+       ? 'Usage: my $hash = $cgix_obj->cookies' : 'Usage: $cgix_obj->cookies(\%cookies)')
+    if ! UNIVERSAL::isa($self, __PACKAGE__);
+  return $self->set_cookies(shift) if $#_ != -1;
+  return $self->get_cookies;
+}
+
+###----------------------------------------------------------------###
+
+### Allow for shared apache request object
+#   my $r = $cgix->apache_request
+#   $cgix->apache_request($r);
+sub apache_request {
+  my $self = shift;
+  die 'Usage: $cgix_obj->apache_request' if ! ref $self;
+  $self->{'apache_request'} = shift if $#_ != -1;
+  if (! defined $self->{'apache_request'}) {
+    return if ! $self->mod_perl_version;
+    $self->{'apache_request'} = Apache->request;
+  }
+  return $self->{'apache_request'};
+}
+
+### Get the version of mod_perl running (0 if not mod_perl)
+#   my $version = $cgix->mod_perl_version;
+sub mod_perl_version {
+  my $self = shift;
+  die 'Usage: $cgix_obj->mod_perl_version' if ! ref $self;
+  if (! defined $self->{'mod_perl_version'}) {
+    return 0 if ! $ENV{'MOD_PERL'};
+    # mod_perl/1.27 or mod_perl/1.99_16
+    # if MOD_PERL is set - don't die if regex fails - just assume 1.0
+    $self->{'mod_perl_version'} = ($ENV{'MOD_PERL'} =~ m|^mod_perl/(\d+\.[\d_]+)$|)
+      ? $1 : '1.0_0';
+  }
+  return $self->{'mod_perl_version'};
+}
+
+sub is_mod_perl_1 { shift->mod_perl_version <  1.98 }
+sub is_mod_perl_2 { shift->mod_perl_version >= 1.98 }
+
+### Allow for a setter
+#   $cgix->set_apache_request($r)
+sub set_apache_request { shift->apache_request(shift) }
+
+###----------------------------------------------------------------###
+
+### same signature as print_content_type
+sub content_type {
+  &print_content_type;
+}
+
+### will send the Content-type header
+#   $cgix->print_content_type;
+#   $cgix->print_content_type('text/plain');
+#   print_content_type();
+#   print_content_type('text/plain);
+sub print_content_type {
+  my ($self, $type) = ($#_ >= 1) ? @_ : ref($_[0]) ? (shift, undef) : (undef, shift);
+  $self = __PACKAGE__->new if ! $self;
+  die 'Usage: $cgix_obj->print_content_type' if ! ref $self;
+  if ($type) {
+    die "Invalid type: $type" if $type !~ m|^[\w\-\.]+/[\w\-\.\+]+$|; # image/vid.x-foo
+  } else {
+    $type = 'text/html';
+  }
+
+  if (my $r = $self->apache_request) {
+    return if $r->bytes_sent;
+    $r->content_type($type);
+    $r->send_http_header if $self->is_mod_perl_1;
+  } else {
+    if (! $ENV{'CONTENT_TYPED'}) {
+      print "Content-Type: $type\r\n\r\n";
+      $ENV{'CONTENT_TYPED'} = '';
+    }
+    $ENV{'CONTENT_TYPED'} .= sprintf("%s, %d\n", (caller)[1,2]);
+  }
+}
+
+### Boolean check if content has been typed
+#   $cgix->content_typed;
+#   content_typed();
+sub content_typed {
+  my $self = shift;
+  $self = __PACKAGE__->new if ! $self;
+  die 'Usage: $cgix_obj->content_typed' if ! ref $self;
+
+  if (my $r = $self->apache_request) {
+    return $r->bytes_sent;
+  } else {
+    return ($ENV{'CONTENT_TYPED'}) ? 1 : undef;
+  }
+}
+
+###----------------------------------------------------------------###
+
+### location bounce nicely - even if we have already sent content
+### may be called as function or a method
+#   $cgix->location_bounce($url);
+#   location_bounce($url);
+sub location_bounce {
+  my ($self, $loc) = ($#_ == 1) ? (@_) : (undef, shift);
+  $self = __PACKAGE__->new if ! $self;
+  die 'Usage: $cgix_obj->location_bounce($url)' if ! ref $self;
+
+  if ($self->content_typed) {
+    if ($DEBUG_LOCATION_BOUNCE) {
+      print "<a class=debug href=\"$loc\">Location: $loc</a><br />\n";
+    } else {
+      print "<meta http-equiv=\"refresh\" content=\"0;url=$loc\" />\n";
+    }
+  } else {
+    if (my $r = $self->apache_request) {
+      $r->status(302);
+      if ($self->is_mod_perl_1) {
+        $r->header_out("Location", $loc);
+        $r->content_type('text/html');
+        $r->send_http_header;
+        $r->print("Bounced to $loc\n");
+      } else {
+        my $t = $r->headers_out;
+        $t->add("Location", $loc);
+        $r->headers_out($t);
+      }
+    } else {
+      print "Location: $loc\r\n",
+            "Status: 302 Bounce\r\n",
+            "Content-Type: text/html\r\n\r\n",
+            "Bounced to $loc\r\n";
+    }
+  }
+}
+
+### set a cookie nicely - even if we have already sent content
+### may be called as function or a method - fancy algo to allow for first argument of args hash
+#   $cgix->set_cookie({name => $name, ...});
+#   $cgix->set_cookie( name => $name, ... );
+#   set_cookie({name => $name, ...});
+#   set_cookie( name => $name, ... );
+sub set_cookie {
+  my $self = UNIVERSAL::isa($_[0], __PACKAGE__) ? shift : __PACKAGE__->new;
+  my $args = ref($_[0]) ? shift : {@_};
+  foreach (keys %$args) {
+    next if /^-/;
+    $args->{"-$_"} = delete $args->{$_};
+  }
+
+  ### default path to / and allow for 1hour instead of 1h
+  $args->{-path} ||= '/';
+  $args->{-expires} = time_calc($args->{-expires}) if $args->{-expires};
+
+  my $obj    = $self->object;
+  my $cookie = "" . $obj->cookie(%$args);
+
+  if ($self->content_typed) {
+    print "<meta http-equiv=\"Set-Cookie\" content=\"$cookie\" />\n";
+  } else {
+    if (my $r = $self->apache_request) {
+      if ($self->is_mod_perl_1) {
+        $r->header_out("Set-cookie", $cookie);
+      } else {
+        my $t = $r->headers_out;
+        $t->add("Set-Cookie", $cookie);
+        $r->headers_out($t);
+      }
+    } else {
+      print "Set-Cookie: $cookie\r\n"
+    }
+  }
+}
+
+### print the last modified time
+### takes a time or filename and an optional keyname
+#   $cgix->last_modified; # now
+#   $cgix->last_modified((stat $file)[9]); # file's time
+#   $cgix->last_modified(time, 'Expires'); # different header
+#   last_modified(); # now
+#   last_modified((stat $file)[9]); # file's time
+#   last_modified(time, 'Expires'); # different header
+sub last_modified {
+  my $self = ref($_[0]) ? shift : __PACKAGE__; # may be called as function or method
+  $self = $self->new if ! ref $self;
+  my $time = shift || time;
+  my $key  = shift || 'Last-Modified';
+
+  ### get a time string - looks like:
+  ### Mon Dec  9 18:03:21 2002
+  ### valid RFC (although not prefered)
+  $time = scalar gmtime time_calc($time);
+
+  if ($self->content_typed) {
+    print "<meta http-equiv=\"$key\" content=\"$time\" />\n";
+  } else {
+    if (my $r = $self->apache_request) {
+      if ($self->is_mod_perl_1) {
+        $r->header_out($key, $time);
+      } else {
+        my $t = $r->headers_out;
+        $t->add($key, $time);
+        $r->headers_out($t);
+      }
+    } else {
+      print "$key: $time\r\n"
+    }
+  }
+
+}
+
+### add expires header
+sub expires {
+  my $self = ref($_[0]) ? shift : __PACKAGE__; # may be called as a function or method
+  my $time = shift || time;
+  return $self->last_modified($time, 'Expires');
+}
+
+### similar to expires_calc from CGI::Util
+### allows for lenient calling, hour instead of just h, etc
+### takes time or 0 or now or filename or types of -23minutes 
+sub time_calc {
+  my $time = shift; # may only be called as a function
+  if (! $time || lc($time) eq 'now') {
+    return time;
+  } elsif ($time =~ m/^\d+$/) {
+    return $time;
+  } elsif ($time =~ m/^([+-]?)\s*(\d+|\d*\.\d+)\s*([a-z])[a-z]*$/i) {
+    my $m = {
+      's' => 1,
+      'm' => 60,
+      'h' => 60 * 60,
+      'd' => 60 * 60 * 24,
+      'w' => 60 * 60 * 24 * 7,
+      'M' => 60 * 60 * 24 * 30,
+      'y' => 60 * 60 * 24 * 365,
+    };
+    return time + ($m->{lc($3)} || 1) * "$1$2";
+  } else {
+    my @stat = stat $time;
+    die "Could not find file \"$time\" for time_calc" if $#stat == -1;
+    return $stat[9];
+  }
+}
+
+
+### allow for generic status send
+sub send_status {
+  my $self = ref($_[0]) ? shift : __PACKAGE__; # may be called as function or method
+  my $code = shift || die "Missing status";
+  my $mesg = shift;
+  if (! defined $mesg) {
+    $mesg = "HTTP Status of $code received\n";
+  }
+  if ($self->content_typed) {
+    die "Cannot send a status ($code - $mesg) after content has been sent";
+  }
+  if (my $r = $self->apache_request) {
+    $r->status($code);
+    if ($self->is_mod_perl_1) {
+      $r->content_type('text/html');
+      $r->send_http_header;
+      $r->print($mesg);
+    } else {
+      # not sure of best way to send the message in MP2
+    }
+  } else {
+    print "Status: $code\r\n";
+    $self->print_content_type;
+    print $mesg;
+  }
+}
+
+### allow for sending a simple header
+sub send_header {
+  my $self = ref($_[0]) ? shift : __PACKAGE__; # may be called as function or method
+  my $key  = shift;
+  my $value = shift;
+  if ($self->content_typed) {
+    die "Cannot send a header ($key - $value) after content has been sent";
+  }
+  if (my $r = $self->apache_request) {
+    if ($self->is_mod_perl_1) {
+      $r->header_out($key, $value);
+    } else {
+      my $t = $r->headers_out;
+      $t->add($key, $value);
+      $r->headers_out($t);
+    }
+  } else {
+    print "$key: $value\r\n";
+  }
+}
+
+###----------------------------------------------------------------###
+
+### allow for printing out a static javascript file
+### for example $self->print_js("CGI::Ex::validate.js");
+sub print_js {
+  my ($self, $js_file) = ($#_ == 1) ? (@_) : (__PACKAGE__, shift);
+  $self = $self->new if ! ref $self;
+
+  ### fix up the file - force .js on the end
+  $js_file .= '.js' if $js_file && $js_file !~ /\.js$/i;
+  $js_file =~ s|::|/|g;
+
+  ### get file info
+  my $stat;
+  if (! $js_file) {
+    # do nothing - give the 404
+  } elsif ($js_file !~ m|^\.{0,2}/|) {
+    foreach my $path (@INC) {
+      my $_file = "$path/$js_file";
+      next if ! -f $_file;
+      $js_file = $_file;
+      $stat = [stat _];
+      last;
+    }
+  } else {
+    if (-f $js_file) {
+      $stat = [stat _];
+    }
+  }
+
+  ### no - file - 404
+  if (! $stat) {
+    if (! $self->content_typed) {
+      $self->send_status(404, "JS File not found for print_js\n");
+    } else {
+      print "<h1>JS File not found for print_js</h1>\n";
+    }
+
+    return;
+  }
+
+  ### do headers
+  if (! $self->content_typed) {
+    $self->last_modified($stat->[9]);
+    $self->expires('+ 1 year');
+    $self->print_content_type('application/x-javascript');
+  }
+
+  return if $ENV{REQUEST_METHOD} && $ENV{REQUEST_METHOD} eq 'HEAD';
+
+  ### send the contents
+  if (open IN, $js_file) {
+    local $/ = undef;
+    print <IN>;
+    close IN;
+  } else {
+    die "Couldn't open file  $js_file: $!";
+  }
+}
+
+###----------------------------------------------------------------###
+
+### form filler that will use either HTML::FillInForm, CGI::Ex::Fill
+### or another specified filler.  Argument style is similar to
+### HTML::FillInForm.  May be called as a method or a function.
+sub fill {
+  my $self = shift;
+  my $args = shift;
+  if (ref($args)) {
+    if (! UNIVERSAL::isa($args, 'HASH')) {
+      $args = {text => $args};
+      @$args{'form','target','fill_password','ignore_fields'} = @_;
+    }
+  } else {
+    $args = {$args, @_};
+  }
+
+  my $module = $self->{fill_module} || $PREFERRED_FILL_MODULE;
+
+  ### allow for using the standard HTML::FillInForm
+  ### too bad it won't modify our file in place for us
+  if ($module eq 'HTML::FillInForm') {
+    eval { require HTML::FillInForm };
+    if ($@) {
+      die "Couldn't require HTML::FillInForm: $@";
+    }
+    $args->{scalarref} = $args->{text} if $args->{text};
+    $args->{fdat}      = $args->{form} if $args->{form};
+    my $filled = HTML::FillInForm->new->fill(%$args);
+    if ($args->{text}) {
+      my $ref = $args->{text};
+      $$ref = $filled;
+      return 1;
+    }
+    return $filled;
+
+  ### allow for some other type - for whatever reason
+  } elsif ($module) {
+    my $file = $module;
+    $file .= '.pm' if $file !~ /\.\w+$/;
+    $file =~ s|::|/|g;
+    eval { require $file };
+    if ($@) {
+      die "Couldn't require $module: $@";
+    }
+    return $module->new->fill(%$args);
+
+  ### well - we will use our own then
+  } else {
+    require CGI::Ex::Fill;
+
+    ### get the text to work on
+    my $ref;
+    if ($args->{text}) {           # preferred method - gets modified in place
+      $ref = $args->{text};
+    } elsif ($args->{scalarref}) { # copy to mimic HTML::FillInForm
+      my $str = ${ $args->{scalarref} };
+      $ref = \$str;
+    } elsif ($args->{arrayref}) {  # joined together (copy)
+      my $str = join "", @{ $args->{arrayref} };
+      $ref = \$str;
+    } elsif ($args->{file}) {      # read it in
+      open (IN, $args->{file}) || die "Couldn't open $args->{file}: $!";
+      my $str = '';
+      read(IN, $str, -s _) || die "Couldn't read $args->{file}: $!";
+      close IN;
+      $ref = \$str;
+    } else {
+      die "No suitable text found for fill.";
+    }
+
+    ### allow for data to be passed many ways
+    my $form = $args->{form} || $args->{fobject}
+      || $args->{fdat} || $self->object;
+    
+    &CGI::Ex::Fill::form_fill($ref,
+                              $form,
+                              $args->{target},
+                              $args->{fill_password},
+                              $args->{ignore_fields},
+                              );
+    return ! $args->{text} ? $$ref : 1;
+  }
+
+}
+
+###----------------------------------------------------------------###
+
+sub validate {
+  my $self = shift || die "Sub \"validate\" must be called as a method";
+  my ($form, $file) = (@_ == 2) ? (shift, shift) : ($self->object, shift);
+
+  require CGI::Ex::Validate;
+
+  my $args = {};
+  $args->{raise_error} = 1 if $self->{raise_error};
+  return CGI::Ex::Validate->new($args)->validate($form, $file);
+}
+
+###----------------------------------------------------------------###
+
+sub conf_obj {
+  my $self = shift || die "Sub \"conf_obj\" must be called as a method";
+  return $self->{conf_obj} ||= do {
+    require CGI::Ex::Conf;
+    CGI::Ex::Conf->new(@_);
+  };
+}
+
+sub conf_read {
+  my $self = shift || die "Sub \"conf_read\" must be called as a method";
+  return $self->conf_obj->read(@_);
+}
+
+###----------------------------------------------------------------###
+
+### This is intended as a simple yet strong subroutine to swap
+### in tags to a document.  It is intended to be very basic
+### for those who may not want the full features of a Templating
+### system such as Template::Toolkit (even though they should
+### investigate them because they are pretty nice)
+sub swap_template {
+  my $self = shift || die "Sub \"swap_template\" must be called as a method";
+  my $str  = shift;
+  return $str if ! $str;
+  my $ref  = ref($str) ? $str : \$str;
+
+  ### basic - allow for passing a hash, or object, or code ref
+  my $form = shift;
+  $form = $self if ! $form && ref($self);
+  $form = $self->get_form() if UNIVERSAL::isa($form, __PACKAGE__);
+
+  my $get_form_value;
+  if (UNIVERSAL::isa($form, 'HASH')) {
+    $get_form_value = sub {
+      my $key = shift;
+      return defined($form->{$key}) ? $form->{$key} : '';
+    };
+  } elsif (my $meth = UNIVERSAL::can($form, 'param')) {
+    $get_form_value = sub {
+      my $key = shift;
+      my $val = $form->$meth($key);
+      return defined($val) ? $val : '';
+    };
+  } elsif (UNIVERSAL::isa($form, 'CODE')) {
+    $get_form_value = sub {
+      my $key = shift;
+      my $val = &{ $form }($key);
+      return defined($val) ? $val : '';
+    };
+  } else {
+    die "Not sure how to use $form passed to swap_template_tags";
+  }
+
+  ### now do the swap
+  $$ref =~ s{$TEMPLATE_OPEN \b (\w+) ((?:\.\w+)*) \b $TEMPLATE_CLOSE}{
+    if (! $2) {
+      &$get_form_value($1);
+    } else {
+      my @extra = split(/\./, substr($2,1));
+      my $ref   = &$get_form_value($1);
+      my $val;
+      while (defined(my $key = shift(@extra))) {
+        if (UNIVERSAL::isa($ref, 'HASH')) {
+          if (! exists($ref->{$key}) || ! defined($ref->{$key})) {
+            $val = '';
+            last;
+          }
+          $ref = $ref->{$key};
+        } elsif (UNIVERSAL::isa($ref, 'ARRAY')) {
+          if (! exists($ref->[$key]) || ! defined($ref->[$key])) {
+            $val = '';
+            last;
+          }
+          $ref = $ref->[$key];
+        } else {
+          $val = '';
+          last;
+        }
+      }
+      if (! defined($val)) {
+        if ($#extra == -1) {
+          $val = $ref;
+        }
+        $val = '' if ! defined($val);
+      }
+      $val; # return of the swap
+    }
+  }xeg;
+
+  return ref($str) ? 1 : $$ref;
+}
+
+###----------------------------------------------------------------###
+
+1;
+
+__END__
+
+=head1 NAME
+
+CGI::Ex - CGI utility suite (form getter/filler/validator/app builder)
+
+=head1 SYNOPSIS
+
+  ### CGI Module Extensions
+
+  my $cgix = CGI::Ex->new;
+  my $hashref = $cgix->get_form; # uses CGI by default
+
+  ### send the Content-type header - whether or not we are mod_perl
+  $cgix->print_content_type;
+
+  my $val_hash = $cgix->conf_read($pathtovalidation);
+
+  my $err_obj = $cgix->validate($hashref, $val_hash);
+  if ($err_obj) {
+    my $errors  = $err_obj->as_hash;
+    my $input   = "Some content";
+    my $content = "";
+    SomeTemplateObject->process($input, $errors, $content);
+    $cgix->fill({text => \$content, form => $hashref});
+    print $content;
+    exit;
+  }
+
+  print "Success\n";
+
+  ### Filling functionality
+
+  $cgix->fill({text => \$text, form    => \%hash});
+  $cgix->fill({text => \$text, fdat    => \%hash});
+  $cgix->fill({text => \$text, fobject => $cgiobject});
+  $cgix->fill({text => \$text, form    => [\%hash1, $cgiobject]});
+  $cgix->fill({text => \$text); # uses $self->object as the form
+  $cgix->fill({text          => \$text,
+                 form          => \%hash,
+                 target        => 'formname',
+                 fill_password => 0,
+                 ignore_fields => ['one','two']});
+  $cgix->fill(\$text); # uses $self->object as the form
+  $cgix->fill(\$text, \%hash, 'formname', 0, ['one','two']);
+  my $copy = $cgix->fill({scalarref => \$text,    fdat => \%hash});
+  my $copy = $cgix->fill({arrayref  => \@lines,   fdat => \%hash});
+  my $copy = $cgix->fill({file      => $filename, fdat => \%hash});
+
+  ### Validation functionality
+
+  my $err_obj = $cgix->validate($form, $val_hash);
+  my $err_obj = $cgix->validate($form, $path_to_validation);
+  my $err_obj = $cgix->validate($form, $yaml_string);
+
+  ### get errors separated by key name
+  ### useful for inline errors
+  my $hash = $err_obj->as_hash;
+  my %hash = $err_obj->as_hash;
+
+  ### get aggregate list of errors
+  ### useful for central error description
+  my $array = $err_obj->as_array;
+  my @array = $err_obj->as_array;
+
+  ### get a string
+  ### useful for central error description
+  my $string = $err_obj->as_string;
+  my $string = "$err_obj";
+
+  $cgix->{raise_error} = 1;
+  $cgix->validate($form, $val_hash);
+    # SAME AS #
+  my $err_obj = $cgix->validate($form, $val_hash);
+  die $err_obj if $err_obj;
+
+  ### Settings functionality
+
+  ### read file via yaml
+  my $ref = $cgix->conf_read('/full/path/to/conf.yaml');
+
+  ### merge all found settings.pl files together
+  @CGI::Ex::Conf::DEFAULT_PATHS = qw(/tmp /my/data/dir /home/foo);
+  @CGI::Ex::Conf::DIRECTIVE     = 'MERGE';
+  @CGI::Ex::Conf::DEFAULT_EXT   = 'pl';
+  my $ref = $cgix->conf_read('settings');
+
+=head1 DESCRIPTION
+
+CGI::Ex provides a suite of utilities to make writing CGI scripts
+more enjoyable.  Although they can all be used separately, the
+main functionality of each of the modules is best represented in
+the CGI::Ex::App module.  CGI::Ex::App takes CGI application building
+to the next step.  CGI::Ex::App is not a framework (which normally
+includes prebuilt html) instead CGI::Ex::App is an extended application
+flow that normally dramatically reduces CGI build time.  See L<CGI::Ex::App>.
+
+CGI::Ex is another form filler / validator / conf reader / template
+interface.  Its goal is to take the wide scope of validators and other
+useful CGI application modules out there and merge them into one
+utility that has all of the necessary features of them all, as well
+as several extended methods that I have found useful in working on the web.
+
+The main functionality is provided by several other modules that
+may be used separately, or together through the CGI::Ex interface.
+
+=over 4
+
+=item C<CGI::Ex::Fill>
+
+A regular expression based form filler inner (accessed through B<-E<gt>fill>
+or directly via its own functions).  Can be a drop in replacement for
+HTML::FillInForm.  See L<CGI::Ex::Fill> for more information.
+
+=item C<CGI::Ex::Validate>
+
+A form field / cgi parameter / any parameter validator (accessed through
+B<-E<gt>validate> or directly via its own methods).  Not quite a drop in
+for most validators, although it has most of the functionality of most
+of the validators but with the key additions of conditional validation.
+Has a tightly integrated JavaScript portion that allows for duplicate client
+side validation.  See L<CGI::Ex::Validate> for more information.
+
+=item C<CGI::Ex::Conf>
+
+A general use configuration, or settings, or key / value file reader.  Has
+ability for providing key fallback as well as immutable key definitions.  Has
+default support for yaml, storable, perl, ini, and xml and open architecture
+for definition of others.  See L<CGI::Ex::Conf> for more information.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item C<-E<gt>fill>
+
+fill is used for filling hash or cgi object values into an existing
+html document (it doesn't deal at all with how you got the document).
+Arguments may be given as a hash, or a hashref or positional.  Some
+of the following arguments will only work using CGI::Ex::Fill - most
+will work with either CGI::Ex::Fill or HTML::FillInForm (assume they
+are available unless specified otherwise).  (See L<CGI::Ex::Fill> for
+a full explanation of functionality).  The arguments to fill are as
+follows (and in order of position):
+
+=over 4
+
+=item C<text>
+
+Text should be a reference to a scalar string containing the html to
+be modified (actually it could be any reference or object reference
+that can be modfied as a string).  It will be modified in place.
+Another named argument B<scalarref> is available if you would like to
+copy rather than modify.
+
+=item C<form>
+
+Form may be a hashref, a cgi style object, a coderef, or an array of
+multiple hashrefs, cgi objects, and coderefs.  Hashes should be key
+value pairs.  CGI objects should be able
+to call the method B<param> (This can be overrided).  Coderefs should
+expect expect the field name as an argument and should return a value.
+Values returned by form may be undef, scalar, arrayref, or coderef 
+(coderef values should expect an argument of field name and should
+return a value).  The code ref options are available to delay or add
+options to the bringing in of form informatin - without having to
+tie the hash.  Coderefs are not available in HTML::FillInForm.  Also
+HTML::FillInForm only allows CGI objects if an arrayref is used.
+
+NOTE: Only one of the form, fdat, and fobject arguments are allowed at
+a time.
+
+=item C<target>
+
+The name of the form that the fields should be filled to.  The default
+value of undef, means to fill in all forms in the html.
+
+=item C<fill_passwords>
+
+Boolean value defaults to 1.  If set to zero - password fields will
+not be filled.
+
+=item C<ignore_fields>
+
+Specify which fields to not fill in.  It takes either array ref of
+names, or a hashref with the names as keys.  The hashref option is
+not available in CGI::Ex::Fill.
+
+=back
+
+Other named arguments are available for compatiblity with HTML::FillInForm.
+They may only be used as named arguments.
+
+=over 4
+
+=item C<scalarref>
+
+Almost the same as the argument text.  If scalarref is used, the filled
+html will be returned.  If text is used the html passed is filled in place.
+
+=item C<arrayref>
+
+An array ref of lines of the document.  Forces a returned filled html
+document.
+
+=item C<file>
+
+An filename that will be opened, filled, and returned.
+
+=item C<fdat>
+
+A hashref of key value pairs.
+
+=item C<fobject>
+
+A cgi style object or arrayref of cgi style objects used for getting
+the key value pairs.  Should be capable of the ->param method and
+->cookie method as document in L<CGI>.
+
+=back
+
+See L<CGI::Ex::Fill> for more information about the filling process.
+
+=item C<-E<gt>object>
+
+Returns the CGI object that is currently being used by CGI::Ex.  If none
+has been set it will automatically generate an object of type
+$PREFERRED_CGI_MODULE which defaults to B<CGI>.
+
+=item C<-E<gt>validate>
+
+Validate has a wide range of options available. (See L<CGI::Ex::Validate>
+for a full explanation of functionality).  Validate has two arguments:
+
+=over 4
+
+=item C<form>
+
+Can be either a hashref to be validated, or a CGI style object (which
+has the param method).
+
+=item C<val_hash>
+
+The val_hash can be one of three items.  First, it can be a straight
+perl hashref containing the validation to be done.  Second, it can
+be a YAML document string.  Third, it can be the path to a file
+containing the validation.  The validation in a validation file will
+be read in depending upon file extension.
+
+=back
+
+=item C<-E<gt>get_form>
+
+Very similar to CGI->new->Vars except that arrays are returned as
+arrays.  Not sure why CGI::Val didn't do this anyway (well - yes -
+legacy Perl 4 - but at some point things need to be updated).
+
+=item C<-E<gt>set_form>
+
+Allow for setting a custom form hash.  Useful for testing, or other
+purposes.
+
+=item C<-E<gt>get_cookies>
+
+Returns a hash of all cookies.
+
+=item C<-E<gt>make_form>
+
+Takes a hash and returns a query_string.  A second optional argument
+may contain an arrayref of keys to use from the hash in building the
+query_string.  First argument is undef, it will use the form stored
+in itself as the hash.
+
+=item C<-E<gt>content_type>
+
+Can be called multiple times during the same session.  Will only
+print content-type once.  (Useful if you don't know if something
+else already printed content-type).  Calling this sends the Content-type
+header.  Trying to print -E<gt>content_type is an error.  For clarity,
+the method -E<gt>print_content_type is available.
+
+=item C<-E<gt>set_cookie>
+
+Arguments are the same as those to CGI->new->cookie({}).
+Uses CGI's cookie method to create a cookie, but then, depending on
+if content has already been sent to the browser will either print
+a Set-cookie header, or will add a <meta http-equiv='set-cookie'>
+tag (this is supported on most major browsers).  This is useful if
+you don't know if something else already printed content-type.
+
+=item C<-E<gt>location_bounce>
+
+Depending on if content has already been sent to the browser will either print
+a Location header, or will add a <meta http-equiv='refresh'>
+tag (this is supported on all major browsers).  This is useful if
+you don't know if something else already printed content-type.  Takes
+single argument of a url.
+
+=item C<-E<gt>last_modified>
+
+Depending on if content has already been sent to the browser will either print
+a Last-Modified header, or will add a <meta http-equiv='Last-Modified'>
+tag (this is supported on most major browsers).  This is useful if
+you don't know if something else already printed content-type.  Takes an
+argument of either a time (may be a CGI -expires style time) or a filename.
+
+=item C<-E<gt>expires>
+
+Depending on if content has already been sent to the browser will either print
+a Expires header, or will add a <meta http-equiv='Expires'>
+tag (this is supported on most major browsers).  This is useful if
+you don't know if something else already printed content-type.  Takes an
+argument of a time (may be a CGI -expires style time).
+
+=item C<-E<gt>send_status>
+
+Send a custom status.  Works in both CGI and mod_perl.  Arguments are
+a status code and the content (optional).
+
+=item C<-E<gt>send_header>
+
+Send a http header.  Works in both CGI and mod_perl.  Arguments are
+a header name and the value for that header.
+
+=item C<-E<gt>print_js>
+
+Prints out a javascript file.  Does everything it can to make sure
+that the javascript will cache.  Takes either a full filename,
+or a shortened name which will be looked for in @INC. (ie /full/path/to/my.js
+or CGI/Ex/validate.js or CGI::Ex::validate)
+
+=item C<-E<gt>swap_template>
+
+This is intended as a simple yet strong subroutine to swap
+in tags to a document.  It is intended to be very basic
+for those who may not want the full features of a Templating
+system such as Template::Toolkit (even though they should
+investigate them because they are pretty nice).  The default allows
+for basic template toolkit variable swapping.  There are two arguments.
+First is a string or a reference to a string.  If a string is passed,
+a copy of that string is swapped and returned.  If a reference to a
+string is passed, it is modified in place.  The second argument is
+a form, or a CGI object, or a cgiex object, or a coderef (if the second
+argument is missing, the cgiex object which called the method will be
+used).  If it is a coderef, it should accept key as its only argument and
+return the proper value.
+
+  my $cgix = CGI::Ex->new;
+  my $form = {foo  => 'bar',
+              this => {is => {nested => ['wow', 'wee']}}
+             };
+
+  my $str =  $cgix->swap_template("<html>[% foo %]<br>[% foo %]</html>", $form));
+  # $str eq '<html>bar<br>bar</html>'
+
+  $str = $cgix->swap_template("[% this.is.nested.1 %]", $form));
+  # $str eq 'wee'
+
+  $str = "[% this.is.nested.0 %]";
+  $cgix->swap_template(\$str, $form);
+  # $str eq 'wow'
+
+  # may also be called with only one argument as follows:
+  # assuming $cgix had a query string of ?foo=bar&baz=wow&this=wee
+  $str = "<html>([% foo %]) <br>
+          ([% baz %]) <br>
+          ([% this %]) </html>";
+  $cgix->swap_template(\$str);
+  #$str eq "<html>(bar) <br>
+  #        (wow) <br>
+  #        (wee) </html>";
+  
+For further examples, please see the code contained in t/samples/cgi_ex_*
+of this distribution.
+
+If at a later date, the developer upgrades to Template::Toolkit, the
+templates that were being swapped by CGI::Ex::swap_template should
+be compatible with Template::Toolkit.
+
+=back
+
+=head1 EXISTING MODULES
+
+The following is a list of existing validator and formfiller modules
+at the time of this writing (I'm sure this probably isn't exaustive).
+
+=over 4
+
+=item C<Email::Valid> - Validator
+
+=item C<SSN::Validate> - Validator
+
+=item C<Embperl::Form::Validate> - Validator
+
+=item C<Data::CGIForm> - Validator
+
+=item C<HTML::FillInForm> - Form filler-iner
+
+=item C<CGI> - CGI Getter.  Form filler-iner
+
+=head1 TODO
+
+Add an integrated template toolkit interface.
+
+Add an integrated debug module.
+
+=head1 MODULES
+
+See also L<CGI::Ex::Fill>.
+
+See also L<CGI::Ex::Validate>.
+
+See also L<CGI::Ex::Conf>.
+
+See also L<CGI::Ex::Die>.
+
+See also L<CGI::Ex::App>.
+
+See also L<CGI::Ex::Dump>.
+
+=head1 AUTHOR
+
+Paul Seamons
+
+=head1 LICENSE
+
+This module may be distributed under the same terms as Perl itself.
+
+=cut
+
+1;
diff --git a/lib/CGI/Ex/App.pm b/lib/CGI/Ex/App.pm
new file mode 100644 (file)
index 0000000..552045a
--- /dev/null
@@ -0,0 +1,1940 @@
+package CGI::Ex::App;
+
+### CGI Extended Application
+
+###----------------------------------------------------------------###
+#  Copyright 2004 - Paul Seamons                                     #
+#  Distributed under the Perl Artistic License without warranty      #
+###----------------------------------------------------------------###
+
+### See perldoc at bottom
+
+
+use strict;
+use vars qw($VERSION
+            $EXT_PRINT $EXT_VAL $BASE_DIR_REL $BASE_DIR_ABS $BASE_NAME_MODULE
+            $RECURSE_LIMIT
+            %CLEANUP_EXCLUDE);
+
+$VERSION = '1.14';
+use CGI::Ex::Dump qw(debug);
+
+BEGIN {
+  ### Default file locations
+  ### these are used for the provided stub functions - if you are not
+  ### using the stub functions - then you won't need to worry about these
+  $EXT_PRINT ||= 'html';
+  $EXT_VAL   ||= 'val';
+  $BASE_DIR_REL ||= ''; # relative path - stub methods will look in $BASE_DIR_REL/dir/of/content.html
+  $BASE_DIR_ABS ||= ''; # content should be found at "$BASE_DIR_ABS/$BASE_DIR_REL/dir/of/content.html"
+  $BASE_NAME_MODULE ||= ''; # the cgi name
+
+  ### list of modules to exclude during cleanup
+  ### this takes care of situations such as
+  ### template toolkits rules area which contains
+  ### a nested structure of rules and sub references.
+  $CLEANUP_EXCLUDE{'Template::Parser'} = 1;
+}
+
+
+###----------------------------------------------------------------###
+
+sub new {
+  my $class = shift || __PACKAGE__;
+  my $self  = ref($_[0]) ? shift : {@_};
+  bless $self, $class;
+  $self->init;
+  return $self;
+}
+
+sub init {}
+
+###----------------------------------------------------------------###
+
+sub navigate {
+  my $self = shift;
+  my $args = ref($_[0]) ? shift : {@_};
+  $self = $self->new($args) if ! ref $self;
+
+  eval {
+
+    ### a chance to do things at the very beginning
+    return $self if $self->pre_navigate;
+
+    ### run the step loop
+    eval {
+      local $self->{'__morph_lineage_start_index'} = $#{$self->{'__morph_lineage'} || []};
+      $self->nav_loop;
+    };
+    if ($@) {
+      ### rethrow the error unless we long jumped out of recursive nav_loop calls
+      die $@ if $@ ne "Long Jump\n";
+    }
+
+    ### one chance to do things at the very end
+    $self->post_navigate;
+
+  };
+
+  ### catch errors - if any
+  if ($@) {
+    $self->handle_error($@);
+  }
+
+  return $self;
+}
+
+sub nav_loop {
+  my $self = shift;
+
+  ### keep from an infinate nesting
+  local $self->{recurse} = $self->{recurse} || 0;
+  if ($self->{recurse} ++ >= $self->recurse_limit) {
+    my $err = "recurse_limit reached (".$self->recurse_limit.")";
+    $err .= " number of jumps (".$self->{jumps}.")" if ($self->{jumps} || 0) > 1;
+    die $err;
+  }
+
+  ### get the path (simple arrayref based thing)
+  my $path = $self->path;
+
+  ### allow for an early return
+  return if $self->pre_loop($path); # a true value means to abort the navigate
+
+  ### get a hash of valid paths (if any)
+  my $valid_steps = $self->valid_steps;
+
+  ### iterate on each step of the path
+  foreach ($self->{path_i} ||= 0;
+           $self->{path_i} <= $#$path;
+           $self->{path_i} ++) {
+    my $step = $path->[$self->{path_i}];
+    next if $step !~ /^[a-zA-Z_]\w*$/; # don't process the step if it contains odd characters
+
+    ### check if this is an allowed step
+    if ($valid_steps) {
+      if (! $valid_steps->{$step}
+          && $step ne $self->default_step
+          && $step ne 'forbidden') {
+        $self->stash->{'forbidden_step'} = $step;
+        $self->replace_path('forbidden');
+        next;
+      }
+    }
+
+    ### allow for becoming another package (allows for some steps in external files)
+    $self->morph($step);
+
+    ### run the guts of the step
+    my $status = $self->run_hook('run_step', $step);
+
+    $self->unmorph($step);
+
+    ### Allow for the run_step to intercept.
+    ### A true status means the run_step took over navigation.
+    return if $status;
+  }
+
+  ### allow for one exit point after the loop
+  return if $self->post_loop($path); # a true value means to abort the navigate
+
+  ### run the default step as a last resort
+  $self->insert_path($self->default_step);
+  $self->nav_loop; # go recursive
+
+  return;
+}
+
+sub pre_navigate {}
+
+sub post_navigate {}
+
+sub recurse_limit { shift->{'recurse_limit'} || $RECURSE_LIMIT || 15 }
+
+sub run_step {
+  my $self = shift;
+  my $step = shift;
+
+  ### if the pre_step exists and returns true, exit the nav_loop
+  return 1 if $self->run_hook('pre_step', $step);
+
+  ### allow for skipping this step (but stay in the nav_loop)
+  return 0 if $self->run_hook('skip', $step);
+
+  ### see if we have complete valid information for this step
+  ### if so, do the next step
+  ### if not, get necessary info and print it out
+  if (   ! $self->run_hook('prepare', $step, 1)
+      || ! $self->run_hook('info_complete', $step)
+      || ! $self->run_hook('finalize', $step, 1)) {
+
+    ### show the page requesting the information
+    $self->run_hook('prepared_print', $step);
+
+    ### a hook after the printing process
+    $self->run_hook('post_print', $step);
+
+    return 2;
+  }
+
+  ### a hook before end of loop
+  ### if the post_step exists and returns true, exit the nav_loop
+  return 1 if $self->run_hook('post_step', $step);
+
+  ### let the nav_loop continue searching the path
+  return 0;
+}
+
+### standard functions for printing - gather information
+sub prepared_print {
+  my $self = shift;
+  my $step = shift;
+
+  my $hash_base = $self->run_hook('hash_base',   $step);
+  my $hash_comm = $self->run_hook('hash_common', $step);
+  my $hash_form = $self->run_hook('hash_form',   $step);
+  my $hash_fill = $self->run_hook('hash_fill',   $step);
+  my $hash_swap = $self->run_hook('hash_swap',   $step);
+  my $hash_errs = $self->run_hook('hash_errors', $step);
+  $_ ||= {} foreach $hash_base, $hash_comm, $hash_form, $hash_fill, $hash_swap, $hash_errs;
+
+  ### fix up errors
+  $hash_errs->{$_} = $self->format_error($hash_errs->{$_})
+    foreach keys %$hash_errs;
+  $hash_errs->{has_errors} = 1 if scalar keys %$hash_errs;
+
+  ### layer hashes together
+  my $fill = {%$hash_form, %$hash_base, %$hash_comm, %$hash_fill};
+  my $swap = {%$hash_form, %$hash_base, %$hash_comm, %$hash_swap, %$hash_errs};
+  $fill = {} if $self->no_fill($step);
+
+  ### run the print hook - passing it the form and fill info
+  $self->run_hook('print', $step, undef,
+                  $swap, $fill);
+}
+
+sub no_fill { shift->{'no_fill'} }
+
+sub exit_nav_loop {
+  my $self = shift;
+
+  ### undo morphs
+  if (my $ref = $self->{'__morph_lineage'}) {
+    ### use the saved index - this allows for early "morphers" to only get rolled back so far
+    my $index = $self->{'__morph_lineage_start_index'};
+    $index = -1 if ! defined $index;
+    $self->unmorph while $#$ref != $index;
+  }
+
+  ### long jump back
+  die "Long Jump\n";
+}
+
+sub jump {
+  my $self   = shift;
+  my $i      = ($#_ == -1) ? 1 : shift;
+  my $path   = $self->path;
+  my $path_i = $self->{path_i};
+  die "Can't jump if nav_loop not started" if ! defined $path_i;
+
+  ### validate where we are jumping to
+  if ($i =~ /^\w+$/) {
+    if ($i eq 'FIRST') {
+      $i = - $path_i - 1;
+    } elsif ($i eq 'LAST') {
+      $i = $#$path - $path_i;
+    } elsif ($i eq 'NEXT') {
+      $i = 1;
+    } elsif ($i eq 'CURRENT') {
+      $i = 0;
+    } elsif ($i eq 'PREVIOUS') {
+      $i = -1;
+    } else { # look for a step by that name
+      for (my $j = $#$path; $j >= 0; $j --) {
+        if ($path->[$j] eq $i) {
+          $i = $j - $path_i;
+          last;
+        }
+      }
+    }
+  }
+  if ($i !~ /^-?\d+$/) {
+    require Carp;
+    Carp::croak("Invalid jump index ($i)");
+  }
+
+  ### manipulate the path to contain the new jump location
+  my @replace;
+  my $cut_i  = $path_i + $i;
+  if ($cut_i > $#$path) {
+    push @replace, $self->default_step;
+  } elsif ($cut_i < 0) {
+    push @replace, @$path;
+  } else {
+    push @replace, @$path[$cut_i .. $#$path];
+  }
+  $self->replace_path(@replace);
+
+  ### record the number of jumps
+  $self->{jumps} ||= 0;
+  $self->{jumps} ++;
+
+  ### run the newly fixed up path (recursively)
+  $self->{path_i} ++; # move along now that the path is updated
+  $self->nav_loop;
+  $self->exit_nav_loop;
+}
+
+sub default_step {
+  my $self = shift;
+  return $self->{'default_step'} || 'main';
+}
+
+###----------------------------------------------------------------###
+
+sub step_key {
+  my $self = shift;
+  return $self->{'step_key'} || 'step';
+}
+
+### determine the path to follow
+sub path {
+  my $self = shift;
+  return $self->{path} ||= do {
+    my @path     = (); # default to empty path
+    my $step_key = $self->step_key;
+
+    if (my $step = $self->form->{$step_key}) {
+      push @path, $step;
+    } elsif ($ENV{'PATH_INFO'} && $ENV{'PATH_INFO'} =~ m|^/(\w+)|) {
+      push @path, lc($1);
+    }
+
+    \@path; # return of the do
+  };
+}
+
+### really should only be used during initialization
+sub set_path {
+  my $self = shift;
+  my $path = $self->{path} ||= [];
+  die "Cannot call set_path after the navigation loop has begun" if $self->{path_i};
+  splice @$path, 0, $#$path + 1, @_; # change entries in the ref
+}
+
+### legacy - same as append_path
+sub add_to_path {
+  my $self = shift;
+  push @{ $self->path }, @_;
+}
+
+### append entries onto the end
+sub append_path {
+  my $self = shift;
+  push @{ $self->path }, @_;
+}
+
+### replace all entries that are left
+sub replace_path {
+  my $self = shift;
+  my $ref  = $self->path;
+  my $i    = $self->{path_i} || 0;
+  if ($i + 1 > $#$ref) {
+    push @$ref, @_;
+  } else {
+    splice(@$ref, $i + 1, $#$ref - $i, @_); # replace remaining entries
+  }
+}
+
+### insert more steps into the current path
+sub insert_path {
+  my $self = shift;
+  my $ref  = $self->path;
+  my $i    = $self->{path_i} || 0;
+  if ($i + 1 > $#$ref) {
+    push @$ref, @_;
+  } else {
+    splice(@$ref, $i + 1, 0, @_); # insert a path at the current location
+  }
+}
+
+### a hash of paths that are allowed, default undef is all
+sub valid_steps {}
+
+###----------------------------------------------------------------###
+### allow for checking where we are in the path
+
+sub step_by_path_index {
+  my $self = shift;
+  my $i    = shift || 0;
+  my $ref  = $self->path;
+  return '' if $i < 0;
+  return $self->default_step if $i > $#$ref;
+  return $ref->[$i];
+}
+
+sub previous_step {
+  my $self = shift;
+  die "previous_step is readonly" if $#_ != -1;
+  return $self->step_by_path_index( ($self->{path_i} || 0) - 1 );
+}
+
+sub current_step {
+  my $self = shift;
+  die "current_step is readonly" if $#_ != -1;
+  return $self->step_by_path_index( ($self->{path_i} || 0) );
+}
+
+sub next_step {
+  my $self = shift;
+  die "next_step is readonly" if $#_ != -1;
+  return $self->step_by_path_index( ($self->{path_i} || 0) + 1 );
+}
+
+sub last_step {
+  my $self = shift;
+  die "last_step is readonly" if $#_ != -1;
+  return $self->step_by_path_index( $#{ $self->path } );
+}
+
+sub first_step {
+  my $self = shift;
+  die "first_step is readonly" if $#_ != -1;
+  return $self->step_by_path_index( 0 );
+}
+
+###----------------------------------------------------------------###
+
+sub pre_loop {}
+sub post_loop {}
+
+### return the appropriate hook to call
+sub hook {
+  my $self    = shift;
+  my $hook    = shift || do { require Carp; Carp::confess("Missing hook name") };
+  my $step    = shift || '';
+  my $default = shift;
+  my $hist    = $self->history;
+  my $code;
+  if ($step && ($code = $self->can("${step}_${hook}"))) {
+    push @$hist, "$step - $hook - ${step}_${hook}";
+    return $code;
+  } elsif ($code = $self->can($hook)) {
+    push @$hist, "$step - $hook - $hook";
+    return $code;
+  } elsif (UNIVERSAL::isa($default, 'CODE')) {
+    push @$hist, "$step - $hook - DEFAULT CODE";
+    return $default;
+  } elsif ($default) {
+    push @$hist, "$step - $hook - DEFAULT";
+    return sub { return $default };
+  } else {
+    return sub {};
+  }
+}
+
+### get and call the appropriate hook
+sub run_hook {
+  my $self    = shift;
+  my $hook    = shift;
+  my $step    = shift;
+  my $default = shift;
+  my $code = $self->hook($hook, $step, $default);
+  return $self->$code($step, @_);
+}
+
+sub history {
+  return shift->{'history'} ||= [];
+}
+
+### default die handler - show what happened and die (so its in the error logs)
+sub handle_error {
+  my $self = shift;
+  my $err  = shift;
+  debug $err, $self->path, $self->history;
+  die $err;
+}
+
+###----------------------------------------------------------------###
+### utility modules for jeckyl/hyde on self
+
+sub allow_morph {
+  my $self = shift;
+  return $self->{'allow_morph'} ? 1 : 0;
+}
+
+sub allow_nested_morph {
+  my $self = shift;
+  return $self->{'allow_nested_morph'} ? 1 : 0;
+}
+
+sub morph {
+  my $self = shift;
+  my $step = shift || return;
+  return if ! (my $allow = $self->allow_morph); # not true
+
+  ### place to store the lineage
+  my $lin = $self->{'__morph_lineage'} ||= [];
+  my $cur = ref $self; # what are we currently
+  push @$lin, $cur;    # store so subsequent unmorph calls can do the right thing
+  my $hist = $self->history;
+  push @$hist, "$step - morph - morph";
+  my $sref = \$hist->[-1]; # get ref so we can add more info in a moment
+
+  if (ref($allow) && ! $allow->{$step}) { # hash - but no step - record for unbless
+    $$sref .= " - not allowed to morph to that step";
+    return;
+  }
+
+  ### make sure we haven't already been reblessed
+  if ($#$lin != 0                                # is this the second morph call
+      && (! ($allow = $self->allow_nested_morph) # not true
+          || (ref($allow) && ! $allow->{$step})  # hash - but no step
+          )) {
+    $$sref .= $allow ? " - not allowed to nested_morph to that step" : " - nested_morph disabled";
+    return; # just return - don't die so that we can morph early
+  }
+
+  ### if we are not already that package - bless us there
+  my $new  = $self->run_hook('morph_package', $step);
+  if ($cur ne $new) {
+    my $file = $new .'.pm';
+    $file =~ s|::|/|g;
+    if (UNIVERSAL::can($new, 'can')  # check if the package space exists
+        || eval { require $file }) { # check for a file that holds this package
+      ### become that package
+      bless $self, $new;
+      $$sref .= " - changed $cur to $new";
+      if (my $method = $self->can('fixup_after_morph')) {
+        $self->$method($step);
+      }
+    } else {
+      if ($@) {
+        if ($@ =~ /^\s*(Can\'t locate \S+ in \@INC)/) { # let us know what happened
+          $$sref .= " - failed from $cur to $new: $1";
+        } else {
+          $$sref .= " - failed from $cur to $new: $@";
+          my $err = "Trouble while morphing to $file: $@";
+          debug $err;
+          warn $err;
+        }
+      }
+    }
+  }
+
+}
+
+sub unmorph {
+  my $self = shift;
+  my $step = shift || '__no_step';
+  my $lin  = $self->{'__morph_lineage'} || return;
+  my $cur  = ref $self;
+  my $prev = pop(@$lin) || die "unmorph called more times than morph - current ($cur)";
+
+  ### if we are not already that package - bless us there
+  my $hist = $self->history;
+  if ($cur ne $prev) {
+    if (my $method = $self->can('fixup_before_unmorph')) {
+      $self->$method($step);
+    }
+    bless $self, $prev;
+    push @$hist, "$step - unmorph - unmorph - changed from $cur to $prev";
+  } else {
+    push @$hist, "$step - unmorph - unmorph - already isa $cur";
+  }
+
+  return $self;
+}
+
+###----------------------------------------------------------------###
+### allow for cleanup including deep nested objects
+
+sub cleanup {
+  my $self = shift;
+  ref($self)->cleanup_cross_references($self);
+}
+
+sub cleanup_cross_references {
+  my $class = shift;
+  my $self  = shift;
+  my $seen  = shift || {};
+  return if $seen->{$self}; # prevent recursive checking
+  $seen->{$self} = 1;
+  return if $CLEANUP_EXCLUDE{ ref($self) };
+  if (UNIVERSAL::isa($self, 'HASH')) {
+    require Scalar::Util; # first self will always be hash
+    foreach my $key (keys %$self) {
+      next if ! $self->{$key};
+      $class->cleanup_cross_references($self->{$key}, $seen);
+      # weaken and remove blessed objects
+      # this will clober objects in global caches that are referenced in the structure
+      # so beware (that means weaken your cached references)
+      if (Scalar::Util::blessed($self->{$key})
+          && ! Scalar::Util::isweak($self->{$key})) {
+        Scalar::Util::weaken($self->{$key});
+        $self->{$key} = undef;
+      } elsif (UNIVERSAL::isa($self->{$key}, 'CODE')) {
+        $self->{$key} = undef;
+      }
+    }
+  } elsif (UNIVERSAL::isa($self, 'ARRAY')) {
+    for my $key (0 .. $#$self) {
+      next if ! $self->[$key];
+      $class->cleanup_cross_references($self->[$key], $seen);
+      if (Scalar::Util::blessed($self->[$key])
+          && ! Scalar::Util::isweak($self->[$key])) {
+        Scalar::Util::weaken($self->[$key]);
+        $self->[$key] = undef;
+      } elsif (UNIVERSAL::isa($self->[$key], 'CODE')) {
+        $self->[$key] = undef;
+      }
+    }
+  }
+}
+
+###----------------------------------------------------------------###
+### a few standard base accessors
+
+sub form {
+  my $self = shift;
+  if ($#_ != -1) {
+    $self->{form} = shift || die "Invalid form";
+  }
+  return $self->{form} ||= $self->cgix->get_form;
+}
+
+sub cookies {
+  my $self = shift;
+  if ($#_ != -1) {
+    $self->{cookies} = shift || die "Invalid cookies";
+  }
+  return $self->{cookies} ||= $self->cgix->get_cookies;
+}
+
+sub cgix {
+  my $self = shift;
+  return $self->{cgix} ||= do {
+    my $args = shift || {};
+    require CGI::Ex;
+    CGI::Ex->new($args); # return of the do
+  };
+}
+
+sub set_cgix {
+  my $self = shift;
+  $self->{cgix} = shift;
+}
+
+sub vob {
+  my $self = shift;
+  return $self->{vob} ||= do {
+    my $args = shift || {};
+    $args->{cgix} ||= $self->cgix;
+    require CGI::Ex::Validate;
+    CGI::Ex::Validate->new($args); # return of the do
+  };
+}
+
+sub set_vob {
+  my $self = shift;
+  $self->{vob} = shift;
+}
+
+sub auth {
+  my $self = shift;
+  return $self->{auth} ||= do {
+    my $args = shift || {};
+    $args->{cgix}    ||= $self->cgix,
+    $args->{form}    ||= $self->form,
+    $args->{cookies} ||= $self->cookies,
+    require CGI::Ex::Auth;
+    CGI::Ex::Auth->new($args); # return of the do
+  };
+}
+
+sub set_auth {
+  my $self = shift;
+  $self->{auth} = shift;
+}
+
+### provide a place for placing variables
+sub stash {
+  my $self = shift;
+  return $self->{'stash'} ||= {};
+}
+
+### allow for adding arbitrary values to self
+sub add_property {
+  my $self = shift;
+  my $prop = shift;
+  my $key  = '__prop_'. $prop;
+  my $name = __PACKAGE__ ."::". $prop;
+  no strict 'refs';
+  *$name = sub : lvalue {
+    my $self = shift;
+    $self->{$key} = shift() if $#_ != -1;
+    $self->{$key};
+  } if ! defined &$name;
+  $self->$prop(shift()) if $#_ != -1;
+}
+
+###----------------------------------------------------------------###
+### js_validation items
+
+### creates javascript suitable for validating the form
+sub js_validation {
+  my $self = shift;
+  my $step = shift;
+  return '' if $self->ext_val eq 'htm'; # let htm validation do it itself
+
+  my $form_name = shift || $self->run_hook('form_name', $step);
+  my $hash_val  = shift || $self->run_hook('hash_validation', $step, {});
+  my $js_uri    = $self->js_uri_path;
+  return '' if UNIVERSAL::isa($hash_val, 'HASH')  && ! scalar keys %$hash_val
+            || UNIVERSAL::isa($hash_val, 'ARRAY') && $#$hash_val == -1;
+
+  return $self->vob->generate_js($hash_val, $form_name, $js_uri);
+}
+
+### where to find the javascript files
+### default to using this script as a handler
+sub js_uri_path {
+  my $self   = shift;
+  my $script = $ENV{'SCRIPT_NAME'} || die "Missing SCRIPT_NAME";
+  return ($self->can('path') == \&CGI::Ex::App::path)
+    ? $script . '/js' # try to use a cache friendly URI (if path is our own)
+    : $script . '?'.$self->step_key.'=js&js='; # use one that works with more paths
+}
+
+### name to attach js validation to
+sub form_name { 'theform' }
+
+### provide some rudimentary javascript support
+### if valid_steps is defined - it should include "js"
+sub js_run_step {
+  my $self = shift;
+
+  ### make sure path info looks like /js/CGI/Ex/foo.js
+  my $file = $self->form->{'js'} || $ENV{'PATH_INFO'} || '';
+  $file = ($file =~  m!^(?:/js/|/)?(\w+(?:/\w+)*\.js)$!) ? $1 : '';
+
+  $self->cgix->print_js($file);
+  return 1; # intercepted
+}
+
+###----------------------------------------------------------------###
+### implementation specific subs
+
+sub template_args {
+  my $self = shift;
+  my $step = shift;
+  return {
+    INCLUDE_PATH => $self->base_dir_abs,
+  };
+}
+
+sub print {
+  my $self = shift;
+  my $step = shift;
+  my $swap = shift;
+  my $fill = shift;
+
+  ### get a filename relative to base_dir_abs
+  my $file = $self->run_hook('file_print', $step);
+
+  require Template;
+  my $t = Template->new($self->template_args($step));
+
+  ### process the document
+  my $out = '';
+  my $status = $t->process($file, $swap, \$out) || die $Template::ERROR;
+
+  ### fill in any forms
+  $self->cgix->fill(\$out, $fill) if $fill && ! $self->{no_fill};
+
+  ### now print
+  $self->cgix->print_content_type();
+  print $out;
+}
+
+sub base_dir_rel {
+  my $self = shift;
+  $self->{base_dir_rel} = shift if $#_ != -1;
+  return $self->{base_dir_rel} ||= $BASE_DIR_REL;
+}
+
+sub base_dir_abs {
+  my $self = shift;
+  $self->{base_dir_abs} = shift if $#_ != -1;
+  return $self->{base_dir_abs} || $BASE_DIR_ABS
+    || die "\$BASE_DIR_ABS not set for use in stub functions";
+}
+
+sub ext_val {
+  my $self = shift;
+  $self->{ext_val} = shift if $#_ != -1;
+  return $self->{ext_val} || $EXT_VAL || die "\$EXT_VAL not set for use in stub functions";
+}
+
+sub ext_print {
+  my $self = shift;
+  $self->{ext_print} = shift if $#_ != -1;
+  return $self->{ext_print} || $EXT_PRINT || die "\$EXT_PRINT not set for use in stub functions";
+}
+
+sub has_errors {
+  my $self = shift;
+  return 1 if scalar keys %{ $self->hash_errors };
+}
+
+sub format_error {
+  my $self  = shift;
+  my $error = shift;
+#  return $error if $error =~ /<span/i;
+#  return "<span class=\"error\">$error</span>";
+}
+
+###----------------------------------------------------------------###
+### default stub subs
+
+### used for looking up a module to morph into
+sub morph_package {
+  my $self = shift;
+  my $step = shift || '';
+  my $cur = ref $self; # default to using self as the base for morphed modules
+  my $new = $cur .'::'. $step;
+  $new =~ s/(\b|_+)(\w)/\u$2/g; # turn Foo::my_step_name into Foo::MyStepName
+  return $new;
+}
+
+sub base_name_module {
+  my $self = shift;
+  $self->{base_name_module} = shift if $#_ != -1;
+  return $self->{base_name_module} ||= $BASE_NAME_MODULE;
+}
+
+### used for looking up template content
+sub name_module {
+  my $self = shift;
+  my $step = shift || '';
+  my $name;
+  if ($name = $self->base_name_module) {
+    return $name;
+  } else {
+    return ($0 =~ m/(\w+)(\.\w+)?$/) ? $1 # allow for cgi-bin/foo or cgi-bin/foo.pl
+      : die "Couldn't determine module name from \"name_module\" lookup ($step)";
+  }
+}
+
+### which file is used for templating
+sub file_print {
+  my $self = shift;
+  my $step = shift;
+
+  my $base_dir_rel = $self->base_dir_rel;
+  my $module       = $self->run_hook('name_module', $step);
+  my $_step        = $self->run_hook('name_step', $step, $step);
+  my $ext          = $self->ext_print;
+
+  return "$base_dir_rel/$module/$_step.$ext";
+}
+
+### which file is used for validation
+sub file_val {
+  my $self = shift;
+  my $step = shift;
+
+  my $base_dir = $self->base_dir_rel;
+  my $module   = $self->run_hook('name_module', $step);
+  my $_step    = $self->run_hook('name_step', $step, $step);
+  my $ext      = $self->ext_val;
+
+  ### get absolute if necessary
+  if ($base_dir !~ m|^/|) {
+    $base_dir = $self->base_dir_abs . "/$base_dir";
+  }
+
+  return "$base_dir/$module/$_step.$ext";
+}
+
+
+sub info_complete {
+  my $self = shift;
+  my $step = shift;
+
+  return 0 if ! $self->run_hook('ready_validate', $step);
+
+  return $self->run_hook('validate', $step);
+}
+
+sub ready_validate {
+  my $self = shift;
+  my $step = shift;
+
+  ### could do a slightly more complex test
+  return 0 if ! $ENV{REQUEST_METHOD} || $ENV{REQUEST_METHOD} ne 'POST';
+  return 1;
+}
+
+sub set_ready_validate {
+  my $self = shift;
+  my $ready = shift;
+  $ENV{REQUEST_METHOD} = ($ready) ? 'POST' : 'GET';
+}
+
+sub validate {
+  my $self = shift;
+  my $step = shift;
+  my $form = shift || $self->form;
+  my $hash = $self->run_hook('hash_validation', $step, {});
+  my $what_was_validated = [];
+
+  my $eob = eval { $self->vob->validate($form, $hash, $what_was_validated) };
+  if (! $eob && $@) {
+    die "Step $step: $@";
+  }
+
+  ### had an error - store the errors and return false
+  if ($eob) {
+    $self->add_errors($eob->as_hash({
+      as_hash_join   => "<br>\n",
+      as_hash_suffix => '_error',
+    }));
+    return 0;
+  }
+
+  ### allow for the validation to give us some redirection
+  my $val;
+  OUTER: foreach my $ref (@$what_was_validated) {
+    foreach my $method (qw(append_path replace_path insert_path)) {
+      next if ! ($val = $ref->{$method});
+      $self->$method(ref $val ? @$val : $val);
+      last OUTER;
+    }
+  }
+
+  return 1;
+}
+
+### allow for using ConfUtil instead of yaml
+sub hash_validation {
+  my $self = shift;
+  my $step = shift;
+  return $self->{hash_validation}->{$step} ||= do {
+    my $hash;
+    my $file = $self->run_hook('file_val', $step);
+
+    ### allow for returning the validation hash in the filename
+    ### a scalar ref means it is a yaml document to be read by get_validation
+    if (ref($file) && ! UNIVERSAL::isa($file, 'SCALAR')) {
+      $hash = $file;
+
+    ### read the file - it it fails - errors should shown in the error logs
+    } elsif ($file) {
+      $hash = eval { $self->vob->get_validation($file) } || {};
+
+    } else {
+      $hash = {};
+    }
+
+    $hash; # return of the do
+  };
+}
+
+sub hash_base {
+  my ($self, $step) = @_;
+  return $self->{hash_base} ||= {
+    script_name   => $ENV{'SCRIPT_NAME'} || $0,
+    path_info     => $ENV{'PATH_INFO'}   || '',
+    js_validation => sub { $self->run_hook('js_validation', $step, shift) },
+    form_name     => sub { $self->run_hook('form_name', $step) },
+  };
+}
+
+sub hash_common { shift->{'hash_common'} ||= {} }
+sub hash_form   { shift->form }
+sub hash_fill   { shift->{'hash_fill'}   ||= {} }
+sub hash_swap   { shift->{'hash_swap'}   ||= {} }
+sub hash_errors { shift->{'hash_errors'} ||= {} }
+
+sub add_errors {
+  my $self = shift;
+  my $hash = $self->hash_errors;
+  my $args = ref($_[0]) ? shift : {@_};
+  foreach my $key (keys %$args) {
+    my $_key = ($key =~ /error$/) ? $key : "${key}_error";
+    if ($hash->{$_key}) {
+      $hash->{$_key} .= '<br>' . $args->{$key};
+    } else {
+      $hash->{$_key} = $args->{$key};
+    }
+  }
+  $hash->{'has_errors'} = 1;
+}
+
+sub add_to_errors { shift->add_errors(@_) }
+sub add_to_swap   { my $self = shift; $self->add_to_hash($self->hash_swap,   @_) }
+sub add_to_fill   { my $self = shift; $self->add_to_hash($self->hash_fill,   @_) }
+sub add_to_form   { my $self = shift; $self->add_to_hash($self->hash_form,   @_) }
+sub add_to_common { my $self = shift; $self->add_to_hash($self->hash_common, @_) }
+sub add_to_base   { my $self = shift; $self->add_to_hash($self->hash_base,   @_) }
+
+sub add_to_hash {
+  my $self = shift;
+  my $old  = shift;
+  my $new  = shift;
+  $new = {$new, @_} if ! ref $new; # non-hashref
+  $old->{$_} = $new->{$_} foreach keys %$new;
+}
+
+###----------------------------------------------------------------###
+
+sub forbidden_info_complete { 0 }
+
+sub forbidden_file_print {
+  my $self = shift;
+  my $step = $self->stash->{'forbidden_step'};
+  my $str = "You do not have access to \"$step\"";
+  return \$str;
+}
+
+###----------------------------------------------------------------###
+
+1;
+
+__END__
+
+=head1 NAME
+
+CGI::Ex::App - Full featured (within reason) application builder.
+
+=head1 DESCRIPTION
+
+Fill in the blanks and get a ready made CGI.  This module is somewhat
+similar in spirit to CGI::Application, CGI::Path, and CGI::Builder and any
+other "CGI framework."  As with the others, CGI::Ex::App tries to do as
+much as possible, in a simple manner, without getting in the
+developer's way.  Your milage may vary.
+
+=head1 SYNOPSIS
+
+More examples will come with time.  Here are the basics for now.
+
+  #!/usr/bin/perl -w
+
+  MyApp->navigate;
+   # OR you could do the following which cleans
+   # circular references - useful for a mod_perl situation
+   # MyApp->navigate->cleanup;
+  exit;
+
+  package MyApp;
+  use strict;
+  use base qw(CGI::Ex::App);
+  use CGI::Ex::Dump qw(debug);
+
+  sub valid_steps { return {success => 1, js => 1} }
+    # default_step (main) is a valid path
+    # note the inclusion of js step to allow the
+    # javascript scripts in js_validation to function properly.
+
+  # base_dir_abs is only needed if default print is used
+  # template toolkit needs an INCLUDE_PATH
+  sub base_dir_abs { '/tmp' }
+
+  sub main_file_print {
+    # reference to string means ref to content
+    # non-reference means filename
+    return \ "<h1>Main Step</h1>
+      <form method=post name=[% form_name %]>
+      <input type=text name=foo>
+      <span style='color:red' id=foo_error>[% foo_error %]</span><br>
+      <input type=submit>
+      </form>
+      [% js_validation %]
+      <a href='[% script_name %]?step=foo'>Link to forbidden step</a>
+    ";
+  }
+
+  sub post_print {
+    debug shift->history;
+  } # show what happened
+
+  sub main_file_val {
+    # reference to string means ref to yaml document
+    # non-reference means filename
+    return \ "foo:
+      required: 1
+      min_len: 2
+      max_len: 20
+      match: 'm/^([a-z]\\d)+[a-z]?\$/'
+      match_error: Characters must alternate letter digit letter.
+      \n";
+  }
+
+  sub main_finalize {
+    my $self = shift;
+
+    debug $self->form, "Do something useful with form here";
+
+    ### add success step
+    $self->add_to_swap({success_msg => "We did something"});
+    $self->append_path('success');
+    $self->set_ready_validate(0);
+    return 1;
+  }
+
+  sub success_file_print {
+    \ "<h1>Success Step</h1> All done.<br>
+       ([% success_msg %])<br>
+       (foo = [% foo %])";
+  }
+
+  ### not necessary - this is the default hash_base
+  sub hash_base { # used to include js_validation
+    my ($self, $step) = @_;
+    return $self->{hash_base} ||= {
+      script_name   => $ENV{SCRIPT_NAME} || '',
+      js_validation => sub { $self->run_hook('js_validation', $step) },
+      form_name     => sub { $self->run_hook('form_name', $step) },
+    };
+  }
+
+  __END__
+
+Note: This example would be considerably shorter if the html file
+(file_print) and the validation file (file_val) had been placed in
+separate files.  Though CGI::Ex::App will work "out of the box" as
+shown it is more probable that any platform using it will customize
+the various hooks to their own tastes (for example, switching print to
+use a system other than Template::Toolkit).
+
+=head1 HOOKS / METHODS
+
+CGI::Ex::App works on the principles of hooks which are essentially
+glorified method lookups.  When a hook is called, CGI::Ex::App will
+look for a corresponding method call for that hook for the current
+step name.  See the discussion under the method named "hook" for more
+details.  The methods listed below are normal method calls.
+Hooks and methods are looked for in the following order:
+
+=over 4
+
+=item Method C<-E<gt>new>
+
+Object creator.  Takes a hash or hashref.
+
+=item Method C<-E<gt>init>
+
+Called by the default new method.  Allows for any object
+initilizations.
+
+=item Method C<-E<gt>form>
+
+Returns a hashref of the items passed to the CGI.  Returns
+$self->{form}.  Defaults to CGI::Ex::get_form.
+
+=item Method C<-E<gt>navigate>
+
+Takes a class name or a CGI::Ex::App object as arguments.  If a class
+name is given it will instantiate an object by that class.  All returns
+from navigate will return the object.
+
+The method navigate is essentially a safe wrapper around the ->nav_loop
+method.  It will catch any dies and pass them to ->handle_error.
+
+=item Method C<-E<gt>nav_loop>
+
+This is the main loop runner.  It figures out the current path
+and runs all of the appropriate hooks for each step of the path.  If
+nav_loop runs out of steps to run (which happens if no path is set, or if
+all other steps run successfully), it will insert the ->default_step into
+the path and run nav_loop again (recursively).  This way a step is always
+assured to run.  There is a method ->recurse_limit (default 15) that
+will catch logic errors (such as inadvertently running the same
+step over and over and over).
+
+The basic outline of navigation is as follows (the default actions for hooks
+are shown):
+
+  navigate {
+    eval {
+      ->pre_navigate
+      ->nav_loop
+      ->post_navigate
+    }
+    # dying errors will run the ->handle_error method
+  }
+
+
+  nav_loop {
+    ->path (get the path steps)
+       # DEFAULT ACTION
+       # look in $ENV{'PATH_INFO'}
+       # look in ->form for ->step_key
+
+    ->pre_loop
+       # navigation stops if true
+
+    ->valid_steps (get list of valid paths)
+
+    foreach step of path {
+
+      # check that path is valid
+
+      ->morph
+        # DEFAULT ACTION
+        # check ->allow_morph
+        # check ->allow_nested_morph
+        # ->morph_package (hook - get the package to bless into)
+        # ->fixup_after_morph if morph_package exists
+
+      ->run_step (hook)
+
+      ->unmorph
+        # DEFAULT ACTION
+        # ->fixup_before_unmorph if blessed to previous package
+
+      # exit loop if ->run_step returned true (intercepted)
+
+    } end of step foreach
+
+    ->post_loop
+      # navigation stops if true
+
+    ->default_step (inserted into path at current location)
+    ->nav_loop (called again recursively)
+
+  } end of nav_loop
+
+
+  run_step {
+    ->pre_step (hook)
+      # exits nav_loop if true
+
+    ->skip (hook)
+      # skips this step if true (stays in nav_loop)
+
+    ->prepare (hook - defaults to true)
+
+    ->info_complete (hook - ran if prepare was true)
+      # DEFAULT ACTION
+      # ->ready_validate (hook)
+      # return false if ! ready_validate
+      # ->validate (hook)
+      #   ->hash_validation (hook)
+      #     ->file_val (hook - uses base_dir_rel, name_module, name_step, ext_val)
+      #   uses CGI::Ex::Validate to validate the hash
+      # returns true if validate is true
+
+    ->finalize (hook - defaults to true - ran if prepare and info_complete were true)
+
+    if ! ->prepare || ! ->info_complete || ! ->finalize {
+      ->prepared_print
+        # DEFAULT ACTION
+        # ->hash_base (hook)
+        # ->hash_common (hook)
+        # ->hash_form (hook)
+        # ->hash_fill (hook)
+        # ->hash_swap (hook)
+        # ->hash_errors (hook)
+        # merge form, base, common, and fill into merged fill
+        # merge form, base, common, swap, and errors into merged swap
+        # ->print (hook - passed current step, merged swap hash, and merged fill)
+          # DEFAULT ACTION
+          # ->file_print (hook - uses base_dir_rel, name_module, name_step, ext_print)
+          # ->template_args
+          # Processes the file with Template Toolkit
+          # Fills the any forms with CGI::Ex::Fill
+          # Prints headers and the content
+
+      ->post_print (hook - used for anything after the print process)
+
+      # return true to exit from nav_loop
+    }
+
+    ->post_step (hook)
+      # exits nav_loop if true
+
+  } end of run_step
+
+
+=item Method C<-E<gt>pre_navigate>
+
+Called from within navigate.  Called before the nav_loop method is started.
+If a true value is returned then navigation is skipped (the nav_loop is never
+started).
+
+=item Method C<-E<gt>post_navigate>
+
+Called from within navigate.  Called after the nav_loop has finished running.
+Will only run if there were no errors which died during the nav_loop
+process.
+
+=item Method C<-E<gt>handle_error>
+
+If anything dies during execution, handle_error will be called with
+the error that had happened.  Default is to debug the error and path
+history.
+
+=item Method C<-E<gt>history>
+
+Returns an arrayref of which hooks of which steps of the path were ran.
+Useful for seeing what happened.  In general - each line of the history
+will show the current step, the hook requested, and which hook was
+actually called. (hooks that don't find a method don't add to history)
+
+=item Method C<-E<gt>path>
+
+Return an arrayref (modifyable) of the steps in the path.  For each
+step the remaining hooks can be run.  Hook methods are looked up and
+ran using the method "run_hook" which uses the method "hook" to lookup
+the hook.  A history of ran hooks is stored in the array ref returned
+by $self->history.  Default will be a single step path looked up in
+$form->{path} or in $ENV{PATH_INFO}.  By default, path will look for
+$ENV{'PATH_INFO'} or the value of the form by the key step_key.  For
+the best functionality, the arrayref returned should be the same
+reference returned for every call to path - this ensures that other
+methods can add to the path (and will most likely break if the
+arrayref is not the same).  If navigation runs out of steps to run,
+the default step found in default_step will be run.
+
+=item Method C<-E<gt>default_step>
+
+Step to show if the path runs out of steps.  Default value is the
+'default_step' property or the value 'main'.
+
+=item Method C<-E<gt>step_key>
+
+Used by default to determine which step to put in the path.  The
+default path will only have one step within it
+
+=item Method C<-E<gt>set_path>
+
+Arguments are the steps to set.  Should be called before navigation
+begins.  This will set the path arrayref to the passed steps.
+
+=item Method C<-E<gt>append_path>
+
+Arguments are the steps to append.  Can be called any time.  Adds more
+steps to the end of the current path.
+
+=item Method C<-E<gt>replace_path>
+
+Arguments are the steps used to replace.  Can be called any time.
+Replaces the remaining steps (if any) of the current path.
+
+=item Method C<-E<gt>insert_path>
+
+Arguments are the steps to insert.  Can be called any time.  Inserts
+the new steps at the current path location.
+
+=item Method C<-E<gt>jump>
+
+This method should not normally be used.  It provides for moving to the
+next step at any point during the nav_loop.  It effectively short circuits
+the remaining hooks for the current step.  It does increment the recursion
+counter (which has a limit of ->recurse_limit - default 15).  It is normally
+better to allow the other hooks in the loop to carry on their normal functions
+and avoid jumping.  (Essentially, this hook behaves like a goto method to
+bypass everything else and continue at a different location in the path - there
+are times when it is necessary or useful - but most of the time should be
+avoided)
+
+Jump takes a single argument which is the location in the path to jump
+to.  This argument may be either a step name, the special words
+"FIRST, LAST, CURRENT, PREVIOUS, OR NEXT" or the number of steps to
+jump forward (or backward) in the path.  The default value, 1,
+indicates that CGI::Ex::App should jump to the next step (the default action for
+jump).  A value of 0 would repeat the current step (watch out for
+recursion).  A value of -1 would jump to the previous step.  The
+special value of "LAST" will jump to the last step.  The special value
+of "FIRST" will jump back to the first step.  In each of these cases,
+the path array retured by ->path is modified to allow for the jumping.
+
+  ### goto previous step
+  $self->jump($self->previous_step);
+  $self->jump('PREVIOUS');
+  $self->jump(-1);
+
+  ### goto next step
+  $self->jump($self->next_step);
+  $self->jump('NEXT');
+  $self->jump(1);
+  $self->jump;
+
+  ### goto current step (repeat)
+  $self->jump($self->current_step);
+  $self->jump('CURRENT');
+  $self->jump(0);
+
+  ### goto last step
+  $self->jump($self->last_step);
+  $self->jump('LAST');
+
+  ### goto first step
+  $self->jump($self->first_step);
+  $self->jump('FIRST');
+
+=item Method C<-E<gt>exit_nav_loop>
+
+This method should not normally used.  It allows for a long jump to the
+end of all nav_loops (even if they are recursively nested).  This
+effectively short circuits all remaining hooks for the current and
+remaining steps.  It is used to allow the ->jump functionality.  If the
+application has morphed, it will be unmorphed before returning.
+
+=item Method C<-E<gt>recurse_limit>
+
+Default 15.  Maximum number of times to allow nav_loop to call itself.
+If ->jump is used alot - the recurse_limit will be reached more quickly.
+It is safe to raise this as high as is necessary - so long as it is intentional.
+
+=item Method C<-E<gt>valid_steps>
+
+Returns a hashref of path steps that are allowed.  If step found in
+default method path is not in the hash, the method path will return a
+single step "forbidden" and run its hooks.  If no hash or undef is
+returned, all paths are allowed (default).  A key "forbidden_step"
+containing the step that was not valid will be placed in the stash.
+Often the valid_steps method does not need to be defined as arbitrary
+method calls are not possible with CGI::Ex::App.
+
+=item Method C<-E<gt>previous_step, -E<gt>current_step, -E<gt>next_step, -E<gt>last_step, -E<gt>first_step>
+
+Return the previous, current, next, last, and first step name - useful for figuring
+out where you are in the path.  Note that first_step may not be the same
+thing as default_step if the path was overridden.
+
+=item Method C<-E<gt>pre_loop>
+
+Called right before the navigation loop is started.  At this point the
+path is set (but could be modified).  The only argument is a reference
+to the path array.  If it returns a true value - the navigation
+routine is aborted.
+
+=item Method C<-E<gt>run_hook>
+
+Calls "hook" to get a code ref which it then calls and returns the
+result.  Arguments are the same as that for "hook".
+
+=item Method C<-E<gt>hook>
+
+Arguments are a hook name, a pathstep name, and an optional code sub
+or default value (default value will be turned to a sub) (code sub
+will be called as method of $self).
+
+  my $code = $self->hook('main', 'info_complete', sub {return 0});
+  ### will look first for $self->main_info_complete;
+  ### will then look  for $self->info_complete;
+  ### will then run       $self->$default_passed_sub; # sub {return 0}
+
+This system is used to allow for multiple steps to be in the same
+file and still allow for moving some steps out to external sub classed
+packages.  If the application has successfully morphed then it is not
+necessary to add the step name to the beginning of the method name as
+the morphed packages method will override the base package (it is still
+OK to use the full method name "${step}_hookname").
+
+If a hook is found (or a default value is found) then an entry is added
+to the arrayref contained in ->history.
+
+=item Method C<-E<gt>morph>
+
+Allows for temporarily "becoming" another object type for the
+execution of the current step.  This allows for separating some steps
+out into their own packages.  Morph will only run if the method
+allow_morph returns true.  Additionally if the allow_morph returns a hash
+ref, morph will only run if the step being morphed to is in the hash.
+The morph call occurs at the beginning of the step loop.  A
+corresponding unmorph call occurs before the loop is exited.  An
+object can morph several levels deep if allow_nested_morph returns
+true. For example, an object running as Foo::Bar that is looping on
+the step "my_step" that has allow_morph = 1, will do the following:
+call the hook morph_package (which would default to returning
+Foo::Bar::MyStep in this case), translate this to a package filename
+(Foo/Bar/MyStep.pm) and try and require it, if the file can be
+required, the object is blessed into that package.  If that package
+has a "fixup_after_morph" method, it is called.  The navigate loop
+then continues for the current step.  At any exit point of the loop,
+the unmorph call is made which reblesses the object into the original
+package.
+
+It is possible to call morph earlier on in the program.  An example of
+a useful early use of morph would be as in the following code:
+
+  sub allow_morph { 1 }
+
+  sub pre_navigate {
+    my $self = shift;
+    if ($ENV{'PATH_INFO'} && $ENV{'PATH_INFO'} =~ s|^/(\w+)||) {
+      my $step = $1;
+      $self->morph($step);
+      $ENV{'PATH_INFO'} = "/$step";
+      $self->stash->{'base_morphed'} = 1;
+    }
+    return 0;
+  }
+
+  sub post_navigate {
+    my $self = shift;
+    $self->unmorph if $self->stash->{'base_morphed'};
+  }
+
+If this code was in a module Base.pm and the cgi running was cgi/base
+and called:
+
+  Base->navigate;
+  # OR - for mod_perl resident programs
+  Base->navigate->cleanup;
+  # OR
+  sub post_navigate { shift->cleanup }
+
+and you created a sub module that inherited Base.pm called
+Base/Ball.pm -- you could then access it using cgi/base/ball.  You
+would be able to pass it steps using either cgi/base/ball/step_name or
+cgi/base/ball?step=step_name - Or Base/Ball.pm could implement its
+own path.  It should be noted that if you do an early morph, it is
+suggested to provide a call to unmorph. And if you want to let your
+early morphed object morph again - you will need to provide
+
+  sub allow_nested_morph { 1 }
+
+With allow_nested_morph enabled you could create the file
+Base/Ball/StepName.pm which inherits Base/Ball.pm.  The Base.pm, with
+the custom init and default path method, would automatically morph us
+first into a Base::Ball object (during init) and then into a
+Base::Ball::StepName object (during the navigation loop).
+
+=item Method C<-E<gt>unmorph>
+
+Allows for returning an object back to its previous blessed state.
+This only happens if the object was previously morphed into another
+object type.  Before the object is reblessed the method
+"fixup_before_unmorph" is called if it exists.
+
+=item Method C<-E<gt>allow_morph>
+
+Boolean value.  Specifies whether or not morphing is allowed.
+Defaults to the property "allow_morph" if found, otherwise false.
+For more granularity, if true value is a hash, the step being
+morphed to must be in the hash.
+
+=item Method C<-E<gt>allow_nested_morph>
+
+Boolean value.  Specifies whether or not nested morphing is allowed.
+Defaults to the property "allow_nested_morph" if found, otherwise
+false.  For more granularity, if true value is a hash, the step being
+morphed to must be in the hash.
+
+=item Hook C<-E<gt>morph_package>
+
+Used by morph.  Return the package name to morph into during a morph
+call.  Defaults to using the current object type as a base.  For
+example, if the current object running is a Foo::Bar object and the
+step running is my_step, then morph_package will return
+Foo::Bar::MyStep.
+
+=item Hook C<-E<gt>run_step>
+
+Runs all of the hooks specific to each step, beginning with pre_step
+and ending with post_step.  Called after ->morph($step) has been
+run.  If this returns true, the nav_loop is exited (meaning the
+run_step hook displayed the information).  If it returns false,
+the nav_loop continues on to run the next step.  This is essentially
+the same thing as a method defined in CGI::Applications ->run_modes.
+
+=item Hook C<-E<gt>pre_step>
+
+Ran at the beginning of the loop before prepare, info_compelete, and
+finalize are called.  If it returns true, execution of nav_loop is
+returned and no more steps are processed.
+
+=item Hook C<-E<gt>skip>
+
+Ran at the beginning of the loop before prepare, info_compelete, and
+finalize are called.  If it returns true, nav_loop moves on to the
+next step (the current step is skipped).
+
+=item Hook C<-E<gt>prepare>
+
+Defaults to true.  A hook before checking if the info_complete is true.
+
+=item Hook C<-E<gt>info_complete>
+
+Checks to see if all the necessary form elements have been passed in.
+Calls hooks ready_validate, and validate.  Will not be run unless
+prepare returns true (default).
+
+=item Hook C<-E<gt>finalize>
+
+Defaults to true. Used to do whatever needs to be done with the data once
+prepare has returned true and info_complete has returned true.  On failure
+the print operations are ran.  On success navigation moves on to the next
+step.
+
+=item Hook C<-E<gt>ready_validate>
+
+Should return true if enough information is present to run validate.
+Default is to look if $ENV{'REQUEST_METHOD'} is 'POST'.  A common
+usage is to pass a common flag in the form such as 'processing' => 1
+and check for its presence - such as the following:
+
+  sub ready_validate { shift->form->{'processing'} }
+
+=item Method C<-E<gt>set_ready_validate>
+
+Sets that the validation is ready to validate.  Should set the value
+checked by the hook ready_validate.  The following would complement the
+processing flag above:
+
+  sub set_ready_validate {
+    my $self = shift;
+    if (shift) {
+      $self->form->{'processing'} = 1;
+    } else {
+      delete $self->form->{'processing'};
+    }
+  }
+
+Note thate for this example the form key "processing" was deleted.  This
+is so that the call to fill in any html forms won't swap in a value of
+zero for form elements named "processing."
+
+=item Hook C<-E<gt>validate>
+
+Runs validation on the information posted in $self->form.  Uses
+CGI::Ex::Validate for the validation.  Calls the hook hash_validation
+to load validation information.  Should return true if enough
+information is present to run validate.  Errors are stored as a hash
+in $self->{hash_errors} via method add_errors and can be checked for
+at a later time with method has_errors (if the default validate was
+used).
+
+Upon success, it will look through all of the items which
+were validated, if any of them contain the keys append_path, insert_path,
+or replace_path, that method will be called with the value as arguments.
+This allows for the validation to apply redirection to the path.  A
+validation item of:
+
+  {field => 'foo', required => 1, append_path => ['bar', 'baz']}
+
+would append 'bar' and 'baz' to the path should all validation succeed.
+
+=item Hook C<-E<gt>hash_validation>
+
+Returns a hash of the validation information to check form against.
+By default, will look for a filename using the hook file_val and will
+pass it to CGI::Ex::Validate::get_validation.  If no file_val is
+returned or if the get_validation fails, an empty hash will be returned.
+Validation is implemented by ->vob which loads a CGI::Ex::Validate object.
+
+=item Hook C<-E<gt>file_val>
+
+Returns a filename containing the validation.  Adds method
+base_dir_rel to hook name_module, and name_step and adds on the
+default file extension found in $self->ext_val which defaults to the
+global $EXT_VAL (the property $self->{ext_val} may also be set).  File
+should be readible by CGI::Ex::Validate::get_validation.
+
+=item Hook C<-E<gt>js_validation>
+
+Requires YAML.pm.
+Will return Javascript that is capable of validating the form.  This
+is done using the capabilities of CGI::Ex::Validate.  This will call
+the hook hash_validation which will then be encoded into yaml and
+placed in a javascript string.  It will also call the hook form_name
+to determine which html form to attach the validation to.  The method
+js_uri_path is called to determine the path to the appropriate
+yaml_load.js and validate.js files.  If the method ext_val is htm,
+then js_validation will return an empty string as it assumes the htm
+file will take care of the validation itself.  In order to make use
+of js_validation, it must be added to either the hash_base, hash_common, hash_swap or
+hash_form hook (see examples of hash_base used in this doc).
+
+=item Hook C<-E<gt>form_name>
+
+Return the name of the form to attach the js validation to.  Used by
+js_validation.
+
+=item Method C<-E<gt>js_uri_path>
+
+Return the URI path where the CGI/Ex/yaml_load.js and
+CGI/Ex/validate.js files can be found.  This will default to
+"$ENV{SCRIPT_NAME}/js" if the path method has not been overridden,
+otherwise it will default to "$ENV{SCRIPT_NAME}?step=js&js=" (the
+latter is more friendly with overridden paths).  A default handler for
+the "js" step has been provided in "js_run_step" (this handler will
+nicely print out the javascript found in the js files which are
+included with this distribution - if valid_steps is defined, it must
+include the step "js" - js_run_step will work properly with the
+default "path" handler.
+
+=item Hook C<-E<gt>hash_swap>
+
+Called in preparation for print after failed prepare, info_complete,
+or finalize.  Should contain a hash of any items needed to be swapped
+into the html during print.  Will be merged with hash_base, hash_common, hash_form,
+and hash_errors.  Can be populated by passing a hash to ->add_to_swap.
+
+=item Hook C<-E<gt>hash_form>
+
+Called in preparation for print after failed prepare, info_complete,
+or finalize.  Defaults to ->form.  Can be populated by passing a hash
+to ->add_to_form.
+
+=item Hook C<-E<gt>hash_fill>
+
+Called in preparation for print after failed prepare, info_complete,
+or finalize.  Should contain a hash of any items needed to be filled
+into the html form during print.  Items from hash_form, hash_base, and hash_common
+will be layered on top during a print cycle.  Can be populated by passing
+a hash to ->add_to_fill.
+
+By default - forms are sticky and data from previous requests will
+try and populate the form.  There is a method called ->no_fill which
+will turn off sticky forms.
+
+=item Method C<-E<gt>no_fill>
+
+Passed the current step.  Should return boolean value of whether or not
+to fill in the form on the printed page. (prevents sticky forms)
+
+=item Hook C<-E<gt>hash_errors>
+
+Called in preparation for print after failed prepare, info_complete,
+or finalize.  Should contain a hash of any errors that occured.  Will
+be merged into hash_form before the pass to print.  Eash error that
+occured will be passed to method format_error before being added to
+the hash.  If an error has occurred, the default validate will
+automatically add {has_errors =>1}.  To the error hash at the time of
+validation.  has_errors will also be added during the merge incase the
+default validate was not used.  Can be populated by passing a hash to
+->add_to_errors or ->add_errors.
+
+=item Hook C<-E<gt>hash_common>
+
+Almost identical in function and purpose to hash_base.  It is
+intended that hash_base be used for common items used in various
+scripts inheriting from a common CGI::Ex::App type parent.  Hash_common
+is more intended for step level populating of both swap and fill.
+
+=item Hook C<-E<gt>hash_base>
+
+A hash of base items to be merged with hash_form - such as pulldown
+menues.  It will now also be merged with hash_fill, so it can contain
+default fillins.  Can be populated by passing a hash to ->add_to_base.
+By default the following sub is what is used for hash_common (or something
+similiar).  Note the use of values that are code refs - so that the
+js_validation and form_name hooks are only called if requested:
+
+  sub hash_base {
+    my ($self, $step) = @_;
+    return $self->{hash_base} ||= {
+      script_name   => $ENV{SCRIPT_NAME},
+      js_validation => sub { $self->run_hook('js_validation', $step) },
+      form_name     => sub { $self->run_hook('form_name', $step) },
+    };
+  }
+
+=item Hook C<-E<gt>name_module>
+
+Return the name (relative path) that should be prepended to name_step
+during the default file_print and file_val lookups.  Defaults to
+base_name_module.
+
+=item Hook C<-E<gt>name_step>
+
+Return the step (appended to name_module) that should used when
+looking up the file in file_print and file_val lookups.  Defaults to
+the current step.
+
+=item Hook C<-E<gt>file_print>
+
+Returns a filename of the content to be used in the default print
+hook.  Adds method base_dir_rel to hook name_module, and name_step and
+adds on the default file extension found in $self->ext_print which
+defaults to the global $EXT_PRINT (the property $self->{ext_print} may
+also be set).  Should be a file that can be handled by hook print.
+
+=item Hook C<-E<gt>print>
+
+Take the information generated by prepared_print, format it, and print it out.
+Default incarnation uses Template::Toolkit.  Arguments are: step name, form hashref,
+and fill hashref.
+
+=item Hook C<-E<gt>prepared_print>
+
+Called when any of prepare, info_complete, or finalize fail.  Prepares
+a form hash and a fill hash to pass to print.  The form hash is primarily
+intended for use by the templating system.  The fill hash is intended
+to be used to fill in any html forms.
+
+=item Hook C<-E<gt>post_print>
+
+A hook which occurs after the printing has taken place.  Is only run
+if the information was not complete.  Useful for printing rows of a
+database query.
+
+=item Hook C<-E<gt>post_step>
+
+Ran at the end of the step's loop if prepare, info_complete, and
+finalize all returned true.  Allows for cleanup.  If a true value is
+returned, execution of navigate is returned and no more steps are
+processed.
+
+=item Method C<-E<gt>post_loop>
+
+Ran after all of the steps in the loop have been processed (if
+prepare, info_complete, and finalize were true for each of the steps).
+If it returns a true value the navigation loop will be aborted.  If it
+does not return true, navigation continues by then inserting the step
+$self->default_step and running $self->nav_loop again (recurses) to
+fall back to the default step.
+
+=item Method C<-E<gt>stash>
+
+Returns a hashref that can store arbitrary user space data without
+clobering the internals of the application.
+
+=item Method C<-E<gt>add_property>
+
+Takes the property name as an argument.  Creates an accessor that can
+be used to access a new property.  If there were additional arguments
+they will call the new accessor. Calling the new accessor with an
+argument will set the property.  Using the accessor in an assignment
+will also set the property (it is an lvalue).  Calling the accessor in
+any other way will return the value.
+
+=item Method C<-E<gt>cleanup>
+
+Can be used at the end of execution to tear down the structure.
+Default method starts a cleanup_cross_references call.
+
+=item Method C<-E<gt>cleanup_cross_references>
+
+Used to destroy links in nested structures.  Will spider through the
+data structure of the passed object and remove any blessed objects
+that are no weakly referenced.  This means if you have a reference to
+an object in a global cache, that object should have its reference
+weakened in the global cache.  Requires Scalar::Util to function.  Use
+of this function is highly recommended in mod_perl environments to
+make sure that there are no dangling objects in memory.  There are
+some global caches that can't be fixed (such as Template::Parser's
+reference to Template::Grammar in the Template::Toolkit).  For these
+situations there is a %CLEANUP_EXCLUDE hash that contains the names of
+Object types to exclude from the cleanup process.  Add any such global
+hashes (or objects with references to the global hashes) there.
+
+=back
+
+=head1 OTHER APPLICATION MODULES
+
+The concepts used in CGI::Ex::App are not novel or unique.  However, they
+are all commonly used and very useful.  All application builders were
+built because somebody observed that there are common design patterns
+in CGI building.  CGI::Ex::App differs in that it has found more common design
+patterns of CGI's.
+
+CGI::Ex::App is intended to be sub classed, and sub sub classed, and each step
+can choose to be sub classed or not.  CGI::Ex::App tries to remain simple
+while still providing "more than one way to do it."  It also tries to avoid
+making any sub classes have to call ->SUPER::.
+
+There are certainly other modules for building CGI applications.  The
+following is a short list of other modules and how CGI::Ex::App is
+different.
+
+=over 4
+
+=item C<CGI::Application>
+
+Seemingly the most well know of application builders.
+CGI::Ex::App is different in that it:
+
+  * Uses Template::Toolkit by default
+      CGI::Ex::App can easily use another toolkit by simply
+      overriding the ->print method.
+      CGI::Application uses HTML::Template.
+  * Offers integrated data validation.
+      CGI::Application has had custom addons created that
+      add some of this functionality.  CGI::Ex::App has the benefit
+      that once validation is created,
+  * Allows the user to print at any time (so long as proper headers
+      are sent.  CGI::Application requires data to be pipelined.
+  * Offers hooks into the various phases of each step ("mode" in
+      CGI::Application lingo).  CGI::Application essentially
+      provides ->runmode
+  * Support for easily jumping around in navigation steps.
+  * Support for storing some steps in another package.
+
+CGI::Ex::App and CGI::Application are similar in that they take care
+of handling headers and they allow for calling other "runmodes" from
+within any given runmode.  CGI::Ex::App's ->run_step is essentially
+equivalent to a method call defined in CGI::Application's ->run_modes.
+The ->run method of CGI::Application starts the application in the same
+manner as CGI::Ex::App's ->navigate call.  Many of the hooks around
+CGI::Ex::App's ->run_step call are similar in nature to those provided by
+CGI::Application.
+
+=item C<CGI::Prototype>
+
+There are actually many simularities.  One of the nicest things about
+CGI::Prototype is that it is extremely short (very very short).  The
+->activate starts the application in the same manner as CGI::Ex::App's
+=>navigate call.  Both use Template::Tookit as the default template system.
+CGI::Ex::App is differrent in that it:
+
+  * Offers integrated data validation.
+      CGI::Application has had custom addons created that
+      add some of this functionality.  CGI::Ex::App has the benefit
+      that once validation is created,
+  * Offers more hooks into the various phases of each step.
+  * Support for easily jumping around in navigation steps.
+  * Support for storing some steps in another package.
+
+=item C<CGI::Path>
+
+CGI::Path and CGI::Ex::App are fairly similar in may ways as they
+were created under similar lines of thought.  The primary difference
+in these two is that CGI::Ex::App:
+
+  * Does not provide "automated path following" based on
+      validated key information.  CGI::Path works well for
+      wizard based applications.  CGI::Ex::App assumes that
+      the application will chose it's own path (it works very
+      well in non-linear paths - it also works fine in
+      linear paths but it doesn't provide some of magic that
+      CGI::Path provides).
+  * Does not provide integrated session support.  CGI::Path
+      requires it for path navigation.  CGI::Ex::App assumes that
+      if session support or authentication is needed by an
+      application, a custom Application layer that inherits
+      from CGI::Ex::App will be written to provide this support.
+  * Offers more granularity in the navigation phase.  CGI::Path
+      has successfully been used as a sub class of CGI::Ex::App
+      with limited modifications.
+
+=back
+
+=head1 BUGS
+
+Uses CGI::Ex for header support by default - which means that support
+for mod_perl 2 is limited at this point.
+
+There are a lot of hooks.  Actually this is not a bug.  Some may
+prefer not calling as many hooks - they just need to override
+methods high in the chain and subsequent hooks will not be called.
+
+=head1 THANKS
+
+Bizhosting.com - giving a problem that fit basic design patterns.
+Earl Cahill    - pushing the idea of more generic frameworks.
+Adam Erickson  - design feedback, bugfixing, feature suggestions.
+James Lance    - design feedback, bugfixing, feature suggestions.
+
+=head1 AUTHOR
+
+Paul Seamons
+
+=cut
diff --git a/lib/CGI/Ex/Auth.pm b/lib/CGI/Ex/Auth.pm
new file mode 100644 (file)
index 0000000..e564efd
--- /dev/null
@@ -0,0 +1,831 @@
+package CGI::Ex::Auth;
+
+### CGI Extended Application
+
+###----------------------------------------------------------------###
+#  Copyright 2004 - Paul Seamons                                     #
+#  Distributed under the Perl Artistic License without warranty      #
+###----------------------------------------------------------------###
+
+### See perldoc at bottom
+
+
+use strict;
+use vars qw($USE_PLAINTEXT
+            $CHECK_CRYPTED
+            $EXPIRE_LOGINS
+            $FAILED_SLEEP
+            $VERSION
+            );
+
+use CGI::Ex::Dump qw(debug);
+use MIME::Base64 qw(encode_base64 decode_base64);
+
+BEGIN {
+  $VERSION = '0.10';
+  $CHECK_CRYPTED = 1        if ! defined $CHECK_CRYPTED;
+  $FAILED_SLEEP  = 2        if ! defined $FAILED_SLEEP;
+  $EXPIRE_LOGINS = 6 * 3600 if ! defined $EXPIRE_LOGINS;
+  #if ($ENV{MOD_PERL}) {
+  #  require Digest::SHA1;
+  #  require Digest::MD5;
+  #}
+}
+
+###----------------------------------------------------------------###
+
+sub new {
+  my $class = shift || __PACKAGE__;
+  my $self  = ref($_[0]) ? shift : {@_};
+  bless $self, $class;
+  $self->init();
+  return $self;
+}
+
+sub init {}
+
+###----------------------------------------------------------------###
+
+sub require_auth {
+  my $self = shift;
+  $self = __PACKAGE__->new($self) if ! UNIVERSAL::isa($self, __PACKAGE__);
+
+  ### shortcut that will print a js file as needed
+  if ($ENV{PATH_INFO} && $ENV{PATH_INFO} =~ m|^/js/(CGI/Ex/\w+\.js)$|) {
+    $self->cgix->print_js($1);
+    return 0;
+  }
+
+  my $form    = $self->form;
+  my $cookies = $self->cookies;
+  my $key_l   = $self->key_logout;
+  my $key_c   = $self->key_cookie;
+  my $key_u   = $self->key_user;
+  my $key_p   = $self->key_pass;
+  my $key_chk = $self->key_cookie_check;
+  my $had_form_info = 0;
+
+  ### if they've passed us information - try and use it
+  if ($form->{$key_l}) {
+    $self->delete_cookie;
+
+  } elsif (exists($form->{$key_u}) && exists($form->{$key_p})) {
+    if ($self->verify_userpass($form->{$key_u}, $form->{$key_p})) {
+      my $has_cookies = scalar keys %$cookies;
+      my $user  = $form->{$key_u};
+      my $str   = encode_base64(join(":", delete($form->{$key_u}), delete($form->{$key_p})), "");
+      my $key_s = $self->key_save;
+      $self->set_cookie($str, delete($form->{$key_s}));
+      #return $self->success($user); # assume that cookies will work - if not next page will cause login
+      #### this may actually be the nicer thing to do in the common case - except for the nasty looking
+      #### url - all things considered - should really get location boucing to work properly while being
+      #### able to set a cookie at the same time
+
+      if ($has_cookies) {
+        return $self->success($user); # assuming if they have cookies - the one we set will work
+      } else {
+        $form->{$key_chk} = time();
+        my $key_r = $self->key_redirect;
+        if (! $form->{$key_r}) {
+          my $script = $ENV{SCRIPT_NAME} || die "Missing SCRIPT_NAME";
+          my $info   = $ENV{PATH_INFO} || '';
+          my $query  = $self->cgix->make_form($form);
+          $form->{$key_r} = $script . $info . ($query ? "?$query" : "");
+        }
+        $self->location_bounce($form->{$key_r});
+        return 0;
+      }
+    } else {
+      $had_form_info = 1;
+      $self->delete_cookie;
+    }
+
+  ### otherwise look for an already set cookie
+  } elsif ($cookies->{$key_c}) {
+    my ($user, $pass) = split /:/, decode_base64($cookies->{$key_c}), 2;
+    return $self->success($user) if $self->verify_userpass($user, $pass);
+    $self->delete_cookie;
+
+  ### cases to handle no cookies
+  } elsif ($form->{$key_chk}) {
+    my $value = delete $form->{$key_chk};
+    if ($self->allow_htauth) {
+      die "allow_htauth is not implemented - yet";
+    } elsif (abs(time() - $value) < 3600) {
+      # fail down to below where we ask for auth
+      # this is assuming that all webservers in the cluster are within 3600 of each other
+    } else {
+      $self->hook_print("no_cookies", $form);
+      return 0;
+    }
+  }
+
+  ### oh - you're still here - well then - ask for login credentials
+  my $key_r = $self->key_redirect;
+  if (! $form->{$key_r}) {
+    my $script = $ENV{SCRIPT_NAME} || die "Missing SCRIPT_NAME";
+    my $info   = $ENV{PATH_INFO} || '';
+    my $query  = $self->cgix->make_form($form);
+    $form->{$key_r} = $script . $info . ($query ? "?$query" : "");
+  }
+  $form->{login_error} = $had_form_info;
+  $self->hook_print("get_login_info", $form);
+  return 0;
+}
+
+###----------------------------------------------------------------###
+
+sub hook_print {
+  my $self = shift;
+  my $page = shift;
+  my $form = shift;
+
+  ### copy the form and add various pieces
+  my $FORM = {%$form};
+  $FORM->{payload}      = $self->payload;
+  $FORM->{error}        = ($form->{login_error}) ? "Login Failed" : "";
+  $FORM->{key_user}     = $self->key_user;
+  $FORM->{key_pass}     = $self->key_pass;
+  $FORM->{key_save}     = $self->key_save;
+  $FORM->{key_redirect} = $self->key_redirect;
+  $FORM->{form_name}    = $self->form_name;
+  $FORM->{script_name}  = $ENV{SCRIPT_NAME};
+  $FORM->{path_info}    = $ENV{PATH_INFO} || '';
+  $FORM->{login_script} = $self->login_script($FORM);
+  delete $FORM->{$FORM->{key_pass}};
+
+  ### allow for custom hook
+  if (my $meth = $self->{hook_print}) {
+    $self->$meth($page, $FORM);
+    return 0;
+  }
+
+  ### no hook - give basic functionality
+  my $content;
+  if ($page eq 'no_cookies') {
+    $content = qq{<div style="border: 2px solid black;background:red;color:white">You do not appear to have cookies enabled.</div>};
+  } elsif ($page eq 'get_login_info') {
+    $content = $self->basic_login_page($FORM);
+  } else {
+    $content = "No content for page \"$page\"";
+  }
+
+  $self->cgix->print_content_type();
+  print $content;
+  return 0;
+}
+
+###----------------------------------------------------------------###
+
+sub success {
+  my $self = shift;
+  my $user = shift;
+  $self->{user} = $ENV{REMOTE_USER} = $user;
+  $self->hook_success($user);
+  return 1;
+}
+
+sub user {
+  my $self = shift;
+  return $self->{user};
+}
+
+sub hook_success {
+  my $self = shift;
+  my $user = shift;
+  my $meth;
+  if ($meth = $self->{hook_success}) {
+    $self->$meth($user);
+  }
+}
+
+###----------------------------------------------------------------###
+
+sub delete_cookie {
+  my $self  = shift;
+  my $key_c = $self->key_cookie;
+  $self->cgix->set_cookie({
+    -name    => $key_c,
+    -value   => '',
+    -expires => '-10y',
+    -path    => '/',
+  });
+}      
+
+sub set_cookie {
+  my $self  = shift;
+  my $key_c = $self->key_cookie;
+  my $value = shift || '';
+  my $save_pass = shift;
+  $self->cgix->set_cookie({
+    -name    => $key_c,
+    -value   => $value,
+    ($save_pass ? (-expires => '+10y') : ()),
+    -path    => '/',
+  });
+}
+
+sub location_bounce {
+  my $self = shift;
+  my $url = shift;
+  return $self->cgix->location_bounce($url);
+}
+
+###----------------------------------------------------------------###
+
+sub key_logout {
+  my $self = shift;
+  $self->{key_logout} = shift if $#_ != -1;
+  return $self->{key_logout} ||= 'logout';
+}
+
+sub key_cookie {
+  my $self = shift;
+  $self->{key_cookie} = shift if $#_ != -1;
+  return $self->{key_cookie} ||= 'ce_auth';
+}
+
+sub key_cookie_check {
+  my $self = shift;
+  $self->{key_cookie_check} = shift if $#_ != -1;
+  return $self->{key_cookie_check} ||= 'ccheck';
+}
+
+sub key_user {
+  my $self = shift;
+  $self->{key_user} = shift if $#_ != -1;
+  return $self->{key_user} ||= 'ce_user';
+}
+
+sub key_pass {
+  my $self = shift;
+  $self->{key_pass} = shift if $#_ != -1;
+  return $self->{key_pass} ||= 'ce_pass';
+}
+
+sub key_save {
+  my $self = shift;
+  $self->{key_save} = shift if $#_ != -1;
+  return $self->{key_save} ||= 'ce_save';
+}
+
+sub key_redirect {
+  my $self = shift;
+  $self->{key_redirect} = shift if $#_ != -1;
+  return $self->{key_redirect} ||= 'redirect';
+}
+
+sub form_name {
+  my $self = shift;
+  $self->{form_name} = shift if $#_ != -1;
+  return $self->{form_name} ||= 'ce_form';
+}
+
+sub allow_htauth {
+  my $self = shift;
+  $self->{allow_htauth} = shift if $#_ != -1;
+  return $self->{allow_htauth} ||= 0;
+}
+
+sub payload {
+  my $self = shift;
+  my $user = shift;
+  my $time = shift || time();
+  my $meth;
+  my @payload = ($time);
+  if ($meth = $self->{hook_payload}) {
+    push @payload, $self->$meth($user);
+  }
+  return join "/", @payload;
+}
+
+###----------------------------------------------------------------###
+
+sub verify_userpass {
+  my $self = shift;
+  my $user = shift;
+  my $pass = shift;
+  my $host = shift || $self->host;
+  my $meth;
+  if ($meth = $self->{hook_verify_userpass}) {
+    return $self->$meth($user, $pass, $host);
+  } else {
+    return $self->hook_verify_userpass($user, $pass, $host);
+  }
+}
+
+sub hook_verify_userpass {
+  my $self = shift;
+  my $user = shift;
+  my $pass_test = shift;
+  my $host = shift || $self->host;
+
+  return undef if ! defined $user;
+  return undef if ! defined $pass_test;
+  my $pass_real = $self->hook_get_pass_by_user($user, $host);
+  return undef if ! defined $pass_real;
+
+  my $type_real = ($pass_real =~ m/^(md5|sha1)\((.+)\)$/) ? $1 : 'plainorcrypt';
+  my $hash_real = $2;
+  my $type_test = ($pass_test =~ m/^(md5|sha1)\((.+)\)$/) ? $1 : 'plainorcrypt';
+  my $hash_test = $2;
+
+  ### if both types were plaintext - check if the equal
+  if ($type_real eq 'plainorcrypt'
+      && $type_test eq 'plainorcrypt') {
+    return 1 if $pass_real eq $pass_test;
+    if ($CHECK_CRYPTED && $pass_real =~ m|^([./0-9A-Za-z]{2})(.{,11})$|) {
+      return 1 if crypt($pass_test, $1) eq $pass_real;
+    }
+    return 0;
+
+  } else {
+    ### if test type is plaintext - then hash it and compare it alone
+    if ($type_test eq 'plainorcrypt') {
+      $pass_test = $self->enc_func($type_real, $pass_test); # encode same as real
+      $pass_test = "$type_real($pass_test)";
+      return $pass_test eq $pass_real;
+
+    ### if real type is plaintext - then hash it to get ready for test
+    } elsif ($type_real eq 'plainorcrypt') {
+      $pass_real = $self->enc_func($type_test, $pass_real);
+      $pass_real = "$type_test($pass_real)";
+      $type_real = $type_test;
+    }
+    
+    ### the types should be the same (unless a system stored sha1 and md5 passwords)
+    if ($type_real ne $type_test) {
+      warn "Test types for user \"$user\" are of two different types - very bad";
+      return 0;
+    }
+
+    ### no payload - compare directly
+    if ($hash_test !~ m|^(.+)/([^/]+)$|) {
+      return lc($pass_test) eq lc($pass_real);
+
+    ### and finally - check the payload (allows for expiring login)
+    } else {
+      my $payload = $1; # payload can be anything
+      my $compare = $2; # a checksum which is the enc of the payload + '/' + enc of password
+      my @payload = split /\//, $payload;
+
+      return 0 if $self->enc_func($type_test, "$payload/$hash_real") ne $compare;
+
+      ### if no save password && greater than expire time- expire
+      if ($EXPIRE_LOGINS && ! $payload[1] && $payload[0] =~ m/^(\d+)/) {
+        return 0 if time() > $1 + $EXPIRE_LOGINS;
+      }
+      return 1;
+    }
+  }
+  return 0; # nothing should make it this far
+}
+
+sub enc_func {
+  my $self = shift;
+  my $type = shift;
+  my $str  = shift;
+  if ($type eq 'md5') {
+    require Digest::MD5;
+    return &Digest::MD5::md5_hex($str);
+  } elsif ($type eq 'sha1') {
+    require Digest::SHA1;
+    return &Digest::SHA1::sha1_hex($str);
+  }
+}
+
+sub set_hook_get_pass_by_user {
+  my $self = shift;
+  $self->{hook_get_pass_by_user} = shift;
+}
+
+sub hook_get_pass_by_user {
+  my $self = shift;
+  my $user = shift;
+  my $host = shift || $self->host;
+  my $meth;
+  if ($meth = $self->{hook_get_pass_by_user}) {
+    return $self->$meth($user, $host);
+  }
+  die "hook_get_pass_by_user is a virtual method - please override - or use set_hook_get_pass_by_user";
+}
+
+###----------------------------------------------------------------###
+
+sub cgix {
+  my $self = shift;
+  $self->{cgix} = shift if $#_ != -1;
+  return $self->{cgix} ||= do {
+    require CGI::Ex;
+    CGI::Ex->new(); # return of the do
+  };
+}
+
+sub form {
+  my $self = shift;
+  if ($#_ != -1) {
+    $self->{form} = shift || die "Invalid form";
+  }
+  return $self->{form} ||= $self->cgix->get_form;
+}
+
+sub cookies {
+  my $self = shift;
+  if ($#_ != -1) {
+    $self->{cookies} = shift || die "Invalid cookies";
+  }
+  return $self->{cookies} ||= $self->cgix->get_cookies;
+}
+
+sub host {
+  my $self = shift;
+  return $self->{host} = shift if $#_ != -1;
+  return $self->{host} ||= do {
+    my $host = $ENV{HTTP_HOST} || die "Missing \$ENV{HTTP_HOST}";
+    $host = lc($host);
+    $host =~ s/:\d*$//;      # remove port number
+    $host =~ s/\.+$//;       # remove qualified dot
+    $host =~ s/[^\w\.\-]//g; # remove odd characters
+    $host; # return of the do
+  };
+}
+
+###----------------------------------------------------------------###
+
+sub basic_login_page {
+  my $self = shift;
+  my $form = shift;
+
+  my $text = $self->basic_login_template();
+  $self->cgix->swap_template(\$text, $form);
+  $self->cgix->fill(\$text, $form);
+
+  return $text;
+}
+
+sub basic_login_template {
+  return qq{
+    [% header %]
+    <div align="center">
+    <span class="error" style="color:red">[% error %]</span>
+    <form name="[% form_name %]" method="get" action="[% script_name %]">
+    <table border="0" class="login_table">
+    <tr>
+      <td>Username:</td>
+      <td><input name="[% key_user %]" type="text" size="30" value=""></td>
+    </tr>
+    <tr>
+      <td>Password:</td>
+      <td><input name="[% key_pass %]" type="password" size="30" value=""></td>
+    </tr>
+    <tr>
+      <td colspan="2">
+        <input type="checkbox" name="[% key_save %]" value="1"> Save Password ?
+      </td>
+    </tr>
+    <tr>
+      <td colspan="2" align="right">
+        <input type="hidden" name="[% key_redirect %]">
+        <input type="hidden" name="payload">
+        <input type="submit" value="Submit">
+      </td>
+    </tr>
+    [% extra_table %]
+    </table>
+    </form>
+    </div>
+    [% login_script %]
+    [% footer %]
+  };
+}
+
+sub login_type {
+  my $self = shift;
+  if ($#_ != -1) {
+    $self->{login_type} = defined($_[0]) ? lc(shift) : undef;
+  }
+  $self->{login_type} = do {
+    my $type;
+    if ($USE_PLAINTEXT) {
+      $type = '';
+    } elsif (eval {require Digest::SHA1}) {
+      $type = 'sha1';
+    } elsif (eval {require Digest::MD5}) {
+      $type = 'md5';
+    } else {
+      $type = "";
+    }
+    $type; # return of the do
+  } if ! defined $self->{login_type};
+  return $self->{login_type};
+}
+
+
+sub login_script {
+  my $self = shift;
+  my $form = shift;
+  my $type = $self->login_type;
+  return if ! $type || $type !~ /^(sha1|md5)$/;
+
+  return qq{
+    <script src="$form->{script_name}/js/CGI/Ex/$type.js"></script>
+    <script>
+    function send_it () {
+      var f = document.$form->{form_name};
+      var s = (f.$form->{key_save}.checked) ? 1 : 0;
+      var l = f.payload.value + '/' + s;
+      var r = f.$form->{key_redirect}.value;
+      var q = document.$form->{form_name}.action;
+      var sum = document.${type}_hex(l+'/'+document.${type}_hex(f.$form->{key_pass}.value));
+      q += '?$form->{key_user}='+escape(f.$form->{key_user}.value);
+      q += '&$form->{key_save}='+escape(s);
+      q += '&$form->{key_pass}='+escape('$type('+l+'/'+sum+')');
+      location.href = q;
+      return false;
+    }
+    if (document.${type}_hex) document.$form->{form_name}.onsubmit = function () { return send_it() }
+    </script>
+  };
+}
+
+###----------------------------------------------------------------###
+
+### return arguments to add on to a url to allow login (for emails)
+sub auth_string_sha1 {
+  my $self = shift;
+  my $user = shift;
+  my $pass = shift;
+  my $save = shift || 0;
+  my $time = shift || time;
+  my $payload = $self->payload($time);
+
+  require Digest::SHA1;
+
+  if ($pass =~ /^sha1\((.+)\)$/) {
+    $pass = $1;
+  } else {
+    $pass = &Digest::SHA1::sha1_hex($pass);
+  }
+  $pass = &Digest::SHA1::sha1_hex("$payload/$save/$pass");
+
+  return $self->cgix->make_form({
+    $self->key_user => $user,
+    $self->key_pass => "sha1($payload/$save/$pass)",
+    $self->key_save => $save,
+  });
+}
+
+###----------------------------------------------------------------###
+
+1;
+
+__END__
+
+=head1 NAME
+
+CGI::Ex::Auth - Handle logins nicely.
+
+=head1 SYNOPSIS
+
+  ### authorize the user
+  my $auth = $self->auth({
+    hook_get_pass_by_user => \&get_pass_by_user,
+    hook_print            => \&my_print,
+    login_type            => 'sha1',
+  });
+  ### login_type may be sha1, md5, or plaintext
+
+
+  sub get_pass_by_user {
+    my $auth = shift;
+    my $username = shift;
+    my $host = shift;
+    my $password = some_way_of_getting_password;
+    return $password;
+  }
+
+  sub my_print {
+    my $auth = shift;
+    my $step = shift;
+    my $form = shift; # form includes login_script at this point
+    my $content = get_content_from_somewhere;
+    $auth->cgix->swap_template(\$content, $form);
+    $auth->cgix->print_content_type;
+    print $content;
+  }
+
+=head1 DESCRIPTION
+
+CGI::Ex::Auth allows for autoexpiring, safe logins.  Auth uses
+javascript modules that perform SHA1 and MD5 encoding to encode
+the password on the client side before passing them through the
+internet.
+
+If SHA1 is used the storage of the password can be described by
+the following code:
+
+  my $pass = "plaintextpassword";
+  my $save = ($save_the_password) ? 1 : 0;
+  my $time = time;
+  my $store = sha1_hex("$time/$save/" . sha1_hex($pass));
+
+This allows for passwords to be stored as sha1 in a database.
+Passwords stored in the database this way are still susceptible to bruteforce
+attack, but are much more secure than storing plain text.
+
+If MD5 is used, the above procedure is replaced with md5_hex.
+
+A downside to this module is that it does not use a session to preserve state
+so authentication has to happen on every request.  A plus is that you don't
+need to use a session.  With later releases, a method will be added to allow
+authentication to look inside of a stored session somewhat similar to
+CGI::Session::Auth.
+
+=head1 METHODS
+
+=over 4
+
+=item C<new>
+
+Constructor.  Takes a hash or hashref of properties as arguments.
+
+=item C<init>
+
+Called automatically near the end of new.
+
+=item C<require_auth>
+
+Performs the core logic.  Returns true on successful login.
+Returns false on failed login.  If a false value is returned,
+execution of the CGI should be halted.  require_auth WILL
+NOT automatically stop execution.
+
+  $auth->require_auth || exit;
+
+=item C<hook_print>
+
+Called if login failed.  Defaults to printing a very basic page.
+You will want to override it with a template from your own system.
+The hook that is called will be passed the step to print (currently
+only "get_login_info" and "no_cookies"), and a hash containing the
+form variables as well as the following:
+
+  payload      - $self->payload
+  error        - The error that occurred (if any)
+  key_user     - $self->key_user;
+  key_pass     - $self->key_pass;
+  key_save     - $self->key_save;
+  key_redirect - $self->key_redirect;
+  form_name    - $self->form_name;
+  script_name  - $ENV{SCRIPT_NAME}
+  path_info    - $ENV{PATH_INFO} || ''
+  login_script - $self->login_script($FORM); # The javascript that does the login
+
+=item C<success>
+
+Method called on successful login.  Sets $self->user as well as $ENV{REMOTE_USER}.
+
+=item C<user>
+
+Returns the user that was successfully logged in (undef if no success).
+
+=item C<hook_success>
+
+Called from success.  May be overridden or a subref may be given as a property.
+
+=item C<key_logout>
+
+If a key is passed the form hash that matches this key, the current user will
+be logged out.  Default is "logout".
+
+=item C<key_cookie>
+
+The name of the auth cookie.  Default is "ce_auth".
+
+=item C<key_cookie_check>
+
+A field name used during a bounce to see if cookies exist.  Default is "ccheck".
+
+=item C<key_user>
+
+The form field name used to pass the username.  Default is "ce_user".
+
+=item C<key_pass>
+
+The form field name used to pass the password.  Default is "ce_pass".
+
+=item C<key_save>
+
+The form field name used to pass whether they would like to save the cookie for
+a longer period of time.  Default is "ce_save".  The value of this form field
+should be 1 or 0.  If it is zero, the cookie installed will be a session cookie
+and will expire in $EXPIRE_LOGINS seconds (default of 6 hours).
+
+=item C<form_name>
+
+The name of the html login form to attach the javascript to.  Default is "ce_form".
+
+=item C<payload>
+
+Additional variables to store in the cookie.  Can be used for anything.  Should be
+kept small.  Default is time (should always use time as the first argument).  Used
+for autoexpiring the cookie and to prevent bruteforce attacks.
+
+=item C<verify_userpass>
+
+Called to verify the passed form information or the stored cookie.  Calls hook_verify_userpass.
+
+=item C<hook_verify_userpass>
+
+Called by verify_userpass.  Arguments are the username, cookie or info to be tested,
+and the hostname.  Default method calls hook_get_pass_by_user to get the real password.
+Then based upon how the real password is stored (sha1, md5, plaintext, or crypted) and
+how the login info was passed from the html form (or javascript), will attempt to compare
+the two and return success or failure.  It should be noted that if the javascript method
+used is SHA1 and the password is stored crypted or md5'ed - the comparison will not work
+and the login will fail.  SHA1 logins require either plaintext password or sha1 stored passwords.
+MD5 logins require either plaintext password or md5 stored passwords.  Plaintext logins
+allow for SHA1 or MD5 or crypted or plaintext storage - but should be discouraged because
+they are plaintext and the users password can be discovered.
+
+=item C<hook_get_pass_by_user>
+
+Called by hook_verify_userpass.  Arguments are the username and hostname.  Should return
+a sha1 password, md5 password, plaintext password, or crypted password depending
+upon which system is being used to get the information from the user.
+
+=item C<set_hook_get_pass_by_user>
+
+Allows for setting the subref used by hook_get_pass_by_user.x
+
+=item C<cgix>
+
+Returns a CGI::Ex object.
+
+=item C<form>
+
+A hash of passed form info.  Defaults to CGI::Ex::get_form.
+
+=item C<cookies>
+
+The current cookies.  Defaults to CGI::Ex::get_cookies.
+
+=item C<host>
+
+What host are we on.  Defaults to a cleaned $ENV{HTTP_HOST}.
+
+=item C<basic_login_page>
+
+Calls the basic_login_template, swaps in the form variables (including
+form name, login_script, etc).  Then prints content_type, the content, and
+returns.
+
+=item C<basic_login_template>
+
+Returns a bare essentials form that will handle the login.  Has place
+holders for all of the form name, and login variables, and errors and
+login javascript.  Variable place holders are of the form
+[% login_script %] which should work with Template::Toolkit or CGI::Ex::swap_template.
+
+=item C<login_type>
+
+Either sha1, md5, or plaintext.  If global $USE_PLAINTEXT is set,
+plaintext password will be used.  login_type will then look for
+Digest::SHA1, then Digest::MD5, and then fail to plaintext.
+
+SHA1 comparison will work with passwords stored as plaintext password,
+or stored as the string "sha1(".sha1_hex($password).")".
+
+MD5 comparison will work with passwords stored as plaintext password,
+or stored as the string "md5(".md5_hex($password).")".
+
+Plaintext comparison will work with passwords stored as sha1(string),
+md5(string), plaintext password string, or crypted password.
+
+=item C<login_script>
+
+Returns a chunk of javascript that will encode the password before
+the html form is ever submitted.  It does require that $ENV{PATH_TRANSLATED}
+is not modified before calling the require_auth method so that any
+external javascript files may be served (also by the require_auth).
+
+=item C<auth_string_sha1>
+
+Arguments are username, password, save_password, and time.  This will
+return a valid login string.  You probably will want to pass 1 for the
+save_password or else the login will only be good for 6 hours.
+
+  my $login = $self->auth->auth_string_sha1($user, $pass, 1);
+  my $url   = "http://$ENV{HTTP_HOST}$ENV{SCRIPT_NAME}?$login";
+
+=head1 TODO
+
+Using plaintext allows for the password to be passed in the querystring.
+It should at least be Base64 encoded.  I'll add that soon - BUT - really
+you should be using the SHA1 or MD5 login types.
+
+=head1 AUTHORS
+
+Paul Seamons <perlspam at seamons dot com>
+
+=cut
diff --git a/lib/CGI/Ex/Conf.pm b/lib/CGI/Ex/Conf.pm
new file mode 100644 (file)
index 0000000..c1d256f
--- /dev/null
@@ -0,0 +1,851 @@
+package CGI::Ex::Conf;
+
+### CGI Extended Conf Reader
+
+###----------------------------------------------------------------###
+#  Copyright 2004 - Paul Seamons                                     #
+#  Distributed under the Perl Artistic License without warranty      #
+###----------------------------------------------------------------###
+
+### See perldoc at bottom
+
+use strict;
+use vars qw($VERSION
+            @DEFAULT_PATHS
+            $DEFAULT_EXT
+            %EXT_READERS
+            %EXT_WRITERS
+            $DIRECTIVE
+            $IMMUTABLE_QR
+            $IMMUTABLE_KEY
+            %CACHE
+            $HTML_KEY
+            $DEBUG_ON_FAIL
+            );
+use CGI::Ex::Dump qw(debug dex_warn);
+
+$VERSION = '0.03';
+
+$DEFAULT_EXT = 'conf';
+
+%EXT_READERS = (''         => \&read_handler_yaml,
+                'conf'     => \&read_handler_yaml,
+                'ini'      => \&read_handler_ini,
+                'pl'       => \&read_handler_pl,
+                'sto'      => \&read_handler_storable,
+                'storable' => \&read_handler_storable,
+                'val'      => \&read_handler_yaml,
+                'xml'      => \&read_handler_xml,
+                'yaml'     => \&read_handler_yaml,
+                'yml'      => \&read_handler_yaml,
+                'html'     => \&read_handler_html,
+                'htm'      => \&read_handler_html,
+                );
+
+%EXT_WRITERS = (''         => \&write_handler_yaml,
+                'conf'     => \&write_handler_yaml,
+                'ini'      => \&write_handler_ini,
+                'pl'       => \&write_handler_pl,
+                'sto'      => \&write_handler_storable,
+                'storable' => \&write_handler_storable,
+                'val'      => \&write_handler_yaml,
+                'xml'      => \&write_handler_xml,
+                'yaml'     => \&write_handler_yaml,
+                'yml'      => \&write_handler_yaml,
+                'html'     => \&write_handler_html,
+                'htm'      => \&write_handler_html,
+                );
+
+### $DIRECTIVE controls how files are looked for when namespaces are not absolute.
+### If directories 1, 2 and 3 are passed and each has a config file
+### LAST would return 3, FIRST would return 1, and MERGE will
+### try to put them all together.  Merge behavior of hashes
+### is determined by $IMMUTABLE_\w+ variables.
+$DIRECTIVE = 'LAST'; # LAST, MERGE, FIRST
+
+$IMMUTABLE_QR = qr/_immu(?:table)?$/i;
+
+$IMMUTABLE_KEY = 'immutable';
+
+###----------------------------------------------------------------###
+
+sub new {
+  my $class = shift || __PACKAGE__;
+  my $self  = (@_ && ref($_[0])) ? shift : {@_};
+
+  return bless $self, $class;
+}
+
+sub paths {
+  my $self = shift;
+  return $self->{paths} ||= \@DEFAULT_PATHS;
+}
+
+###----------------------------------------------------------------###
+
+sub read_ref {
+  my $self = shift;
+  my $file = shift;
+  my $args = shift || {};
+  my $ext;
+
+  ### they passed the right stuff already
+  if (ref $file) {
+    if (UNIVERSAL::isa($file, 'SCALAR')) {
+      if ($$file =~ /^\s*</) {
+        return &html_parse_yaml_load($$file, $self, $args); # allow for ref to a YAML string
+      } else {
+        return &yaml_load($$file); # allow for ref to a YAML string
+      }
+    } else {
+      return $file;
+    }
+
+  ### if contains a newline - treat it as a YAML string
+  } elsif (index($file,"\n") != -1) {
+    return &yaml_load($file);
+
+  ### otherwise base it off of the file extension
+  } elsif ($args->{file_type}) {
+    $ext = $args->{file_type};
+  } elsif ($file =~ /\.(\w+)$/) {
+    $ext = $1;
+  } else {
+    $ext = defined($args->{default_ext}) ? $args->{default_ext}
+      : defined($self->{default_ext}) ? $self->{default_ext}
+      : defined($DEFAULT_EXT) ? $DEFAULT_EXT : '';
+    $file = length($ext) ? "$file.$ext" : $file;
+  }
+
+  ### allow for a pre-cached reference
+  if (exists $CACHE{$file} && ! $self->{no_cache}) {
+    return $CACHE{$file};
+  }
+
+  ### determine the handler
+  my $handler;
+  if ($args->{handler}) {
+    $handler = (UNIVERSAL::isa($args->{handler},'CODE'))
+      ? $args->{handler} : $args->{handler}->{$ext};
+  } elsif ($self->{handler}) {
+    $handler = (UNIVERSAL::isa($self->{handler},'CODE'))
+      ? $self->{handler} : $self->{handler}->{$ext};
+  }
+  if (! $handler) {
+    $handler = $EXT_READERS{$ext} || die "Unknown file extension: $ext";
+  }
+
+  return eval { scalar &$handler($file, $self, $args) } || do {
+    debug    "Couldn't read $file: $@" if $DEBUG_ON_FAIL;
+    dex_warn "Couldn't read $file: $@" if ! $self->{no_warn_on_fail};
+    return undef;
+  };
+}
+
+### allow for different kinds of merging of arguments
+### allow for key fallback on hashes
+### allow for immutable values on hashes
+sub read {
+  my $self      = shift;
+  my $namespace = shift;
+  my $args      = shift || {};
+  my $REF       = $args->{ref} || undef;    # can pass in existing set of options
+  my $IMMUTABLE = $args->{immutable} || {}; # can pass existing immutable types
+
+  $self = $self->new() if ! ref $self;
+
+  ### allow for fast short ciruit on path lookup for several cases
+  my $directive;
+  my @paths = ();
+  if (ref($namespace)                   # already a ref
+      || index($namespace,"\n") != -1   # yaml string to read in
+      || $namespace =~ m|^\.{0,2}/.+$|  # absolute or relative file
+      ) {
+    push @paths, $namespace;
+    $directive = 'FIRST';
+
+  ### use the default directories
+  } else {
+    $directive = uc($args->{directive} || $self->{directive} || $DIRECTIVE);
+    $namespace =~ s|::|/|g;  # allow perlish style namespace
+    my $paths = $args->{paths} || $self->paths
+      || die "No paths found during read on $namespace";
+    $paths = [$paths] if ! ref $paths;
+    if ($directive eq 'LAST') { # LAST shall be FIRST
+      $directive = 'FIRST';
+      $paths = [reverse @$paths] if $#$paths != 0;
+    }
+    foreach my $path (@$paths) {
+      next if exists $CACHE{$path} && ! $CACHE{$path};
+      push @paths, "$path/$namespace";
+    }
+  }
+
+  ### make sure we have at least one path
+  if ($#paths == -1) {
+    die "Couldn't find a path for namespace $namespace.  Perhaps you need to pass paths => \@paths";
+  }
+  
+  ### now loop looking for a ref
+  foreach my $path (@paths) {
+    my $ref = $self->read_ref($path, $args) || next;
+    if (! $REF) {
+      if (UNIVERSAL::isa($ref, 'ARRAY')) {
+        $REF = [];
+      } elsif (UNIVERSAL::isa($ref, 'HASH')) {
+        $REF = {};
+      } else {
+        die "Unknown config type of \"".ref($ref)."\" for namespace $namespace";
+      }
+    } elsif (! UNIVERSAL::isa($ref, ref($REF))) {
+      die "Found different reference types for namespace $namespace"
+        . " - wanted a type ".ref($REF);
+    }
+    if (ref($REF) eq 'ARRAY') {
+      if ($directive eq 'MERGE') {
+        push @$REF, @$ref;
+        next;
+      }
+      splice @$REF, 0, $#$REF + 1, @$ref;
+      last;
+    } else {
+      my $immutable = delete $ref->{$IMMUTABLE_KEY};
+      my ($key,$val);
+      if ($directive eq 'MERGE') {
+        while (($key,$val) = each %$ref) {
+          next if $IMMUTABLE->{$key};
+          my $immute = $key =~ s/$IMMUTABLE_QR//o;
+          $IMMUTABLE->{$key} = 1 if $immute || $immutable;
+          $REF->{$key} = $val;
+        }
+        next;
+      }
+      delete $REF->{$key} while $key = each %$REF;
+      while (($key,$val) = each %$ref) {
+        my $immute = $key =~ s/$IMMUTABLE_QR//o;
+        $IMMUTABLE->{$key} = 1 if $immute || $immutable;
+        $REF->{$key} = $val;
+      }
+      last;
+    }
+  }
+  $REF->{"Immutable Keys"} = $IMMUTABLE if scalar keys %$IMMUTABLE;
+  return $REF;
+}
+
+###----------------------------------------------------------------###
+
+sub read_handler_ini {
+  my $file = shift;
+  require Config::IniHash;
+  return &Config::IniHash::ReadINI($file);
+}
+
+sub read_handler_pl {
+  my $file = shift;
+  ### do has odd behavior in that it turns a simple hashref
+  ### into hash - help it out a little bit
+  my @ref = do $file;
+  return ($#ref != 0) ? {@ref} : $ref[0];
+}
+
+sub read_handler_storable {
+  my $file = shift;
+  require Storable;
+  return &Storable::retrieve($file);
+}
+
+sub read_handler_yaml {
+  my $file = shift;
+  local *IN;
+  open (IN, $file) || die "Couldn't open $file: $!";
+  CORE::read(IN, my $text, -s $file);
+  close IN;
+  return &yaml_load($text);
+}
+
+sub yaml_load {
+  my $text = shift;
+  require YAML;
+  my @ret = eval { &YAML::Load($text) };
+  if ($@) {
+    die "$@";
+  }
+  return ($#ret == 0) ? $ret[0] : \@ret;
+}
+
+sub read_handler_xml {
+  my $file = shift;
+  require XML::Simple;
+  return XML::Simple::XMLin($file);
+}
+
+### this handler will only function if a html_key (such as validation)
+### is specified - actually this somewhat specific to validation - but
+### I left it as a general use for other types
+
+### is specified
+sub read_handler_html {
+  my $file = shift;
+  my $self = shift;
+  my $args = shift;
+  if (! eval {require YAML}) {
+    my $err   = $@;
+    my $found = 0;
+    my $i     = 0;
+    while (my($pkg, $file, $line, $sub) = caller($i++)) {
+      return undef if $sub =~ /\bpreload_files$/;
+    }
+    die $err;
+  }
+
+  ### get the html
+  local *IN;
+  open (IN, $file) || return undef;
+  CORE::read(IN, my $html, -s $file);
+  close IN;
+
+  return &html_parse_yaml_load($html, $self, $args);
+}
+
+sub html_parse_yaml_load {
+  my $html = shift;
+  my $self = shift || {};
+  my $args = shift || {};
+  my $key = $args->{html_key} || $self->{html_key} || $HTML_KEY;
+  return undef if ! $key || $key !~ /^\w+$/;
+
+  my $str = '';
+  my @order = ();
+  while ($html =~ m{
+    (document\.    # global javascript
+     | var\s+      # local javascript
+     | <\w+\s+[^>]*?) # input, form, select, textarea tag
+      \Q$key\E   # the key
+      \s*=\s*    # an equals sign
+      ([\"\'])   # open quote
+      (.+?[^\\]) # something in between
+      \2        # close quote
+    }xsg) {
+    my ($line, $quot, $yaml) = ($1, $2, $3);
+    if ($line =~ /^(document\.|var\s)/) { # js variable
+      $yaml =~ s/\\$quot/$quot/g;
+      $yaml =~ s/\\n\\\n?/\n/g;
+      $yaml =~ s/\\\\/\\/g;
+      $yaml =~ s/\s*$/\n/s; # fix trailing newline
+      $str = $yaml; # use last one found
+    } else { # inline attributes
+      $yaml =~ s/\s*$/\n/s; # fix trailing newline
+      if ($line =~ m/<form/i) {
+        $yaml =~ s/^\Q$1\E//m if $yaml =~ m/^( +)/s;
+        $str .= $yaml;
+
+      } elsif ($line =~ m/\bname\s*=\s*('[^\']*'|"[^\"]*"|\S+)/) {
+        my $key = $1;
+        push @order, $key;
+        $yaml =~ s/^/ /mg; # indent entire thing
+        $yaml =~ s/^(\ *[^\s&*\{\[])/\n$1/; # add first newline
+        $str .= "$key:$yaml";
+      }
+    }
+  }
+  $str .= "group order: [".join(", ",@order)."]\n"
+    if $str && $#order != -1 && $key eq 'validation';
+
+  return undef if ! $str;
+  my $ref = eval {&yaml_load($str)};
+  if ($@) {
+    my $err = "$@";
+    if ($err =~ /line:\s+(\d+)/) {
+      my $line = $1;
+      while ($str =~ m/(.+)/gm) {
+        next if -- $line;
+        $err .= "LINE = \"$1\"\n";
+        last;
+      }
+    }
+    debug $err;
+    die $err;
+  }
+  return $ref;
+}
+
+###----------------------------------------------------------------###
+
+### Allow for writing out conf values
+### Allow for writing out the correct filename (if there is a path array)
+### Allow for not writing out immutable values on hashes
+sub write {
+  my $self      = shift;
+  my $namespace = shift;
+  my $conf      = shift || die "Must pass hashref to write out"; # the info to write
+  my $args      = shift || {};
+  my $IMMUTABLE = $args->{immutable} || {}; # can pass existing immutable types
+
+  $self = $self->new() if ! ref $self;
+
+  ### allow for fast short ciruit on path lookup for several cases
+  my $directive;
+  my @paths = ();
+  if (ref($namespace)                   # already a ref
+      || $namespace =~ m|^\.{0,2}/.+$|  # absolute or relative file
+      ) {
+    push @paths, $namespace;
+    $directive = 'FIRST';
+
+  } elsif (index($namespace,"\n") != -1) { # yaml string - can't write that
+    die "Cannot use a yaml string as a namespace for write";
+
+  ### use the default directories
+  } else {
+    $directive = uc($args->{directive} || $self->{directive} || $DIRECTIVE);
+    $namespace =~ s|::|/|g;  # allow perlish style namespace
+    my $paths = $args->{paths} || $self->paths
+      || die "No paths found during write on $namespace";
+    $paths = [$paths] if ! ref $paths;
+    if ($directive eq 'LAST') { # LAST shall be FIRST
+      $directive = 'FIRST';
+      $paths = [reverse @$paths] if $#$paths != 0;
+    }
+    foreach my $path (@$paths) {
+      next if exists $CACHE{$path} && ! $CACHE{$path};
+      push @paths, "$path/$namespace";
+    }
+  }
+
+  ### make sure we have at least one path
+  if ($#paths == -1) {
+    die "Couldn't find a path for namespace $namespace.  Perhaps you need to pass paths => \@paths";
+  }
+
+  my $path;
+  if ($directive eq 'FIRST') {
+    $path = $paths[0];
+  } elsif ($directive eq 'LAST' || $directive eq 'MERGE') {
+    $path = $paths[-1];
+  } else {
+    die "Unknown directive ($directive) during write of $namespace";
+  }
+
+  ### remove immutable items (if any)
+  if (UNIVERSAL::isa($conf, 'HASH') && $conf->{"Immutable Keys"}) {
+    $conf = {%$conf}; # copy the values - only for immutable
+    my $IMMUTABLE = delete $conf->{"Immutable Keys"};
+    foreach my $key (keys %$IMMUTABLE) {
+      delete $conf->{$key};
+    }
+  }
+
+  ### finally write it out
+  $self->write_ref($path, $conf);
+
+  return 1;
+}
+
+sub write_ref {
+  my $self = shift;
+  my $file = shift;
+  my $conf = shift || die "Missing conf";
+  my $args = shift || {};
+  my $ext;
+
+  if (ref $file) {
+    die "Invalid filename for write: $file";
+
+  } elsif (index($file,"\n") != -1) {
+    die "Cannot use a yaml string as a filename during write";
+
+  ### otherwise base it off of the file extension
+  } elsif ($args->{file_type}) {
+    $ext = $args->{file_type};
+  } elsif ($file =~ /\.(\w+)$/) {
+    $ext = $1;
+  } else {
+    $ext = defined($args->{default_ext}) ? $args->{default_ext}
+      : defined($self->{default_ext}) ? $self->{default_ext}
+      : defined($DEFAULT_EXT) ? $DEFAULT_EXT : '';
+    $file = length($ext) ? "$file.$ext" : $file;
+  }
+
+  ### allow for a pre-cached reference
+  if (exists $CACHE{$file} && ! $self->{no_cache}) {
+    warn "Cannot write back to a file that is in the cache";
+    return 0;
+  }
+
+  ### determine the handler
+  my $handler;
+  if ($args->{handler}) {
+    $handler = (UNIVERSAL::isa($args->{handler},'CODE'))
+      ? $args->{handler} : $args->{handler}->{$ext};
+  } elsif ($self->{handler}) {
+    $handler = (UNIVERSAL::isa($self->{handler},'CODE'))
+      ? $self->{handler} : $self->{handler}->{$ext};
+  }
+  if (! $handler) {
+    $handler = $EXT_WRITERS{$ext} || die "Unknown file extension: $ext";
+  }
+
+  return eval { scalar &$handler($file, $conf, $args) } || do {
+    debug    "Couldn't write $file: $@" if $DEBUG_ON_FAIL;
+    dex_warn "Couldn't write $file: $@" if ! $self->{no_warn_on_fail};
+    return 0;
+  };
+
+  return 1;
+}
+
+###----------------------------------------------------------------###
+
+sub write_handler_ini {
+  my $file = shift;
+  my $ref  = shift;
+  require Config::IniHash;
+  return &Config::IniHash::WriteINI($file, $ref);
+}
+
+sub write_handler_pl {
+  my $file = shift;
+  my $ref  = shift;
+  ### do has odd behavior in that it turns a simple hashref
+  ### into hash - help it out a little bit
+  require Data::Dumper;
+  local $Data::Dump::Purity = 1;
+  local $Data::Dumper::Sortkeys  = 1;
+  local $Data::Dumper::Quotekeys = 0;
+  local $Data::Dumper::Pad       = '  ';
+  local $Data::Dumper::Varname   = 'VunderVar';
+  my $str = Data::Dumper->Dumpperl([$ref]);
+  if ($str =~ s/^(.+?=\s*)//s) {
+    my $l = length($1);
+    $str =~ s/^\s{1,$l}//mg;
+  }
+  if ($str =~ /\$VunderVar/) {
+    die "Ref to be written contained circular references - can't write";
+  }
+
+  local *OUT;
+  open (OUT, ">$file") || die $!;
+  print OUT $str;
+  close(OUT);
+}
+
+sub write_handler_storable {
+  my $file = shift;
+  my $ref  = shift;
+  require Storable;
+  return &Storable::store($ref, $file);
+}
+
+sub write_handler_yaml {
+  my $file = shift;
+  my $ref  = shift;
+  require YAML;
+  &YAML::DumpFile($file, $ref);
+}
+
+sub write_handler_xml {
+  my $file = shift;
+  my $ref  = shift;
+  require XML::Simple;
+  local *OUT;
+  open (OUT, ">$file") || die $!;
+  print OUT scalar(XML::Simple->new->XMLout($ref, noattr => 1));
+  close(OUT);
+}
+
+sub write_handler_html {
+  my $file = shift;
+  my $ref  = shift;
+  die "Write of conf information to html is not supported";
+}
+
+###----------------------------------------------------------------###
+
+sub preload_files {
+  my $self  = shift;
+  my $paths = shift || $self->paths;
+  require File::Find;
+
+  ### what extensions do we look for
+  my %EXT;
+  if ($self->{handler}) {
+    if (UNIVERSAL::isa($self->{handler},'HASH')) {
+      %EXT = %{ $self->{handler} };
+    }
+  } else {
+    %EXT = %EXT_READERS;
+    if (! $self->{html_key} && ! $HTML_KEY) {
+      delete $EXT{$_} foreach qw(html htm);
+    }
+  }
+  return if ! keys %EXT;
+  
+  ### look in the paths for the files
+  foreach my $path (ref($paths) ? @$paths : $paths) {
+    $path =~ s|//+|/|g;
+    $path =~ s|/$||;
+    next if exists $CACHE{$path};
+    if (-f $path) {
+      my $ext = ($path =~ /\.(\w+)$/) ? $1 : '';
+      next if ! $EXT{$ext};
+      $CACHE{$path} = $self->read($path);
+    } elsif (-d _) {
+      $CACHE{$path} = 1;
+      &File::Find::find(sub {
+        return if exists $CACHE{$File::Find::name};
+        return if $File::Find::name =~ m|/CVS/|;
+        return if ! -f;
+        my $ext = (/\.(\w+)$/) ? $1 : '';
+        return if ! $EXT{$ext};
+        $CACHE{$File::Find::name} = $self->read($File::Find::name);
+      }, "$path/");
+    } else {
+      $CACHE{$path} = 0;
+    }
+  }
+}
+
+###----------------------------------------------------------------###
+
+1;
+
+__END__
+
+=head1 NAME
+
+CGI::Ex::Conf - CGI Extended Conf Reader
+
+=head1 SYNOPSIS
+
+  my $cob = CGI::Ex::Conf->new;
+
+  my $full_path_to_file = "/tmp/foo.val"; # supports ini, sto, val, pl, xml
+  my $hash = $cob->read($file);
+
+  local $cob->{default_ext} = 'conf'; # default anyway
+
+
+  my @paths = qw(/tmp, /home/pauls);
+  local $cob->{paths} = \@paths;
+  my $hash = $cob->read('My::NameSpace');
+  # will look in /tmp/My/NameSpace.conf and /home/pauls/My/NameSpace.conf
+
+  my $hash = $cob->read('My::NameSpace', {paths => ['/tmp']});
+  # will look in /tmp/My/NameSpace.conf
+
+
+  local $cob->{directive} = 'MERGE';
+  my $hash = $cob->read('FooSpace');
+  # OR #
+  my $hash = $cob->read('FooSpace', {directive => 'MERGE'});
+  # will return merged hashes from /tmp/FooSpace.conf and /home/pauls/FooSpace.conf
+  # immutable keys are preserved from originating files
+
+
+  local $cob->{directive} = 'FIRST';
+  my $hash = $cob->read('FooSpace');
+  # will return values from first found file in the path.
+
+
+  local $cob->{directive} = 'LAST'; # default behavior
+  my $hash = $cob->read('FooSpace');
+  # will return values from last found file in the path.
+
+
+  ### manipulate $hash
+  $cob->write('FooSpace'); # will write it out the changes
+
+=head1 DESCRIPTION
+
+There are half a million Conf readers out there.  Why not add one more.
+Actually, this module provides a wrapper around the many file formats
+and the config modules that can handle them.  It does not introduce any
+formats of its own.
+
+This module also provides a preload ability which is useful in conjunction
+with mod_perl.
+
+Oh - and it writes too.
+
+=head1 METHODS
+
+=over 4
+
+=item C<-E<gt>read_ref>
+
+Takes a file and optional argument hashref.  Figures out the type
+of handler to use to read the file, reads it and returns the ref.
+If you don't need the extended merge functionality, or key fallback,
+or immutable keys, or path lookup ability - then use this method.
+Otherwise - use ->read.
+
+=item C<-E<gt>read>
+
+First argument may be either a perl data structure, yaml string, a
+full filename, or a file "namespace".
+
+The second argument can be a hashref of override values (referred to
+as $args below)..
+
+If the first argument is a perl data structure, it will be
+copied one level deep and returned (nested structures will contain the
+same references).  A yaml string will be parsed and returned.  A full
+filename will be read using the appropriate handler and returned (a
+file beginning with a / or ./ or ../ is considered to be a full
+filename).  A file "namespace" (ie "footer" or "my::config" or
+"what/ever") will be turned into a filename by looking for that
+namespace in the paths found either in $args->{paths} or in
+$self->{paths} or in @DEFAULT_PATHS.  @DEFAULT_PATHS is empty by
+default as is $self->{paths} - read makes no attempt to guess what
+directories to look in.  If the namespace has no extension the
+extension listed in $args->{default_ext} or $self->{default_ext} or
+$DEFAULT_EXT will be used).
+
+  my $ref = $cob->read('My::NameSpace', {
+    paths => [qw(/tmp /usr/data)],
+    default_ext => 'pl',
+  });
+  # would look first for /tmp/My/NameSpace.pl
+  # and then /usr/data/My/NameSpace.pl
+
+  my $ref = $cob->read('foo.sto', {
+    paths => [qw(/tmp /usr/data)],
+    default_ext => 'pl',
+  });
+  # would look first for /tmp/foo.sto
+  # and then /usr/data/foo.sto
+
+When a namespace is used and there are multiple possible paths, there
+area a few options to control which file to look for.  A directive of
+'FIRST', 'MERGE', or 'LAST' may be specified in $args->{directive} or
+$self->{directive} or the default value in $DIRECTIVE will be used
+(default is 'LAST'). When 'FIRST' is specified the first path that
+contains the namespace is returned.  If 'LAST' is used, the last
+found path that contains the namespace is returned.  If 'MERGE' is
+used, the data structures are joined together.  If they are
+arrayrefs, they are joined into one large arrayref.  If they are
+hashes, they are layered on top of each other with keys found in later
+paths overwriting those found in earlier paths.  This allows for
+setting system defaults in a root file, and then allow users to have
+custom overrides.
+
+It is possible to make keys in a root file be immutable (non
+overwritable) by adding a suffix of _immutable or _immu to the key (ie
+{foo_immutable => 'bar'}).  If a value is found in the file that
+matches $IMMUTABLE_KEY, the entire file is considered immutable.
+The immutable defaults may be overriden using $IMMUTABLE_QR and $IMMUTABLE_KEY.
+
+=item C<-E<gt>write_ref>
+
+Takes a file and the reference to be written.  Figures out the type
+of handler to use to write the file and writes it. If you used the ->read_ref
+use this method.  Otherwise, use ->write.
+
+=item C<-E<gt>write>
+
+Allows for writing back out the information read in by ->read.  If multiple
+paths where used - the directive 'FIRST' will write the changes to the first
+file in the path - otherwise the last path will be used.  If ->read had found
+immutable keys, then those keys are removed before writing.
+
+=item C<-E<gt>preload_files>
+
+Arguments are file(s) and/or directory(s) to preload.  preload_files will
+loop through the arguments, find the files that exist, read them in using
+the handler which matches the files extension, and cache them by filename
+in %CACHE.  Directories are spidered for file extensions which match those
+listed in %EXT_READERS.  This is useful for a server environment where CPU
+may be more precious than memory.
+
+=head1 FILETYPES
+
+CGI::Ex::Conf supports the files found in %EXT_READERS by default.
+Additional types may be added to %EXT_READERS, or a custom handler may be
+passed via $args->{handler} or $self->{handler}.  If the custom handler is
+a code ref, all files will be passed to it.  If it is a hashref, it should
+contain keys which are extensions it supports, and values which read those
+extensions.
+
+Some file types have benefits over others.  Storable is very fast, but is
+binary and not human readable.  YAML is readable but very slow.  I would
+suggest using a readable format such as YAML and then using preload_files
+to load in what you need at run time.  All preloaded files are faster than
+any of the other types.
+
+The following is the list of handlers that ships with CGI::Ex::Conf (they
+will only work if the supporting module is installed on your system):
+
+=over 4
+
+=item C<pl>
+
+Should be a file containing a perl structure which is the last thing returned.
+
+=item C<sto> and C<storable>
+
+Should be a file containing a structure stored in Storable format.
+See L<Storable>.
+
+=item C<yaml> and C<conf> and C<val>
+
+Should be a file containing a yaml document.  Multiple documents are returned
+as a single arrayref.  Also - any file without an extension and custom handler
+will be read using YAML.  See L<YAML>.
+
+=item C<ini>
+
+Should be a windows style ini file.  See L<Config::IniHash>
+
+=item C<xml>
+
+Should be an xml file.  It will be read in by XMLin.  See L<XML::Simple>.
+
+=item C<html> and C<htm>
+
+This is actually a custom type intended for use with CGI::Ex::Validate.
+The configuration to be read is actually validation that is stored
+inline with the html.  The handler will look for any form elements or
+input elements with an attribute with the same name as in $HTML_KEY.  It
+will also look for a javascript variable by the same name as in $HTML_KEY.
+All configuration items done this way should be written in YAML.
+For example, if $HTML_KEY contained 'validation' it would find validation in:
+
+  <input type=text name=username validation="{required: 1}">
+  # automatically indented and "username:\n" prepended
+  # AND #
+  <form name=foo validation="
+  general no_confirm: 1
+  ">
+  # AND #
+  <script>
+  document.validation = "\n\
+  username: {required: 1}\n\
+  ";
+  </script>
+  # AND #
+  <script>
+  var validation = "\n\
+  username: {required: 1}\n\
+  ";
+  </script>
+
+If the key $HTML_KEY is not set, the handler will always return undef
+without even opening the file.
+
+=back
+
+=head1 TODO
+
+Make a similar write method that handles immutability.
+
+=head1 AUTHOR
+
+Paul Seamons
+
+=head1 LICENSE
+
+This module may be distributed under the same terms as Perl itself.
+
+=cut
+
diff --git a/lib/CGI/Ex/Die.pm b/lib/CGI/Ex/Die.pm
new file mode 100644 (file)
index 0000000..a3787af
--- /dev/null
@@ -0,0 +1,178 @@
+package CGI::Ex::Die;
+
+use strict;
+use vars qw($no_recurse
+            $EXTENDED_ERRORS $SHOW_TRACE $IGNORE_EVAL
+            $ERROR_TEMPLATE
+            $LOG_HANDLER $FINAL_HANDLER
+            );
+
+use CGI::Ex;
+use CGI::Ex::Dump qw(debug ctrace dex_html);
+
+BEGIN {
+  $SHOW_TRACE = 0      if ! defined $SHOW_TRACE;
+  $IGNORE_EVAL = 0     if ! defined $IGNORE_EVAL;
+  $EXTENDED_ERRORS = 1 if ! defined $EXTENDED_ERRORS;
+}
+
+###----------------------------------------------------------------###
+
+sub import {
+  my $class = shift;
+  if ($#_ != -1) {
+    if (($#_ + 1) % 2) {
+      require Carp;
+      &Carp::croak("Usage: use ".__PACKAGE__." register => 1");
+    }
+    my %args = @_;
+    ### may be called as
+    #   use CGI::Ex::Die register => 1;
+    #   OR
+    #   use CGI::Ex::Die register => [qw(die)];
+    if (! ref($args{register}) || grep {/die/} @{ $args{register} }) {
+      $SIG{__DIE__} = \&die_handler;
+    }
+    $SHOW_TRACE      = $args{'show_trace'}      if exists $args{'show_trace'};
+    $IGNORE_EVAL     = $args{'ignore_eval'}     if exists $args{'ignore_eval'};
+    $EXTENDED_ERRORS = $args{'extended_errors'} if exists $args{'extended_errors'};
+    $ERROR_TEMPLATE  = $args{'error_template'}  if exists $args{'error_template'};
+    $LOG_HANDLER     = $args{'log_handler'}     if exists $args{'log_handler'};
+    $FINAL_HANDLER   = $args{'final_handler'}   if exists $args{'final_handler'};
+  }
+  return 1;
+}
+
+###----------------------------------------------------------------###
+
+sub die_handler {
+  my $err   = shift;
+
+  die $err if $no_recurse;
+  local $no_recurse = 1;
+
+  ### test for eval - if eval - propogate it up
+  if (! $IGNORE_EVAL) {
+    if (! $ENV{MOD_PERL}) {
+      my $n = 0;
+      while (my $sub = (caller(++$n))[3]) {
+        next if $sub !~ /eval/;
+        die $err; # die and let the eval catch it
+      }
+
+      ### test for eval in a mod_perl environment
+    } else {
+      my $n     = 0;
+      my $found = 0;
+      while (my $sub = (caller(++$n))[3]) {
+        $found = $n if ! $found && $sub =~ /eval/;
+        last if $sub =~ /^(Apache|ModPerl)::(PerlRun|Registry)/;
+      }
+      if ($found && $n - 1 != $found) {
+        die $err;
+      }
+    }
+  }
+
+  ### decode the message
+  if (ref $err) {
+
+  } elsif ($EXTENDED_ERRORS && $err) {
+    my $copy = "$err";
+    if ($copy =~ m|^Execution of ([/\w\.\-]+) aborted due to compilation errors|si) {
+      eval {
+        local $SIG{__WARN__} = sub {};
+        require $1;
+      };
+      my $error = $@ || '';
+      $error =~ s|Compilation failed in require at [/\w/\.\-]+/Die.pm line \d+\.\s*$||is;
+      chomp $error;
+      $err .= "\n($error)\n";
+    } elsif ($copy =~ m|^syntax error at ([/\w.\-]+) line \d+, near|mi) {
+    }
+  }
+
+  ### prepare common args
+  my $msg = &CGI::Ex::Dump::_html_quote("$err");
+  $msg = "<pre style='background:red;color:white;border:2px solid black;font-size:120%;padding:3px'>Error: $msg</pre>\n";
+  my $ctrace = ! $SHOW_TRACE ? ""
+    : "<pre style='background:white;color:black;border:2px solid black;padding:3px'>"
+    . dex_html(ctrace)."</pre>";
+  my $args = {err => "$err", msg => $msg, ctrace => $ctrace};
+
+  &$LOG_HANDLER($args) if $LOG_HANDLER;
+
+  ### web based - give more options
+  if ($ENV{REQUEST_METHOD}) {
+    my $cgix = CGI::Ex->new;
+    $| = 1;
+    ### get the template and swap it in
+    # allow for a sub that returns the template
+    # or a string
+    # or a filename (string starting with /)
+    my $out;
+    if ($ERROR_TEMPLATE) {
+      $out = UNIVERSAL::isa($ERROR_TEMPLATE, 'CODE') ? &$ERROR_TEMPLATE($args) # coderef
+        : (substr($ERROR_TEMPLATE,0,1) ne '/') ? $ERROR_TEMPLATE # html string
+        : do { # filename
+          if (open my $fh, $ERROR_TEMPLATE) {
+            read($fh, my $str, -s $ERROR_TEMPLATE);
+            $str; # return of the do
+          } };
+    }
+    if ($out) {
+      $cgix->swap_template(\$out, $args);
+    } else {
+      $out = $msg.'<p></p>'.$ctrace;
+    }
+
+    ### similar to CGI::Carp
+    if (my $r = $cgix->apache_request) {
+      if ($r->bytes_sent) {
+        $r->print($out);
+      } else {
+        $r->status(500);
+        $r->custom_response(500, $out);
+      }
+    } else {
+      $cgix->print_content_type;
+      print $out;
+    }
+  } else {
+    ### command line execution
+  }
+
+  &$FINAL_HANDLER($args) if $FINAL_HANDLER;
+
+  die $err;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+CGI::Ex::Die - A CGI::Carp::FatalsToBrowser type utility.
+
+=head1 SYNOPSIS
+
+  use CGI::Ex::Die;
+  $SIG{__DIE__} = \&CGI::Ex::Die::die_handler;
+
+  # OR #
+
+  use CGI::Ex::Die register => 1;
+
+=head1 DESCRIPTION
+
+This module is intended for showing more useful messages to
+the developer, should errors occur.  This is a stub phase module.
+More features (error notification, custom error page, etc) will
+be added later.
+
+=head1 AUTHORS
+
+Paul Seamons <perlspam at seamons dot com>
+
+=cut
diff --git a/lib/CGI/Ex/Dump.pm b/lib/CGI/Ex/Dump.pm
new file mode 100644 (file)
index 0000000..fd76291
--- /dev/null
@@ -0,0 +1,242 @@
+package CGI::Ex::Dump;
+
+### CGI Extended Data::Dumper Extension
+
+###----------------------------------------------------------------###
+#  Copyright 2004 - Paul Seamons                                     #
+#  Distributed under the Perl Artistic License without warranty      #
+###----------------------------------------------------------------###
+
+### See perldoc at bottom
+
+use vars qw(@ISA @EXPORT @EXPORT_OK $ON $SUB $QR1 $QR2 $full_filename);
+use strict;
+use Exporter;
+
+@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 what_is_this);
+
+### is on or off
+sub on  { $ON = 1 };
+sub off { $ON = 0; }
+&on();
+
+sub set_deparse {
+  $Data::Dumper::Deparse = eval {require B::Deparse};
+}
+
+###----------------------------------------------------------------###
+
+BEGIN {
+  ### setup the Data::Dumper usage
+  $Data::Dumper::Sortkeys  = 1    if ! defined $Data::Dumper::Sortkeys; # not avail pre 5.8
+  $Data::Dumper::Useqq     = 1    if ! defined $Data::Dumper::Useqq;
+  $Data::Dumper::Quotekeys = 0    if ! defined $Data::Dumper::Quotekeys;
+  $Data::Dumper::Pad       = '  ' if ! defined $Data::Dumper::Pad;
+  #$Data::Dumper::Deparse   = 1    if ! defined $Data::Dumper::Deparse; # very useful
+  $SUB = sub {
+    require Data::Dumper;
+    return Data::Dumper->Dumpperl(\@_);
+  };
+
+  ### how to display or parse the filename
+  $QR1 = qr{\A(?:/[^/]+){2,}/(?:perl|lib)/(.+)\Z};
+  $QR2 = qr{\A.+?([\w\.\-]+/[\w\.\-]+)\Z};
+}
+
+###----------------------------------------------------------------###
+
+
+### same as dumper but with more descriptive output and auto-formatting
+### for cgi output
+sub what_is_this {
+  return if ! $ON;
+  ### figure out which sub we called
+  my ($pkg, $file, $line_n, $called) = caller(0);
+  ($pkg, $file, $line_n, $called) = caller(1) if $pkg eq __PACKAGE__;
+  substr($called, 0, length(__PACKAGE__) + 2, '');
+
+  ### get the actual line
+  my $line = '';
+  if (open(IN,$file)) {
+    $line = <IN> for 1 .. $line_n;
+    close IN;
+  }
+
+  ### get rid of extended filename
+  if (! $full_filename) {
+    $file =~ s/$QR1/$1/ || $file =~ s/$QR2/$1/;
+  }
+
+  ### dump it out
+  my @dump = map {&$SUB($_)} @_;
+  my @var  = ('$VAR') x ($#dump + 1);
+  if ($line =~ s/^ .*\b \Q$called\E ( \(?\s* | \s+ )//x
+      && $line =~ s/(?:\s+if\s+.+)? ;? \s*$//x) {
+    $line =~ s/ \s*\) $ //x if $1 && $1 =~ /\(/;
+    my @_var = map {/^[\"\']/ ? 'String' : $_} split (/\s*,\s*/, $line);
+    @var = @_var if $#var == $#_var;
+  }
+
+  ### spit it out
+  if ($called eq 'dex_text'
+      || $called eq 'dex_warn'
+      || ! $ENV{REQUEST_METHOD}) {
+    my $txt = "$called: $file line $line_n\n";
+    for (0 .. $#dump) {
+      $dump[$_] =~ s|\$VAR1|$var[$_]|g;
+      $txt .= $dump[$_];
+    }
+    if    ($called eq 'dex_text') { return $txt }
+    elsif ($called eq 'dex_warn') { warn  $txt  }
+    else                          { print $txt  }
+  } else {
+    my $html = "<pre><b>$called: $file line $line_n</b>\n";
+    for (0 .. $#dump) {
+      $dump[$_] =~ s/\\n/\n/g;
+      $dump[$_] = _html_quote($dump[$_]);
+      $dump[$_] =~ s|\$VAR1|<b>$var[$_]</b>|g;
+      $html .= $dump[$_];
+    }
+    $html .= "</pre>\n";
+    return $html if $called eq 'dex_html';
+    require CGI::Ex;
+    &CGI::Ex::print_content_type();
+    print $html;
+  }
+}
+
+### some aliases
+sub debug    { &what_is_this }
+sub dex      { &what_is_this }
+sub dex_warn { &what_is_this }
+sub dex_text { &what_is_this }
+sub dex_html { &what_is_this }
+
+sub _html_quote {
+  my $value = shift;
+  return '' if ! defined $value;
+  $value =~ s/&/&amp;/g;
+  $value =~ s/</&lt;/g;
+  $value =~ s/>/&gt;/g;
+#  $value =~ s/\"/&quot;/g;
+  return $value;
+}
+
+### ctrace is intended for work with perl 5.8 or higher's Carp
+sub ctrace {
+  require 5.8.0;
+  require Carp::Heavy;
+  local $Carp::MaxArgNums = 3;
+  local $Carp::MaxArgLen  = 20;
+  my $i = shift || 0;
+  my @i = ();
+  my $max1 = 0;
+  my $max2 = 0;
+  my $max3 = 0;
+  while (my %i = &Carp::caller_info(++$i)) {
+    $i{sub_name} =~ s/\((.*)\)$//;
+    $i{args} = $i{has_args} ? $1 : "";
+    $i{sub_name} =~ s/^.*?([^:]+)$/$1/;
+    $i{file} =~ s/$QR1/$1/ || $i{file} =~ s/$QR2/$1/;
+    $max1 = length($i{sub_name}) if length($i{sub_name}) > $max1;
+    $max2 = length($i{file})     if length($i{file})     > $max2;
+    $max3 = length($i{line})     if length($i{line})     > $max3;
+    push @i, \%i;
+  }
+  foreach my $ref (@i) {
+    $ref = sprintf("%-${max1}s at %-${max2}s line %${max3}s", $ref->{sub_name}, $ref->{file}, $ref->{line})
+      . ($ref->{args} ? " ($ref->{args})" : "");
+  }
+  return \@i;
+}
+
+sub dex_trace {
+  &what_is_this(ctrace(1));
+}
+
+###----------------------------------------------------------------###
+
+1;
+
+__END__
+
+=head1 NAME
+
+CGI::Ex::Dump - A debug utility
+
+=head1 SYNOPSIS
+
+  use CGI::Ex::Dump; # auto imports dex, dex_warn, dex_text and others
+
+  my $hash = {
+    foo => ['a', 'b', 'Foo','a', 'b', 'Foo','a', 'b', 'Foo','a'],
+  };
+
+  dex $hash; # or dex_warn $hash;
+
+  dex;
+
+  dex "hi";
+
+  dex $hash, "hi", $hash;
+
+  dex \@INC; # print to STDOUT, or format for web if $ENV{REQUEST_METHOD}
+
+  dex_warn \@INC;  # same as dex but to STDOUT
+
+  print FOO dex_text \@INC; # same as dex but return dump
+
+  # ALSO #
+
+  use CGI::Ex::Dump qw(debug);
+  
+  debug; # same as dex
+
+=head1 DESCRIPTION
+
+Uses the base Data::Dumper of the distribution and gives it nicer formatting - and
+allows for calling just about anytime during execution.
+
+Calling &CGI::Ex::set_deparse() will allow for dumped output of subroutines
+if available.
+
+perl -e 'use CGI::Ex::Dump;  dex "foo";'
+
+See also L<Data::Dumper>.
+
+Setting any of the Data::Dumper globals will alter the output.
+
+=head1 SUBROUTINES
+
+=over 4
+
+=item C<dex>, C<debug>
+
+Prints out pretty output to STDOUT.  Formatted for the web if on the web.
+
+=item C<dex_warn>
+
+Prints to STDERR.
+
+=item C<dex_text>
+
+Return the text as a scalar.
+
+=item C<ctrace>
+
+Caller trace returned as an arrayref.  Suitable for use like "debug ctrace".
+This does require at least perl 5.8.0's Carp.
+
+=item C<on>, C<off>
+
+Turns calls to routines on or off.  Default is to be on.
+
+=back
+
+=head1 AUTHORS
+
+Paul Seamons <perlspam at seamons dot com>
+
+=cut
diff --git a/lib/CGI/Ex/Fill.pm b/lib/CGI/Ex/Fill.pm
new file mode 100644 (file)
index 0000000..e1094ef
--- /dev/null
@@ -0,0 +1,465 @@
+package CGI::Ex::Fill;
+
+### CGI Extended Form Filler
+
+###----------------------------------------------------------------###
+#  Copyright 2003 - Paul Seamons                                     #
+#  Distributed under the Perl Artistic License without warranty      #
+###----------------------------------------------------------------###
+
+### See perldoc at bottom
+
+use strict;
+use vars qw($VERSION
+            @ISA @EXPORT @EXPORT_OK
+            $REMOVE_SCRIPT
+            $REMOVE_COMMENT
+            $MARKER_SCRIPT
+            $MARKER_COMMENT
+            $OBJECT_METHOD
+            $TEMP_TARGET
+            );
+use Exporter;
+
+$VERSION   = '1.3';
+@ISA       = qw(Exporter);
+@EXPORT    = qw(form_fill);
+@EXPORT_OK = qw(form_fill html_escape get_tagval_by_key swap_tagval_by_key);
+
+### These directives are used to determine whether or not to
+### remove html comments and script sections while filling in
+### a form.  Default is on.  This may give some trouble if you
+### have a javascript section with form elements that you would
+### like filled in.
+$REMOVE_SCRIPT  = 1;
+$REMOVE_COMMENT = 1;
+$MARKER_SCRIPT  = "\0SCRIPT\0";
+$MARKER_COMMENT = "\0COMMENT\0";
+$OBJECT_METHOD  = "param";
+
+###----------------------------------------------------------------###
+
+### Regex based filler - as opposed to HTML::Parser based HTML::FillInForm
+### arguments are positional
+### pos1 - text or textref - if textref it is modified in place
+### pos2 - hash or cgi obj ref, or array ref of hash and cgi obj refs
+### pos3 - target - to be used for choosing a specific form - default undef
+### pos4 - boolean fill in password fields - default is true
+### pos5 - hashref or arrayref of fields to ignore
+sub form_fill {
+  my $text          = shift;
+  my $ref           = ref($text) ? $text : \$text;
+  my $form          = shift;
+  my $forms         = UNIVERSAL::isa($form, 'ARRAY') ? $form : [$form];
+  my $target        = shift;
+  my $fill_password = shift;
+  my $ignore        = shift || {};
+  $ignore = {map {$_ => 1} @$ignore} if UNIVERSAL::isa($ignore, 'ARRAY');
+  $fill_password = 1 if ! defined $fill_password;
+
+
+  ### allow for optionally removing comments and script
+  my @comment;
+  my @script;
+  if ($REMOVE_SCRIPT) {
+    $$ref =~ s|(<script\b.+?</script>)|push(@script, $1);$MARKER_SCRIPT|egi;
+  }
+  if ($REMOVE_COMMENT) {
+    $$ref =~ s|(<!--.*?-->)|push(@comment, $1);$MARKER_COMMENT|eg;
+  }
+
+  ### if there is a target - focus in on it
+  ### possible bug here - name won't be found if
+  ### there is nested html inside the form tag that comes before
+  ### the name field - if no close form tag - don't swap in anything
+  if ($target) {
+    local $TEMP_TARGET = $target;
+    $$ref =~ s{(<form            # open form
+                [^>]+            # some space
+                \bname=([\"\']?) # the name tag
+                $target          # with the correct name (allows for regex)
+                \2               # closing quote
+                .+?              # as much as there is
+                (?=</form>))     # then end
+              }{
+                local $REMOVE_SCRIPT  = undef;
+                local $REMOVE_COMMENT = undef;
+                &form_fill($1, $form, undef, $fill_password, $ignore);
+              }sigex;
+
+    ### put scripts and comments back and return
+    $$ref =~ s/$MARKER_COMMENT/shift(@comment)/eg if $#comment != -1;
+    $$ref =~ s/$MARKER_SCRIPT/ shift(@script) /eg if $#script  != -1;
+    return ref($text) ? 1 : $$ref;
+  }
+
+  ### build a sub to get a value
+  my %indexes = (); # store indexes for multivalued elements
+  my $get_form_value = sub {
+    my $key = shift;
+    my $all = $_[0] && $_[0] eq 'all';
+    if (! defined $key || ! length $key) {
+      return $all ? [] : undef;
+    }
+
+    my $val;
+    my $meth;
+    foreach my $form (@$forms) {
+      next if ! ref $form;
+      if (UNIVERSAL::isa($form, 'HASH') && defined $form->{$key}) {
+        $val = $form->{$key};
+        last;
+      } elsif ($meth = UNIVERSAL::can($form, $OBJECT_METHOD)) {
+        $val = $form->$meth($key);
+        last if defined $val;
+      } elsif (UNIVERSAL::isa($form, 'CODE')) {
+        $val = &{ $form }($key, $TEMP_TARGET);
+        last if defined $val;
+      }
+    }
+    if (! defined $val) {
+      return $all ? [] : undef;
+    }
+
+    ### fix up the value some
+    if (UNIVERSAL::isa($val, 'CODE')) {
+      $val = &{ $val }($key, $TEMP_TARGET);
+    }
+    if (UNIVERSAL::isa($val, 'ARRAY')) {
+      $val = [@$val]; # copy the values
+    } elsif (ref $val) {
+      # die "Value for $key is not an array or a scalar";
+      $val = "$val";  # stringify anything else
+    }
+
+    ### html escape them all
+    &html_escape(\$_) foreach (ref($val) ? @$val : $val);
+
+    ### allow for returning all elements
+    ### or one at a time
+    if ($all) {
+      return ref($val) ? $val : [$val];
+    } elsif (ref($val)) {
+      $indexes{$key} ||= 0;
+      my $ret = $val->[$indexes{$key}] || '';
+      $indexes{$key} ++; # don't wrap - if we run out of values - we're done
+      return $ret;
+    } else {
+      return $val;
+    }
+  };
+
+
+  ###--------------------------------------------------------------###
+
+  ### First pass
+  ### swap <input > form elements if they have a name
+  $$ref =~ s{
+    (<input \s (?: ([\"\'])(?:|.*?[^\\])\2 | [^>] )* >) # nested html ok
+    }{
+      ### get the type and name - intentionally exlude names with nested "'
+      my $tag   = $1;
+      my $type  = uc(&get_tagval_by_key(\$tag, 'type') || '');
+      my $name  = &get_tagval_by_key(\$tag, 'name');
+
+      if ($name && ! $ignore->{$name}) {
+        if (! $type
+            || $type eq 'HIDDEN'
+            || $type eq 'TEXT'
+            || $type eq 'FILE'
+            || ($type eq 'PASSWORD' && $fill_password)) {
+          
+          my $value = &$get_form_value($name, 'next');
+          if (defined $value) {
+            &swap_tagval_by_key(\$tag, 'value', $value);
+          } elsif (! defined &get_tagval_by_key(\$tag, 'value')) {
+            &swap_tagval_by_key(\$tag, 'value', '');
+          }
+
+        } elsif ($type eq 'CHECKBOX'
+                 || $type eq 'RADIO') {
+          my $values = &$get_form_value($name, 'all');          
+          if (@$values) {
+            $tag =~ s{\s+\bCHECKED\b(?:=([\"\']?)checked\1)?(?=\s|>|/>)}{}ig;
+            
+            if ($type eq 'CHECKBOX' && @$values == 1 && $values->[0] eq 'on') {
+              $tag =~ s|(/?>\s*)$| checked="checked"$1|;
+            } else {
+              my $fvalue = &get_tagval_by_key(\$tag, 'value');
+              if (defined $fvalue) {
+                foreach (@$values) {
+                  next if $_ ne $fvalue;
+                  $tag =~ s|(\s*/?>\s*)$| checked="checked"$1|;
+                  last;
+                }
+              }
+            }
+          }
+        }
+      }
+      $tag; # return of swap
+    }sigex;
+
+
+  ### Second pass
+  ### swap select boxes (must be done in such a way as to allow no closing tag)
+  my @start = ();
+  my @close = ();
+  push @start, pos($$ref) - length($1) while $$ref =~ m|(<\s*select\b)|ig;
+  push @close, pos($$ref) - length($1) while $$ref =~ m|(</\s*select\b)|ig;
+  for (my $i = 0; $i <= $#start; $i ++) {
+    while (defined($close[$i]) && $close[$i] < $start[$i]) {
+      splice (@close,$i,1,());
+    }
+    if ($i == $#start) {
+      $close[$i] = length($$ref) if ! defined $close[$i]; # set to end of string if no closing
+    } elsif (! defined($close[$i]) || $close[$i] > $start[$i + 1]) {
+      $close[$i] = $start[$i + 1]; # set to start of next select if no closing or > next select
+    }
+  }
+  for (my $i = $#start; $i >= 0; $i --) {
+    my $opts = substr($$ref, $start[$i], $close[$i] - $start[$i]);
+    $opts =~ s{
+      (<select \s                                 # opening
+       (?: "" | '' | ([\"\']).*?[^\\]\2 | [^>] )* # nested html ok
+       >)                                         # end of tag
+      }{}sxi || next;
+    next if ! $opts;
+    my $tag    = $1;
+    my $name   = &get_tagval_by_key(\$tag, 'name');
+    my $values = $ignore->{$name} ? [] : &$get_form_value($name, 'all');
+    if ($#$values != -1) {
+      my $n = $opts =~ s{
+        (<option[^>]*>)           # opening tag - no embedded > allowed
+          (.*?)                   # the text value
+          (?=<option|$|</option>) # the next tag
+        }{
+          my ($tag2, $opt) = ($1, $2);
+          $tag2 =~ s%\s+\bSELECTED\b(?:=([\"\']?)selected\1)?(?=\s|>|/>)%%ig;
+          
+          my $fvalues = &get_tagval_by_key(\$tag2, 'value', 'all');
+          my $fvalue  = @$fvalues ? $fvalues->[0]
+            : $opt =~ /^\s*(.*?)\s*$/ ? $1 : "";
+          foreach (@$values) {
+            next if $_ ne $fvalue;
+            $tag2 =~ s|(\s*/?>\s*)$| selected="selected"$1|;
+            last;
+          }
+          "$tag2$opt"; # return of the swap
+        }sigex;
+      if ($n) {
+        substr($$ref, $start[$i], $close[$i] - $start[$i], "$tag$opts");
+      }
+    }
+  }
+
+
+  ### Third pass
+  ### swap textareas (must be done in such a way as to allow no closing tag)
+  @start = ();
+  @close = ();
+  push @start, pos($$ref) - length($1) while $$ref =~ m|(<\s*textarea\b)|ig;
+  push @close, pos($$ref) - length($1) while $$ref =~ m|(</\s*textarea\b)|ig;
+  for (my $i = 0; $i <= $#start; $i ++) {
+    while (defined($close[$i]) && $close[$i] < $start[$i]) {
+      splice (@close,$i,1,());
+    }
+    if ($i == $#start) {
+      $close[$i] = length($$ref) if ! defined $close[$i]; # set to end of string if no closing
+    } elsif (! defined($close[$i]) || $close[$i] > $start[$i + 1]) {
+      $close[$i] = $start[$i + 1]; # set to start of next select if no closing or > next select
+    }
+  }
+  for (my $i = $#start; $i >= 0; $i --) {
+    my $oldval = substr($$ref, $start[$i], $close[$i] - $start[$i]);
+    $oldval =~ s{
+      (<textarea \s                               # opening
+       (?: "" | '' | ([\"\']).*?[^\\]\2 | [^>] )* # nested html ok
+       >)                                         # end of tag
+      }{}sxi || next;
+    my $tag   = $1;
+    my $name  = &get_tagval_by_key(\$tag, 'name');
+    my $value = $ignore->{$name} ? [] : &$get_form_value($name, 'next');
+    next if ! defined $value;
+    substr($$ref, $start[$i], $close[$i] - $start[$i], "$tag$value");
+  }
+
+  ### put scripts and comments back and return
+  $$ref =~ s/$MARKER_COMMENT/shift(@comment)/eg if $#comment != -1;
+  $$ref =~ s/$MARKER_SCRIPT/ shift(@script) /eg if $#script  != -1;
+  return ref($text) ? 1 : $$ref;
+}
+
+
+### yet another html escaper
+### allow pass by value or by reference (reference is modified inplace)
+sub html_escape {
+  my $str = shift;
+  return $str if ! $str;
+  my $ref = ref($str) ? $str : \$str;
+
+  $$ref =~ s/&/&amp;/g;
+  $$ref =~ s/</&lt;/g;
+  $$ref =~ s/>/&gt;/g;
+  $$ref =~ s/\"/&quot;/g;
+
+  return ref($str) ? 1 : $$ref;
+}
+
+### get a named value for key="value" pairs
+### usage: my $val     = &get_tagval_by_key(\$tag, $key);
+### usage: my $valsref = &get_tagval_by_key(\$tag, $key, 'all');
+sub get_tagval_by_key {
+  my $tag = shift;
+  my $ref = ref($tag) ? $tag : \$tag;
+  my $key = lc(shift);
+  my $all = $_[0] && $_[0] eq 'all';
+  my @all = ();
+  pos($$ref) = 0; # fix for regex below not resetting and forcing order on key value pairs
+
+  ### loop looking for tag pairs
+  while ($$ref =~ m{
+    (?<![\w\.\-])                  # 0 - not proceded by letter or .
+      ([\w\.\-]+)                  # 1 - the key
+      \s*=                         # equals
+      (?: \s*([\"\'])(|.*?[^\\])\2 # 2 - a quote, 3 - the quoted
+       |  ([^\s/]*? (?=\s|>|/>))   # 4 - a non-quoted string
+       )
+    }sigx) {
+    next if lc($1) ne $key;
+    my ($val,$quot) = ($2) ? ($3,$2) : ($4,undef);
+    $val =~ s/\\$quot/$quot/ if $quot;
+    return $val if ! $all;
+    push @all, $val;
+  }
+  return undef if ! $all;
+  return \@all;
+}
+
+### swap out values for key="value" pairs
+### usage: my $count  = &swap_tagval_by_key(\$tag, $key, $val);
+### usage: my $newtag = &swap_tagval_by_key($tag, $key, $val);
+sub swap_tagval_by_key {
+  my $tag = shift;
+  my $ref = ref($tag) ? $tag : \$tag;
+  my $key = lc(shift);
+  my $val = shift;
+  my $n   = 0;
+
+  ### swap a key/val pair at time
+  $$ref =~ s{(^\s*<\s*\w+\s+ | \G\s+)         # 1 - open tag or previous position
+               ( ([\w\-\.]+)                  # 2 - group, 3 - the key
+                 (\s*=)                       # 4 - equals
+                  (?: \s* ([\"\']) (?:|.*?[^\\]) \5 # 5 - the quote mark, the quoted
+                   |  [^\s/]*? (?=\s|>|/>)    # a non-quoted string (may be zero length)
+                  )
+                | ([^\s/]+?) (?=\s|>|/>)      # 6 - a non keyvalue chunk (CHECKED)
+               )
+             }{
+               if (defined($3) && lc($3) eq $key) { # has matching key value pair
+                 if (! $n ++) {  # only put value back on first match
+                   "$1$3$4\"$val\""; # always double quote
+                 } else {
+                   $1; # second match
+                 }
+               } elsif (defined($6) && lc($6) eq $key) { # has matching key
+                 if (! $n ++) {  # only put value back on first match
+                   "$1$6=\"$val\"";
+                 } else {
+                   $1; # second match
+                 }
+               } else {
+                 "$1$2"; # non-keyval
+               }
+             }sigex;
+
+  ### append value on if none were swapped
+  if (! $n) {
+    $$ref =~ s|(\s*/?>\s*)$| value="$val"$1|;
+    $n = -1;
+  }
+
+  return ref($tag) ? $n : $$ref;
+}
+
+1;
+
+__END__
+
+###----------------------------------------------------------------###
+
+=head1 NAME
+
+CGI::Ex::Fill - Yet another form filler
+
+=head1 SYNOPSIS
+
+  use CGI::Ex::Fill qw(form_fill);
+
+  my $text = my_own_template_from_somewhere();
+
+  my $form = CGI->new;
+  # OR
+  # my $form = {key => 'value'}
+  # OR 
+  # my $form = [CGI->new, CGI->new, {key1 => 'val1'}, CGI->new];
+
+
+  form_fill(\$text, $form); # modifies $text
+  # OR
+  # my $copy = form_fill($text, $form); # copies $text
+
+
+  ALSO
+
+  my $formname = 'formname';     # table to parse (undef = anytable)
+  my $fp = 0;                    # fill_passwords ? default is true
+  my $ignore = ['key1', 'key2']; # OR {key1 => 1, key2 => 1};
+
+  form_fill(\$text, $form, $formname, $fp, $ignore);
+
+  ALSO
+
+  ### delay getting the value until we find an element that needs it
+  my $form = {key => sub {my $key = shift; # get and return value}};
+
+
+=head1 DESCRIPTION
+
+form_fill is directly comparable to HTML::FillInForm.  It will pass the
+same suite of tests (actually - it is a little bit kinder on the parse as
+it won't change case, reorder your attributes, or miscellaneous spaces).
+
+HTML::FillInForm both benefits and suffers from being based on
+HTML::Parser. It is good for standards and poor for performance.  Testing
+the form_fill module against HTML::FillInForm gave some surprising
+results.  On tiny forms (< 1 k) form_fill was ~ 17% faster than FillInForm.
+If the html document incorporated very many entities at all, the
+performace of FillInForm goes down (and down).  However, if you are only
+filling in one form every so often, then it shouldn't matter - but form_fill
+will be nicer on the tags and won't balk at ugly html.
+See the benchmarks in the t/samples directory for more information (ALL
+BENCHMARKS SHOULD BE TAKEN WITH A GRAIN OF SALT).
+
+=head1 HTML COMMENT / JAVASCRIPT
+
+Because there are too many problems that could occur with html
+comments and javascript, form_fill temporarily removes them during the
+fill.  You may disable this behavior by setting $REMOVE_COMMENT and
+$REMOVE_SCRIPT to 0 before calling form_fill.  The main reason for
+doing this would be if you wanted to have form elments inside the
+javascript and comments get filled.  Disabling the removal only
+results in a speed increase of 5%. The function uses \0COMMENT\0 and
+\0SCRIPT\0 as placeholders so i'd avoid these in your text (Actually
+they may be reset to whatever you'd like via $MARKER_COMMENT and
+$MARKER_SCRIPT).
+
+=head1 AUTHOR
+
+Paul Seamons
+
+=head1 LICENSE
+
+This module may distributed under the same terms as Perl itself.
+
+=cut
diff --git a/lib/CGI/Ex/Template.pm b/lib/CGI/Ex/Template.pm
new file mode 100644 (file)
index 0000000..76cae38
--- /dev/null
@@ -0,0 +1,92 @@
+package CGI::Ex::Template;
+
+use strict;
+use vars qw(@INCLUDE_PATH $CONTENT_SUBDIR);
+use base qw(Template);
+
+use CGI::Ex;
+use CGI::Ex::Fill;
+
+$CONTENT_SUBDIR ||= 'content';
+
+###----------------------------------------------------------------###
+
+sub new {
+  my $class = shift;
+  my $args  = ref($_[0]) ? shift : {@_};
+
+  $args->{INCLUDE_PATH} ||= \@INCLUDE_PATH;
+
+  return $class->SUPER::new($args);
+}
+
+sub process {
+  my $self = ref($_[0]) ? shift : shift->new;
+  my $in   = shift;
+
+  ### force the content to have a .html prefix
+  if (! ref $in) {
+    $in .= '.html' if $in !~ /\.\w+$/;
+  }
+
+  ### prepend "content" dir as needed
+  if (! ref($in)                                # not a scalar ref or a file glob
+      && $in =~ m|^\w+(\.\w+)?(/\w+(\.\w+)?)*$| # not an absolute filename
+      && index($in, $CONTENT_SUBDIR) == -1) {
+    $in = $CONTENT_SUBDIR .'/'. $in;
+  }
+
+  return $self->SUPER::process($in, @_);
+}
+
+###----------------------------------------------------------------###
+
+sub out {
+  my $self = ref($_[0]) ? shift : shift->new;
+#  dex $self;
+  my $in   = shift;
+  my $form = shift;
+  my $fill = shift;
+  my $out  = '';
+
+  ### run the template
+  my $status = $self->process($in, $form, \$out) || die $Template::ERROR;
+
+  ### fill in any forms
+  &CGI::Ex::Fill::form_fill(\$out, $fill) if $fill && ! $self->{no_fill};
+
+  return $out;
+}
+
+sub print {
+  my $self   = ref($_[0]) ? shift : shift->new;
+  my $in     = shift;
+  my $form   = shift;
+  my $fill   = shift || $form;
+
+  &CGI::Ex::content_type();
+  print $self->out($in, $form, $fill);
+}
+
+###----------------------------------------------------------------###
+
+1;
+
+__END__
+
+=head1 NAME
+
+CGI::Ex::Template - Beginning interface to Templating systems - for they are many
+
+=head1 SYNOPSIS
+
+  None yet.
+
+=head1 DESCRIPTION
+
+=head1 AUTHORS
+
+Paul Seamons <perlspam at seamons dot com>
+
+=cut
+
diff --git a/lib/CGI/Ex/Validate.pm b/lib/CGI/Ex/Validate.pm
new file mode 100644 (file)
index 0000000..591168f
--- /dev/null
@@ -0,0 +1,2111 @@
+package CGI::Ex::Validate;
+
+### CGI Extended Validator
+
+###----------------------------------------------------------------###
+#  Copyright 2004 - Paul Seamons                                     #
+#  Distributed under the Perl Artistic License without warranty      #
+###----------------------------------------------------------------###
+
+### See perldoc at bottom
+
+use strict;
+use vars qw($VERSION
+            $ERROR_PACKAGE
+            $DEFAULT_EXT
+            %DEFAULT_OPTIONS
+            $JS_URI_PATH
+            $JS_URI_PATH_YAML
+            $JS_URI_PATH_VALIDATE
+            $QR_EXTRA
+            @UNSUPPORTED_BROWSERS
+            );
+
+$VERSION = '1.14';
+
+$ERROR_PACKAGE = 'CGI::Ex::Validate::Error';
+$DEFAULT_EXT   = 'val';
+$QR_EXTRA      = qr/^(\w+_error|as_(array|string|hash)_\w+|no_\w+)/;
+@UNSUPPORTED_BROWSERS = (qr/MSIE\s+5.0\d/i);
+
+use CGI::Ex::Conf ();
+
+###----------------------------------------------------------------###
+
+sub new {
+  my $class = shift || __PACKAGE__;
+  my $self  = (@_ && ref($_[0])) ? shift : {@_};
+
+  ### allow for global defaults
+  foreach (keys %DEFAULT_OPTIONS) {
+    $self->{$_} = $DEFAULT_OPTIONS{$_} if ! exists $self->{$_};
+  }
+
+  return bless $self, $class;
+}
+
+###----------------------------------------------------------------###
+
+sub cgix {
+  my $self = shift;
+  return $self->{cgix} ||= do {
+    require CGI::Ex;
+    CGI::Ex->new;
+  };
+}
+
+sub conf {
+  my $self = shift;
+  return $self->{conf_obj} ||= CGI::Ex::Conf->new({
+    default_ext => $DEFAULT_EXT,
+    directive   => 'LAST',
+  });
+}
+
+### the main validation routine
+sub validate {
+  my $self = (! ref($_[0])) ? shift->new                    # $class->validate
+              : UNIVERSAL::isa($_[0], __PACKAGE__) ? shift  # $self->validate
+              : __PACKAGE__->new;                           # &validate
+  my $form     = shift || die "Missing form hash";
+  my $val_hash = shift || die "Missing validation hash";
+  my $what_was_validated = shift; # allow for extra arrayref that stores what was validated
+
+  ### turn the form into a form if it is really a CGI object
+  if (! ref($form)) {
+    die "Invalid form hash or cgi object";
+  } elsif(! UNIVERSAL::isa($form,'HASH')) {
+    local $self->{cgi_object} = $form;
+    $form = $self->cgix->get_form($form);
+  }
+
+  ### get the validation - let get_validation deal with types
+  ### if a ref is not passed - assume it is a filename
+  $val_hash = $self->get_validation($val_hash);
+
+  ### allow for validation passed as single group hash, single group array,
+  ### or array of group hashes or group arrays
+  my @ERRORS = ();
+  my %EXTRA  = ();
+  my @USED_GROUPS = ();
+  my $group_order = (UNIVERSAL::isa($val_hash,'HASH')) ? [$val_hash] : $val_hash;
+  foreach my $group_val (@$group_order) {
+    die "Validation groups must be a hashref" if ! UNIVERSAL::isa($group_val,'HASH');
+    my $title       = $group_val->{'group title'};
+    my $validate_if = $group_val->{'group validate_if'};
+
+    ### only validate this group if it is supposed to be checked
+    next if $validate_if && ! $self->check_conditional($form, $validate_if);
+    push @USED_GROUPS, $group_val;
+
+    ### If the validation items were not passed as an arrayref.
+    ### Look for a group order and then fail back to the keys of the group.
+    ### We will keep track of what was added using %found - the keys will
+    ###   be the hash signatures of the field_val hashes (ignore the hash internals).
+    my @order  = sort keys %$group_val;
+    my $fields = $group_val->{'group fields'};
+    my %found = (); # attempt to keep track of what field_vals have been added
+    if ($fields) { # if I passed group fields array - use it
+      die "'group fields' must be an arrayref" if ! UNIVERSAL::isa($fields,'ARRAY');
+    } else { # other wise - create our own array
+      my @fields = ();
+      if (my $order = $group_val->{'group order'} || \@order) {
+        die "Validation 'group order' must be an arrayref" if ! UNIVERSAL::isa($order,'ARRAY');
+        foreach my $field (@$order) {
+          next if $field =~ /^(group|general)\s/;
+          my $field_val = exists($group_val->{$field}) ? $group_val->{$field}
+            : ($field eq 'OR') ? 'OR' : die "No element found in group for $field";
+          $found{"$field_val"} = 1; # do this before modifying on the next line
+          if (ref $field_val && ! $field_val->{'field'}) {
+            $field_val = { %$field_val, 'field' => $field }; # copy the values to add the key
+          }
+          push @fields, $field_val;
+        }
+      }
+      $fields = \@fields;
+    }
+
+    ### double check which field_vals have been used so far
+    foreach my $field_val (@$fields) {
+      my $field = $field_val->{'field'} || die "Missing field key in validation";
+      $found{"$field_val"} = 1;
+    }
+
+    ### add any remaining field_vals from the order
+    ### this is necessary for items that weren't in group fields or group order
+    foreach my $field (@order) {
+      next if $field =~ /^(group|general)\s/;
+      my $field_val = $group_val->{$field};
+      die "Found a nonhashref value on field $field" if ! UNIVERSAL::isa($field_val, 'HASH');
+      next if $found{"$field_val"}; # do before modifying ref on next line
+      $field_val = { %$field_val, 'field' => $field } if ! $field_val->{'field'}; # copy the values
+      push @$fields, $field_val;
+    }
+
+    ### Finally we have our arrayref of hashrefs that each have their 'field' key
+    ### now lets do the validation
+    my $found  = 1;
+    my @errors = ();
+    my $hold_error; # hold the error for a moment - to allow for an "Or" operation
+    foreach (my $i = 0; $i <= $#$fields; $i ++) {
+      my $ref = $fields->[$i];
+      if (! ref($ref) && $ref eq 'OR') {
+        $i ++ if $found; # if found skip the OR altogether
+        $found = 1; # reset
+        next;
+      }
+      $found = 1;
+      die "Missing field key during normal validation" if ! $ref->{'field'};
+      local $ref->{'was_validated'} = 1;
+      my @err = $self->validate_buddy($form, $ref->{'field'}, $ref);
+      if (delete($ref->{'was_validated'}) && $what_was_validated) {
+        push @$what_was_validated, $ref;
+      }
+
+      ### test the error - if errors occur allow for OR - if OR fails use errors from first fail
+      if (scalar @err) {
+        if ($i < $#$fields && ! ref($fields->[$i + 1]) && $fields->[$i + 1] eq 'OR') {
+          $hold_error = \@err;
+        } else {
+          push @errors, $hold_error ? @$hold_error : @err;
+          $hold_error = undef;
+        }
+      } else {
+        $hold_error = undef;
+      }
+    }
+    push(@errors, @$hold_error) if $hold_error; # allow for final OR to work
+
+    ### add on errors as requested
+    if ($#errors != -1) {
+      push @ERRORS, $title if $title;
+      push @ERRORS, @errors;
+    }
+
+    ### add on general options, and group options if errors in group occurred
+    foreach my $field (@order) {
+      next if $field !~ /^(general|group)\s+(\w+)$/;
+      my $key = $2;
+      next if $1 eq 'group' && ($#errors == -1 || $key =~ /^(field|order|title)$/);
+      $EXTRA{$key} = $group_val->{$field};
+    }
+  }
+
+  ### store any extra items from self
+  foreach my $key (keys %$self) {
+    next if $key !~ $QR_EXTRA;
+    $EXTRA{$key} = $self->{$key};
+  }
+
+  ### allow for checking for unused keys
+  if ($EXTRA{no_extra_fields}) {
+    my $which = ($EXTRA{no_extra_fields} =~ /used/i) ? 'used' : 'all';
+    my $ref   = ($which eq 'all') ? $val_hash : \@USED_GROUPS;
+    my $keys  = $self->get_validation_keys($ref);
+    foreach my $key (sort keys %$form) {
+      next if $keys->{$key};
+      $self->add_error(\@ERRORS, $key, 'no_extra_fields', {}, undef);
+    }
+  }
+
+  ### return what they want
+  if ($#ERRORS != -1) {
+    my $err_obj = $ERROR_PACKAGE->new(\@ERRORS, \%EXTRA);
+    die    $err_obj if $EXTRA{raise_error};
+    return $err_obj;
+  } else {
+    return wantarray ? () : undef;
+  }
+}
+
+
+### allow for optional validation on groups and on individual items
+sub check_conditional {
+  my ($self, $form, $ifs, $N_level, $ifs_match) = @_;
+
+  $N_level ||= 0;
+  $N_level ++; # prevent too many recursive checks
+
+  ### can pass a single hash - or an array ref of hashes
+  if (! $ifs) {
+    die "Need reference passed to check_conditional";
+  } elsif (! ref($ifs)) {
+    $ifs = [$ifs];
+  } elsif (UNIVERSAL::isa($ifs,'HASH')) {
+    $ifs = [$ifs];
+  }
+
+  ### run the if options here
+  ### multiple items can be passed - all are required unless OR is used to separate
+  my $found = 1;
+  foreach (my $i = 0; $i <= $#$ifs; $i ++) {
+    my $ref = $ifs->[$i];
+    if (! ref $ref) {
+      if ($ref eq 'OR') {
+        $i ++ if $found; # if found skip the OR altogether
+        $found = 1; # reset
+        next;
+      } else {
+        if ($ref =~ s/^\s*!\s*//) {
+          $ref = {field => $ref, max_in_set => "0 of $ref"};
+        } else {
+          $ref = {field => $ref, required => 1};
+        }
+      }
+    }
+    last if ! $found;
+
+    ### get the field - allow for custom variables based upon a match
+    my $field = $ref->{'field'} || die "Missing field key during validate_if (possibly used a reference to a main hash *foo -> &foo)";
+    $field =~ s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg if $ifs_match;
+
+    my @err = $self->validate_buddy($form, $field, $ref, $N_level);
+    $found = 0 if scalar @err;
+  }
+  return $found;
+}
+
+
+### this is where the main checking goes on
+sub validate_buddy {
+  my $self = shift;
+  my ($form, $field, $field_val, $N_level, $ifs_match) = @_;
+  $N_level ||= 0;
+  $N_level ++; # prevent too many recursive checks
+  die "Max dependency level reached $N_level" if $N_level > 10;
+
+  my @errors = ();
+  my $types  = [sort keys %$field_val];
+
+  ### allow for not running some tests in the cgi
+  if (scalar $self->filter_type('exclude_cgi',$types)) {
+    delete $field_val->{'was_validated'};
+    return wantarray ? @errors : $#errors + 1;
+  }
+
+  ### allow for field names that contain regular expressions
+  if ($field =~ m/^(!\s*|)m([^\s\w])(.*)\2([eigsmx]*)$/s) {
+    my ($not,$pat,$opt) = ($1,$3,$4);
+    $opt =~ tr/g//d;
+    die "The e option cannot be used on validation keys on field $field" if $opt =~ /e/;
+    foreach my $_field (sort keys %$form) {
+      next if ($not && $_field =~ m/(?$opt:$pat)/) || (! $not && $_field !~ m/(?$opt:$pat)/);
+      my @match = (undef,$1,$2,$3,$4,$5); # limit to the matches
+      push @errors, $self->validate_buddy($form, $_field, $field_val, $N_level, \@match);
+    }
+    return wantarray ? @errors : $#errors + 1;
+  }
+
+  ### allow for default value
+  foreach my $type ($self->filter_type('default', $types)) {
+    if (! defined($form->{$field}) || (! ref($form->{$field}) && ! length($form->{$field}))) {
+      $form->{$field} = $field_val->{$type};
+    }
+  }
+
+  my $n_values = UNIVERSAL::isa($form->{$field},'ARRAY') ? $#{ $form->{$field} } + 1 : 1;
+  my $values = ($n_values > 1) ? $form->{$field} : [$form->{$field}];
+
+  ### allow for a few form modifiers
+  my $modified = 0;
+  foreach my $value (@$values) {
+    next if ! defined $value;
+    if (! scalar $self->filter_type('do_not_trim',$types)) { # whitespace
+      $value =~ s/^\s+//;
+      $value =~ s/\s+$//;
+      $modified = 1;
+    }
+    if (scalar $self->filter_type('to_upper_case',$types)) { # uppercase
+      $value = uc($value);
+      $modified = 1;
+    } elsif (scalar $self->filter_type('to_lower_case',$types)) { # lowercase
+      $value = lc($value);
+      $modified = 1;
+    }
+  }
+  # allow for inline specified modifications (ie s/foo/bar/)
+  foreach my $type ($self->filter_type('replace',$types)) {
+    my $ref = UNIVERSAL::isa($field_val->{$type},'ARRAY') ? $field_val->{$type}
+      : [split(/\s*\|\|\s*/,$field_val->{$type})];
+    foreach my $rx (@$ref) {
+      if ($rx !~ m/^\s*s([^\s\w])(.+)\1(.*)\1([eigsmx]*)$/s) {
+        die "Not sure how to parse that match ($rx)";
+      }
+      my ($pat,$swap,$opt) = ($2,$3,$4);
+      die "The e option cannot be used in swap on field $field" if $opt =~ /e/;
+      my $global = $opt =~ s/g//g;
+      $swap =~ s/\\n/\n/g;
+      if ($global) {
+        foreach my $value (@$values) {
+          $value =~ s{(?$opt:$pat)}{
+            my @match = (undef,$1,$2,$3,$4,$5,$6); # limit on the number of matches
+            my $copy = $swap;
+            $copy =~ s/\$(\d+)/defined($match[$1]) ? $match[$1] : ""/ge;
+            $modified = 1;
+            $copy; # return of the swap
+          }eg;
+        }
+      }else{
+        foreach my $value (@$values) {
+          $value =~ s{(?$opt:$pat)}{
+            my @match = (undef,$1,$2,$3,$4,$5,$6); # limit on the number of matches
+            my $copy = $swap;
+            $copy =~ s/\$(\d+)/defined($match[$1]) ? $match[$1] : ""/ge;
+            $modified = 1;
+            $copy; # return of the swap
+          }e;
+        }
+      }
+    }
+  }
+  ### put them back into the form if we have modified it
+  if ($modified) {
+    if ($n_values == 1) {
+      $form->{$field} = $values->[0];
+      $self->{cgi_object}->param(-name => $field, -value => $values->[0])
+        if $self->{cgi_object};
+    } else {
+      ### values in @{ $form->{$field} } were modified directly
+      $self->{cgi_object}->param(-name => $field, -value => $values)
+        if $self->{cgi_object};
+    }
+  }
+
+  ### only continue if a validate_if is not present or passes test
+  my $needs_val = 0;
+  my $n_vif = 0;
+  foreach my $type ($self->filter_type('validate_if',$types)) {
+    $n_vif ++;
+    my $ifs = $field_val->{$type};
+    my $ret = $self->check_conditional($form, $ifs, $N_level, $ifs_match);
+    $needs_val ++ if $ret;
+  }
+  if (! $needs_val && $n_vif) {
+    delete $field_val->{'was_validated'};
+    return wantarray ? @errors : $#errors + 1;
+  }
+
+  ### check for simple existence
+  ### optionally check only if another condition is met
+  my $is_required = '';
+  foreach my $type ($self->filter_type('required',$types)) {
+    next if ! $field_val->{$type};
+    $is_required = $type;
+    last;
+  }
+  if (! $is_required) {
+    foreach my $type ($self->filter_type('required_if',$types)) {
+      my $ifs = $field_val->{$type};
+      next if ! $self->check_conditional($form, $ifs, $N_level, $ifs_match);
+      $is_required = $type;
+      last;
+    }
+  }
+  if ($is_required && (! defined($form->{$field})
+                       || ((UNIVERSAL::isa($form->{$field},'ARRAY') && $#{ $form->{$field} } == -1)
+                           || ! length($form->{$field})))) {
+    return 1 if ! wantarray;
+    $self->add_error(\@errors, $field, $is_required, $field_val, $ifs_match);
+    return @errors;
+  }
+
+  ### min values check
+  foreach my $type ($self->filter_type('min_values',$types)) {
+    my $n = $field_val->{$type} || 0;
+    if ($n_values < $n) {
+      return 1 if ! wantarray;
+      $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+      return @errors;
+    }
+  }
+
+  ### max values check
+  my @keys = $self->filter_type('max_values',$types);
+  if ($#keys == -1) {
+    push @keys, 'max_values';
+    $field_val->{'max_values'} = 1;
+  }
+  foreach my $type (@keys) {
+    my $n = $field_val->{$type} || 0;
+    if ($n_values > $n) {
+      return 1 if ! wantarray;
+      $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+      return @errors;
+    }
+  }
+
+  ### max_in_set and min_in_set checks
+  foreach my $minmax (qw(min max)) {
+    my @keys = $self->filter_type("${minmax}_in_set",$types);
+    foreach my $type (@keys) {
+      $field_val->{$type} =~ m/^\s*(\d+)(?i:\s*of)?\s+(.+)\s*$/
+        || die "Invalid in_set check $field_val->{$type}";
+      my $n = $1;
+      foreach my $_field (split /[\s,]+/, $2) {
+        my $ref = UNIVERSAL::isa($form->{$_field},'ARRAY') ? $form->{$_field} : [$form->{$_field}];
+        foreach my $_value (@$ref) {
+          $n -- if defined($_value) && length($_value);
+        }
+      }
+      if (   ($minmax eq 'min' && $n > 0)
+          || ($minmax eq 'max' && $n < 0)) {
+        return 1 if ! wantarray;
+        $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+        return @errors;
+      }
+    }
+  }
+
+  ### at this point @errors should still be empty
+  my $content_checked; # allow later for possible untainting (only happens if content was checked)
+
+  ### loop on values of field
+  foreach my $value (@$values) {
+
+    ### allow for enum types
+    foreach my $type ($self->filter_type('enum',$types)) {
+      my $ref = ref($field_val->{$type}) ? $field_val->{$type} : [split(/\s*\|\|\s*/,$field_val->{$type})];
+      my $found = 0;
+      foreach (@$ref) {
+        $found = 1 if defined($value) && $_ eq $value;
+      }
+      if (! $found) {
+        return 1 if ! wantarray;
+        $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+      }
+      $content_checked = 1;
+    }
+
+    ### field equality test
+    foreach my $type ($self->filter_type('equals',$types)) {
+      my $field2  = $field_val->{$type};
+      my $not     = ($field2 =~ s/^!\s*//) ? 1 : 0;
+      my $success = 0;
+      if ($field2 =~ m/^([\"\'])(.*)\1$/) {
+        my $test = $2;
+        $success = (defined($value) && $value eq $test);
+      } elsif (exists($form->{$field2}) && defined($form->{$field2})) {
+        $success = (defined($value) && $value eq $form->{$field2});
+      } elsif (! defined($value)) {
+        $success = 1; # occurs if they are both undefined
+      }
+      if ($not ? $success : ! $success) {
+        return 1 if ! wantarray;
+        $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+      }
+      $content_checked = 1;
+    }
+
+    ### length min check
+    foreach my $type ($self->filter_type('min_len',$types)) {
+      my $n = $field_val->{$type};
+      if (! defined($value) || length($value) < $n) {
+        return 1 if ! wantarray;
+        $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+      }
+    }
+
+    ### length max check
+    foreach my $type ($self->filter_type('max_len',$types)) {
+      my $n = $field_val->{$type};
+      if (defined($value) && length($value) > $n) {
+        return 1 if ! wantarray;
+        $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+      }
+    }
+
+    ### now do match types
+    foreach my $type ($self->filter_type('match',$types)) {
+      my $ref = UNIVERSAL::isa($field_val->{$type},'ARRAY') ? $field_val->{$type}
+         : UNIVERSAL::isa($field_val->{$type}, 'Regexp') ? [$field_val->{$type}]
+         : [split(/\s*\|\|\s*/,$field_val->{$type})];
+      foreach my $rx (@$ref) {
+        if (UNIVERSAL::isa($rx,'Regexp')) {
+          if (! defined($value) || $value !~ $rx) {
+            $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+          }
+        } else {
+          if ($rx !~ m/^(!\s*|)m([^\s\w])(.*)\2([eigsmx]*)$/s) {
+            die "Not sure how to parse that match ($rx)";
+          }
+          my ($not,$pat,$opt) = ($1,$3,$4);
+          $opt =~ tr/g//d;
+          die "The e option cannot be used on validation keys on field $field" if $opt =~ /e/;
+          if ( (     $not && (  defined($value) && $value =~ m/(?$opt:$pat)/))
+               || (! $not && (! defined($value) || $value !~ m/(?$opt:$pat)/))
+               ) {
+            return 1 if ! wantarray;
+            $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+          }
+        }
+      }
+      $content_checked = 1;
+    }
+
+    ### allow for comparison checks
+    foreach my $type ($self->filter_type('compare',$types)) {
+      my $ref = UNIVERSAL::isa($field_val->{$type},'ARRAY') ? $field_val->{$type}
+        : [split(/\s*\|\|\s*/,$field_val->{$type})];
+      foreach my $comp (@$ref) {
+        next if ! $comp;
+        my $test  = 0;
+        if ($comp =~ /^\s*(>|<|[><!=]=)\s*([\d\.\-]+)\s*$/) {
+          my $val = $value || 0;
+          $val *= 1;
+          if    ($1 eq '>' ) { $test = ($val >  $2) }
+          elsif ($1 eq '<' ) { $test = ($val <  $2) }
+          elsif ($1 eq '>=') { $test = ($val >= $2) }
+          elsif ($1 eq '<=') { $test = ($val <= $2) }
+          elsif ($1 eq '!=') { $test = ($val != $2) }
+          elsif ($1 eq '==') { $test = ($val == $2) }
+
+        } elsif ($comp =~ /^\s*(eq|ne|gt|ge|lt|le)\s+(.+?)\s*$/) {
+          my $val = defined($value) ? $value : '';
+          my ($op, $value2) = ($1, $2);
+          $value2 =~ s/^([\"\'])(.*)\1$/$2/;
+          if    ($op eq 'gt') { $test = ($val gt $value2) }
+          elsif ($op eq 'lt') { $test = ($val lt $value2) }
+          elsif ($op eq 'ge') { $test = ($val ge $value2) }
+          elsif ($op eq 'le') { $test = ($val le $value2) }
+          elsif ($op eq 'ne') { $test = ($val ne $value2) }
+          elsif ($op eq 'eq') { $test = ($val eq $value2) }
+
+        } else {
+          die "Not sure how to compare \"$comp\"";
+        }
+        if (! $test) {
+          return 1 if ! wantarray;
+          $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+        }
+      }
+      $content_checked = 1;
+    }
+
+    ### server side sql type
+    foreach my $type ($self->filter_type('sql',$types)) {
+      my $db_type = $field_val->{"${type}_db_type"};
+      my $dbh = ($db_type) ? $self->{dbhs}->{$db_type} : $self->{dbh};
+      if (! $dbh) {
+        die "Missing dbh for $type type on field $field" . ($db_type ? " and db_type $db_type" : "");
+      } elsif (UNIVERSAL::isa($dbh,'CODE')) {
+        $dbh = &$dbh($field, $self) || die "SQL Coderef did not return a dbh";
+      }
+      my $sql  = $field_val->{$type};
+      my @args = ($value) x $sql =~ tr/?//;
+      my $return = $dbh->selectrow_array($sql, {}, @args); # is this right - copied from O::FORMS
+      $field_val->{"${type}_error_if"} = 1 if ! defined $field_val->{"${type}_error_if"};
+      if ( (! $return && $field_val->{"${type}_error_if"})
+           || ($return && ! $field_val->{"${type}_error_if"}) ) {
+        return 1 if ! wantarray;
+        $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+      }
+      $content_checked = 1;
+    }
+
+    ### server side custom type
+    foreach my $type ($self->filter_type('custom',$types)) {
+      my $check = $field_val->{$type};
+      next if UNIVERSAL::isa($check, 'CODE') ? &$check($field, $value, $field_val, $type) : $check;
+      return 1 if ! wantarray;
+      $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+      $content_checked = 1;
+    }
+
+    ### do specific type checks
+    foreach my $type ($self->filter_type('type',$types)) {
+      if (! $self->check_type($value,$field_val->{'type'},$field,$form)){
+        return 1 if ! wantarray;
+        $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+      }
+      $content_checked = 1;
+    }
+  }
+
+  ### allow for the data to be "untainted"
+  ### this is only allowable if the user ran some other check for the datatype
+  foreach my $type ($self->filter_type('untaint',$types)) {
+    last if $#errors != -1;
+    if (! $content_checked) {
+      $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+    } else {
+      ### generic untainter - assuming the other required content_checks did good validation
+      $_ = /(.*)/ ? $1 : die "Couldn't match?" foreach @$values;
+      if ($n_values == 1) {
+        $form->{$field} = $values->[0];
+        $self->{cgi_object}->param(-name => $field, -value => $values->[0])
+          if $self->{cgi_object};
+      } else {
+        ### values in @{ $form->{$field} } were modified directly
+        $self->{cgi_object}->param(-name => $field, -value => $values)
+          if $self->{cgi_object};
+      }
+    }
+  }
+
+  ### all done - time to return
+  return wantarray ? @errors : $#errors + 1;
+}
+
+### simple error adder abstraction
+sub add_error {
+  my $self = shift;
+  my $errors = shift;
+  push @$errors, \@_;
+}
+
+### allow for multiple validations in the same hash
+### ie Match, Match1, Match2, Match234
+sub filter_type {
+  my $self  = shift;
+  my $type  = shift;
+  my $order = shift || die "Missing order array";
+  my @array = ();
+  foreach (@$order) {
+    push @array, $_ if /^\Q$type\E_?\d*$/;
+  }
+  return wantarray ? @array : $#array + 1;
+}
+
+###----------------------------------------------------------------###
+
+### used to validate specific types
+sub check_type {
+  my $self  = shift;
+  my $value = shift;
+  my $type  = uc(shift);
+
+  ### do valid email address for our system
+  if ($type eq 'EMAIL') {
+    return 0 if ! $value;
+    my($local_p,$dom) = ($value =~ /^(.+)\@(.+?)$/) ? ($1,$2) : return 0;
+
+    return 0 if length($local_p) > 60;
+    return 0 if length($dom) > 100;
+    return 0 if ! $self->check_type($dom,'DOMAIN') && ! $self->check_type($dom,'IP');
+    return 0 if ! $self->check_type($local_p,'LOCAL_PART');
+
+  ### the "username" portion of an email address
+  } elsif ($type eq 'LOCAL_PART') {
+    return 0 if ! defined($value) || ! length($value);
+    return 0 if $value =~ m/[^a-z0-9.\-\!\&]/;
+    return 0 if $value =~ m/^[\.\-]/;
+    return 0 if $value =~ m/[\.\-\&]$/;
+    return 0 if $value =~ m/(\.\-|\-\.|\.\.)/;
+
+  ### standard IP address
+  } elsif ($type eq 'IP') {
+    return 0 if ! $value;
+    return (4 == grep {!/\D/ && $_ < 256} split /\./, $value, 4);
+
+  ### domain name - including tld and subdomains (which are all domains)    
+  } elsif ($type eq 'DOMAIN') {
+    return 0 if ! $value;
+    return 0 if $value =~ m/[^a-z0-9.\-]/;
+    return 0 if $value =~ m/^[\.\-]/;
+    return 0 if $value =~ m/(\.\-|\-\.|\.\.)/;
+    return 0 if length($value) > 255;
+    return 0 if $value !~ s/\.([a-z]+)$//;
+
+    my $ext = $1;
+    if ($ext eq 'name') { # .name domains
+      return 0 if $value !~ /^[a-z0-9][a-z0-9\-]{0,62} \. [a-z0-9][a-z0-9\-]{0,62}$/x;
+    } else {              # any other domains
+      return 0 if $value !~ /^([a-z0-9][a-z0-9\-]{0,62} \.)* [a-z0-9][a-z0-9\-]{0,62}$/x;
+    }
+
+  ### validate a url
+  } elsif ($type eq 'URL') {
+    return 0 if ! $value;
+    $value =~ s|^https?://([^/]+)||i || return 0;
+    my $dom = $1;
+    return 0 if ! $self->check_type($dom,'DOMAIN') && ! $self->check_type($dom,'IP');
+    return 0 if $value && ! $self->check_type($value,'URI');
+
+  ### validate a uri - the path portion of a request
+  } elsif ($type eq 'URI') {
+    return 0 if ! $value;
+    return 0 if $value =~ m/\s+/;
+
+  } elsif ($type eq 'CC') {
+    return 0 if ! $value;
+    ### validate the number
+    return 0 if $value =~ /[^\d\-\ ]/
+      || length($value) > 16
+      || length($value) < 13;
+
+    ### simple mod10 check
+    $value =~ s/\D//g;
+    my $sum    = 0;
+    my $switch = 0;
+    foreach my $digit ( reverse split //, $value ){
+      $switch = 1 if ++ $switch > 2;
+      my $y = $digit * $switch;
+      $y -= 9 if $y > 9;
+      $sum += $y;
+    }
+    return 0 if $sum % 10;
+
+  }
+
+  return 1;
+}
+
+###----------------------------------------------------------------###
+
+sub get_validation {
+  my $self = shift;
+  my $val  = shift;
+  return $self->conf->read($val, {html_key => 'validation'});
+}
+
+### returns all keys from all groups - even if group has validate_if
+sub get_validation_keys {
+  my $self     = shift;
+  my $val_hash = shift;
+  my $form     = shift; # with optional form - will only return keys in validated groups
+  my %keys     = ();
+
+  ### if a form was passed - make sure it is a hashref
+  if ($form) {
+    if (! ref($form)) {
+      die "Invalid form hash or cgi object";
+    } elsif(! UNIVERSAL::isa($form,'HASH')) {
+      require CGI::Ex;
+      $form = CGI::Ex->new->get_form($form);
+    }
+  }
+
+  my $refs     = $self->get_validation($val_hash);
+  $refs = [$refs] if ! UNIVERSAL::isa($refs,'ARRAY');
+  foreach my $group_val (@$refs) {
+    die "Group found that was not a hashref" if ! UNIVERSAL::isa($group_val, 'HASH');
+
+    ### if form is passed, check to see if the group passed validation
+    if ($form) {
+      my $validate_if = $group_val->{'group validate_if'};
+      next if $validate_if && ! $self->check_conditional($form, $validate_if);
+    }
+
+    if ($group_val->{"group fields"}) {
+      die "Group fields must be an arrayref" if ! UNIVERSAL::isa($group_val->{"group fields"}, 'ARRAY');
+      foreach my $field_val (@{ $group_val->{"group fields"} }) {
+        next if ! ref($field_val) && $field_val eq 'OR';
+        die "Field_val must be a hashref" if ! UNIVERSAL::isa($field_val, 'HASH');
+        my $key = $field_val->{'field'} || die "Missing field key in field_val hashref";
+        $keys{$key} = $field_val->{'name'} || 1;
+      }
+    } elsif ($group_val->{"group order"}) {
+      die "Group order must be an arrayref" if ! UNIVERSAL::isa($group_val->{"group order"}, 'ARRAY');
+      foreach my $key (@{ $group_val->{"group order"} }) {
+        my $field_val = $group_val->{$key};
+        next if ! $field_val && $key eq 'OR';
+        die "Field_val for $key must be a hashref" if ! UNIVERSAL::isa($field_val, 'HASH');
+        $key = $field_val->{'field'} if $field_val->{'field'};
+        $keys{$key} = $field_val->{'name'} || 1;
+      }
+    }
+
+    ### get all others
+    foreach my $key (keys %$group_val) {
+      next if $key =~ /^(general|group)\s/;
+      my $field_val = $group_val->{$key};
+      next if ! UNIVERSAL::isa($field_val, 'HASH');
+      $keys{$key} = $field_val->{'name'} || 1;
+    }
+  }
+
+  return \%keys;
+}
+
+###----------------------------------------------------------------###
+
+### spit out a chunk that will do the validation
+sub generate_js {
+  ### allow for some browsers to not receive the validation
+  if ($ENV{HTTP_USER_AGENT}) {
+    foreach (@UNSUPPORTED_BROWSERS) {
+      next if $ENV{HTTP_USER_AGENT} !~ $_;
+      return "<!-- JS Validation not supported in this browser $_ -->"
+    }
+  }
+
+  my $self        = shift;
+  my $val_hash    = shift || die "Missing validation";
+  my $form_name   = shift || die "Missing form name";
+  my $js_uri_path = shift || $JS_URI_PATH;
+  $val_hash = $self->get_validation($val_hash);
+  require YAML;
+
+  ### store any extra items from self
+  my %EXTRA = ();
+  foreach my $key (keys %$self) {
+    next if $key !~ $QR_EXTRA;
+    $EXTRA{"general $key"} = $self->{$key};
+  }
+
+  my $str = &YAML::Dump((scalar keys %EXTRA) ? (\%EXTRA) : () , $val_hash);
+  $str =~ s/(?<!\\)\\(?=[sSdDwWbB0-9?.*+|\-\^\${}()\[\]])/\\\\/g;
+  $str =~ s/\n/\\n\\\n/g; # allow for one big string
+  $str =~ s/\"/\\\"/g; # quotify it
+
+  ### get the paths
+  my $js_uri_path_yaml = $JS_URI_PATH_YAML || do {
+    die "Missing \$js_uri_path" if ! $js_uri_path;
+    "$js_uri_path/CGI/Ex/yaml_load.js";
+  };
+  my $js_uri_path_validate = $JS_URI_PATH_VALIDATE || do {
+    die "Missing \$js_uri_path" if ! $js_uri_path;
+    "$js_uri_path/CGI/Ex/validate.js";
+  };
+
+  ### return the string
+  return qq{<script src="$js_uri_path_yaml"></script>
+<script src="$js_uri_path_validate"></script>
+<script><!--
+document.validation = "$str";
+if (document.check_form) document.check_form("$form_name");
+//--></script>
+};
+
+}
+
+###----------------------------------------------------------------###
+### How to handle errors
+
+package CGI::Ex::Validate::Error;
+
+use strict;
+use overload '""' => \&as_string;
+
+sub new {
+  my $class  = shift || __PACKAGE__;
+  my $errors = shift;
+  my $extra  = shift || {};
+  die "Missing or invalid arrayref" if ! UNIVERSAL::isa($errors, 'ARRAY');
+  die "Missing or invalid hashref"  if ! UNIVERSAL::isa($extra,  'HASH');
+  return bless {errors => $errors, extra => $extra}, $class;
+}
+
+sub as_string {
+  my $self = shift;
+  my $extra  = $self->{extra} || {};
+  my $extra2 = shift || {};
+
+  ### allow for formatting
+  my $join = defined($extra2->{as_string_join}) ? $extra2->{as_string_join}
+    : defined($extra->{as_string_join}) ? $extra->{as_string_join}
+    : "\n";
+  my $header = defined($extra2->{as_string_header}) ? $extra2->{as_string_header}
+    : defined($extra->{as_string_header}) ? $extra->{as_string_header} : "";
+  my $footer = defined($extra2->{as_string_footer}) ? $extra2->{as_string_footer}
+    : defined($extra->{as_string_footer}) ? $extra->{as_string_footer} : "";
+
+  return $header . join($join, @{ $self->as_array($extra2) }) . $footer;
+}
+
+### return an array of applicable errors
+sub as_array {
+  my $self = shift;
+  my $errors = $self->{errors} || die "Missing errors";
+  my $extra  = $self->{extra}  || {};
+  my $extra2 = shift || {};
+
+  my $title = defined($extra2->{as_array_title}) ? $extra2->{as_array_title}
+    : defined($extra->{as_array_title}) ? $extra->{as_array_title}
+    : "Please correct the following items:";
+
+  ### if there are heading items then we may end up needing a prefix
+  my $has_headings;
+  if ($title) {
+    $has_headings = 1;
+  } else {
+    foreach (@$errors) {
+      next if ref;
+      $has_headings = 1;
+      last;
+    }
+  }
+
+  my $prefix = defined($extra2->{as_array_prefix}) ? $extra2->{as_array_prefix}
+    : defined($extra->{as_array_prefix}) ? $extra->{as_array_prefix}
+    : $has_headings ? '  ' : '';
+
+  ### get the array ready
+  my @array = ();
+  push @array, $title if length $title;
+
+  ### add the errors
+  my %found = ();
+  foreach my $err (@$errors) {
+    if (! ref $err) {
+      push @array, $err;
+      %found = ();
+    } else {
+      my $text = $self->get_error_text($err);
+      next if $found{$text};
+      $found{$text} = 1;
+      push @array, "$prefix$text";
+    }
+  }
+
+  return \@array;
+}
+
+### return a hash of applicable errors
+sub as_hash {
+  my $self = shift;
+  my $errors = $self->{errors} || die "Missing errors";
+  my $extra  = $self->{extra}  || {};
+  my $extra2 = shift || {};
+
+  my $suffix = defined($extra2->{as_hash_suffix}) ? $extra2->{as_hash_suffix}
+    : defined($extra->{as_hash_suffix}) ? $extra->{as_hash_suffix} : '_error';
+  my $join   = defined($extra2->{as_hash_join}) ? $extra2->{as_hash_join}
+    : defined($extra->{as_hash_join}) ? $extra->{as_hash_join} : '<br />';
+
+  ### now add to the hash
+  my %found  = ();
+  my %return = ();
+  foreach my $err (@$errors) {
+    next if ! ref $err;
+
+    my ($field, $type, $field_val, $ifs_match) = @$err;
+    die "Missing field name" if ! $field;
+    if ($field_val->{delegate_error}) {
+      $field = $field_val->{delegate_error};
+      $field =~ s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg if $ifs_match;
+    }
+
+    my $text = $self->get_error_text($err);
+    next if $found{$field}->{$text};
+    $found{$field}->{$text} = 1;
+
+    $field .= $suffix;
+    $return{$field} ||= [];
+    $return{$field} = [$return{$field}] if ! ref($return{$field});
+    push @{ $return{$field} }, $text;
+  }
+
+  ### allow for elements returned as
+  if ($join) {
+    my $header = defined($extra2->{as_hash_header}) ? $extra2->{as_hash_header}
+      : defined($extra->{as_hash_header}) ? $extra->{as_hash_header} : "";
+    my $footer = defined($extra2->{as_hash_footer}) ? $extra2->{as_hash_footer}
+      : defined($extra->{as_hash_footer}) ? $extra->{as_hash_footer} : "";
+    foreach my $key (keys %return) {
+      $return{$key} = $header . join($join,@{ $return{$key} }) . $footer;
+    }
+  }
+
+  return \%return;
+}
+
+### return a user friendly error message
+sub get_error_text {
+  my $self  = shift;
+  my $err   = shift;
+  my $extra = $self->{extra} || {};
+  my ($field, $type, $field_val, $ifs_match) = @$err;
+  my $dig     = ($type =~ s/(_?\d+)$//) ? $1 : '';
+  my $type_lc = lc($type);
+
+  ### allow for delegated field names - only used for defaults
+  if ($field_val->{delegate_error}) {
+    $field = $field_val->{delegate_error};
+    $field =~ s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg if $ifs_match;
+  }
+
+  ### the the name of this thing
+  my $name = $field_val->{'name'} || "The field $field";
+  $name =~ s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg if $ifs_match;
+
+  ### type can look like "required" or "required2" or "required100023"
+  ### allow for fallback from required100023_error through required_error
+  my @possible_error_keys = ("${type}_error");
+  unshift @possible_error_keys, "${type}${dig}_error" if length($dig);
+
+  ### look in the passed hash or self first
+  my $return;
+  foreach my $key (@possible_error_keys){
+    $return = $field_val->{$key} || $extra->{$key} || next;
+    $return =~ s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg if $ifs_match;
+    $return =~ s/\$field/$field/g;
+    $return =~ s/\$name/$name/g;
+    if (my $value = $field_val->{"$type$dig"}) {
+      $return =~ s/\$value/$value/g if ! ref $value;
+    }
+    last;
+  }
+
+  ### set default messages
+  if (! $return) {
+    if ($type eq 'required' || $type eq 'required_if') {
+      $return = "$name is required.";
+
+    } elsif ($type eq 'min_values') {
+      my $n = $field_val->{"min_values${dig}"};
+      my $values = ($n == 1) ? 'value' : 'values';
+      $return = "$name had less than $n $values.";
+
+    } elsif ($type eq 'max_values') {
+      my $n = $field_val->{"max_values${dig}"};
+      my $values = ($n == 1) ? 'value' : 'values';
+      $return = "$name had more than $n $values.";
+
+    } elsif ($type eq 'enum') {
+      $return = "$name is not in the given list.";
+
+    } elsif ($type eq 'equals') {
+      my $field2 = $field_val->{"equals${dig}"};
+      my $name2  = $field_val->{"equals${dig}_name"} || "the field $field2";
+      $name2 =~ s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg if $ifs_match;
+      $return = "$name did not equal $name2.";
+
+    } elsif ($type eq 'min_len') {
+      my $n = $field_val->{"min_len${dig}"};
+      my $char = ($n == 1) ? 'character' : 'characters';
+      $return = "$name was less than $n $char.";
+
+    } elsif ($type eq 'max_len') {
+      my $n = $field_val->{"max_len${dig}"};
+      my $char = ($n == 1) ? 'character' : 'characters';
+      $return = "$name was more than $n $char.";
+
+    } elsif ($type eq 'max_in_set') {
+      my $set = $field_val->{"max_in_set${dig}"};
+      $return = "Too many fields were chosen from the set ($set)";
+
+    } elsif ($type eq 'min_in_set') {
+      my $set = $field_val->{"min_in_set${dig}"};
+      $return = "Not enough fields were chosen from the set ($set)";
+
+    } elsif ($type eq 'match') {
+      $return = "$name contains invalid characters.";
+
+    } elsif ($type eq 'compare') {
+      $return = "$name did not fit comparison.";
+
+    } elsif ($type eq 'sql') {
+      $return = "$name did not match sql test.";
+
+    } elsif ($type eq 'custom') {
+      $return = "$name did not match custom test.";
+
+    } elsif ($type eq 'type') {
+      my $_type = $field_val->{"type${dig}"};
+      $return = "$name did not match type $_type.";
+
+    } elsif ($type eq 'untaint') {
+      $return = "$name cannot be untainted without one of the following checks: enum, equals, match, compare, sql, type, custom";
+
+    } elsif ($type eq 'no_extra_fields') {
+      $return = "$name should not be passed to validate.";
+    }
+  }
+
+  die "Missing error on field $field for type $type$dig" if ! $return;
+  return $return;
+
+}
+
+###----------------------------------------------------------------###
+
+1;
+
+
+__END__
+
+=head1 NAME
+
+CGI::Ex::Validate - Yet another form validator - does good javascript too
+
+$Id: Validate.pm,v 1.79 2005/02/23 21:28:11 pauls Exp $
+
+=head1 SYNOPSIS
+
+  use CGI::Ex::Validate;
+
+  ### THE SHORT
+
+  my $errobj = CGI::Ex::Validate->new->validate($form, $val_hash);
+
+  ### THE LONG
+
+  my $form = CGI->new;
+   # OR #
+  my $form = CGI::Ex->new; # OR CGI::Ex->get_form;
+   # OR #
+  my $form = {key1 => 'val1', key2 => 'val2'};
+
+
+  ### simplest
+  my $val_hash = {
+    username => {required => 1,
+                 max_len  => 30
+                 field    => 'username',
+                 # field is optional in this case - will use key name
+                },
+    email    => {required => 1,
+                 max_len  => 100
+                },
+    email2   => {validate_if => 'email'
+                 equals      => 'email'
+                },
+  };
+
+  ### ordered
+  my $val_hash = {
+    'group order' => [qw(username email email2)],
+    username => {required => 1, max_len => 30},
+    email    => ...,
+    email2   => ...,
+  };
+
+  ### ordered again
+  my $val_hash = {
+    'group fields' => [
+      {field    => 'username', # field is not optional in this case
+       required => 1,
+       max_len  => 30,
+      },
+      {field    => 'email',
+       required => 1,
+       max_len  => 100,
+      }
+      {field       => 'email2',
+       validate_if => 'email',
+       equals      => 'email',
+      }
+    ],
+  };
+
+  
+  my $vob    = CGI::Ex::Validate->new;
+  my $errobj = $vob->validate($form, $val_hash);
+    # OR #
+  my $errobj = $vob->validate($form, "/somefile/somewhere.val"); # import config using yaml file
+    # OR #
+  my $errobj = $vob->validate($form, "/somefile/somewhere.pl");  # import config using perl file
+    # OR #
+  my $errobj = $vob->validate($form, "--- # a yaml document\n"); # import config using yaml str
+
+
+  if ($errobj) {
+    my $error_heading = $errobj->as_string; # OR "$errobj";
+    my $error_list    = $errobj->as_array;  # ordered list of what when wrong
+    my $error_hash    = $errobj->as_hash;   # hash of arrayrefs of errors
+  } else {
+    # form passed validation
+  }
+
+  ### will add an error for any form key not found in $val_hash
+  my $vob = CGI::Ex::Validate->new({no_extra_keys => 1});
+  my $errobj = $vob->validate($form, $val_hash);  
+
+=head1 DESCRIPTION
+
+CGI::Ex::Validate is yet another module used for validating input.  It
+aims to have all of the power of former modules, while advancing them
+with more flexibility, external validation files, and identical
+javascript validation.  CGI::Ex::Validate can work in a simple way
+like all of the other validators do.  However, it also allows for
+grouping of validation items and conditional validaion of groups or
+individual items.  This is more in line with the normal validation
+procedures for a website.
+
+=head1 METHODS
+
+=over 4
+
+=item C<new>
+
+Used to instantiate the object.  Arguments are either a hash, or hashref,
+or nothing at all.  Keys of the hash become the keys of the object.
+
+=item C<get_validation>
+
+Given a filename or YAML string will return perl hash.  If more than one
+group is contained in the file, it will return an arrayref of hashrefs.
+
+  my $ref = $self->get_validation($file);
+
+=item C<get_validation_keys>
+
+Given a filename or YAML string or a validation hashref, will return all
+of the possible keys found in the validation hash.  This can be used to
+check to see if extra items have been passed to validate.  If a second
+argument contains a form hash is passed, get_validation_keys will only
+return the keys of groups that were validated.
+
+  my $key_hashref = $self->get_validation_keys($val_hash);
+
+The values of the hash are the names of the fields.
+
+=item C<validate>
+
+Arguments are a form hashref or cgi object, a validation hashref or filename, and
+an optional what_was_validated arrayref.
+If a CGI object is passed, CGI::Ex::get_form will be called on that object
+to turn it into a hashref.  If a filename is given for the validation, get_validation
+will be called on that filename.  If the what_was_validated_arrayref is passed - it
+will be populated (pushed) with the field hashes that were actually validated (anything
+that was skipped because of validate_if will not be in the array).
+
+If the form passes validation, validate will return undef.  If it fails validation, it
+will return a CGI::Ex::Validate::Error object.  If the 'raise_error' general option
+has been set, validate will die with a CGI::Ex::validate::Error object as the value.
+
+  my $err_obj = $self->validate($form, $val_hash);
+
+    # OR #
+
+  $self->{raise_error} = 1; # raise error can also be listed in the val_hash
+  eval { $self->validate($form, $val_hash) };
+  if ($@) {
+    my $err_obj = $@;
+  }
+
+=item C<generate_js>
+
+Requires YAML to work properly (see L<YAML>).
+
+Takes a validation hash, a form name, and an optional javascript uri
+path and returns Javascript that can be embedded on a page and will
+perform identical validations as the server side.  The validation can
+be any validation hash (or arrayref of hashes.  The form name must be
+the name of the form that the validation will act upon - the name is
+used to register an onsubmit function.  The javascript uri path is
+used to embed the locations two external javascript source files.
+
+
+The javascript uri path is highly dependent upon the server
+implementation and therefore must be configured manually.  It may be
+passed to generate_js, or it may be specified in $JS_URI_PATH.  There
+are two files included with this module that are needed -
+CGI/Ex/yaml_load.js and CGI/Ex/validate.js.  When generating the js
+code, generate_js will look in $JS_URI_PATH_YAML and
+$JS_URI_PATH_VALIDATE.  If either of these are not set, generate_js
+will default to "$JS_URI_PATH/CGI/Ex/yaml_load.js" and
+"$JS_URI_PATH/CGI/Ex/validate.js".
+
+  $self->generate_js($val_hash, 'my_form', "/cgi-bin/js")
+  # would generate something like the following...
+  # <script src="/cgi-bin/js/CGI/Ex/yaml_load.js"></script>
+  # <script src="/cgi-bin/js/CGI/Ex/validate.js"></script>
+  # ... more js follows ...
+
+  $CGI::Ex::Validate::JS_URI_PATH      = "/stock/js";
+  $CGI::Ex::Validate::JS_URI_PATH_YAML = "/js/yaml_load.js";
+  $self->generate_js($val_hash, 'my_form')  
+  # would generate something like the following...
+  # <script src="/js/yaml_load.js"></script>
+  # <script src="/stock/js/CGI/Ex/validate.js"></script>
+  # ... more js follows ...
+
+Referencing yaml_load.js and validate.js can be done in any of
+several ways.  They can be copied to or symlinked to a fixed location
+in the servers html directory.  They can also be printed out by a cgi.
+The method C<-E<gt>print_js> has been provided in CGI::Ex for printing
+js files found in the perl heirchy.  See L<CGI::Ex> for more details.
+The $JS_URI_PATH of "/cgi-bin/js" could contain the following:
+
+  #!/usr/bin/perl -w
+
+  use strict;
+  use CGI::Ex;
+
+  ### path_info should contain something like /CGI/Ex/yaml_load.js
+  my $info = $ENV{PATH_INFO} || '';
+  die "Invalid path" if $info !~ m|^(/\w+)+.js$|;
+  $info =~ s|^/+||;
+
+  CGI::Ex->new->print_js($info);
+  exit;
+
+The print_js method in CGI::Ex is designed to cache the javascript in
+the browser (caching is suggested as they are medium sized files).
+
+=item C<-E<gt>cgix>
+
+Returns a CGI::Ex object.  Used internally.
+
+=item C<-E<gt>conf>
+
+Returns a CGI::Ex::Conf object.  Used internally.
+
+=back
+
+=head1 VALIDATION HASH
+
+The validation hash may be passed as a perl a hashref or 
+as a filename, or as a YAML document string.  If it is a filename,
+it will be translated into a hash using the %EXT_HANDLER for the
+extension on the file.  If there is no extension, it will use $DEFAULT_EXT
+as a default.
+
+The validation hash may also be an arrayref of hashrefs.  In this
+case, each arrayref is treated as a group and is validated separately.
+
+=head1 GROUPS
+
+Each hashref that is passed as a validation hash is treated as a
+group.  Keys matching the regex m/^group\s+(\w+)$/ are reserved and
+are counted as GROUP OPTIONS.  Keys matching the regex m/^general\s+(\w+)$/
+are reserved and are counted as GENERAL OPTIONS.  Other keys (if
+any, should be keys that need validation).
+
+If the GROUP OPTION 'group validate_if' is set, the group will only
+be validated if the conditions are met.  Any group with out a validate_if
+fill be automatically validated.
+
+Each of the items listed in the group will be validated.  The
+validation order is determined in one of three ways:
+
+=over 4
+
+=item Specify 'group fields' arrayref.
+
+  # order will be (username, password, 'm/\w+_foo/', somethingelse)
+  {
+    'group title' => "User Information",
+    'group fields' => [
+      {field => 'username',   required => 1},
+      {field => 'password',   required => 1},
+      {field => 'm/\w+_foo/', required => 1},
+    ],
+    somethingelse => {required => 1},
+  }
+
+=item Specify 'group order' arrayref.
+
+  # order will be (username, password, 'm/\w+_foo/', somethingelse)
+  {
+    'group title' => "User Information",
+    'group order' => [qw(username password), 'm/\w+_foo/'],
+    username      => {required => 1},
+    password      => {required => 1},
+    'm/\w+_foo/'  => {required => 1},
+    somethingelse => {required => 1},
+  }
+
+=item Do nothing - use sorted order.
+
+  # order will be ('m/\w+_foo/', password, somethingelse, username)
+  {
+    'group title' => "User Information",
+    username      => {required => 1},
+    password      => {required => 1},
+    'm/\w+_foo/'  => {required => 1},
+    somethingelse => {required => 1},
+  }
+
+=back
+
+Each of the individual field validation hashrefs should contain
+the types listed in VALIDATION TYPES.
+
+Optionally the 'group fields' or the 'group order' may contain the word
+'OR' as a special keyword.  If the item preceding 'OR' fails validation
+the item after 'OR' will be tested instead.  If the item preceding 'OR'
+passes validation the item after 'OR' will not be tested.
+
+  'group order' => [qw(zip OR postalcode state OR region)],
+
+Each individual validation hashref will operate on the field contained
+in the 'field' key.  This key may also be a regular expression in the
+form of 'm/somepattern/'.  If a regular expression is used, all keys
+matching that pattern will be validated.
+
+=head1 VALIDATION TYPES
+
+The following are the available validation types.  Multiple instances of
+the same type may be used by adding a number to the type (ie match, match2,
+match232, match_94).  Multiple instances are validated in sorted order.
+
+=over 4
+
+=item C<validate_if>
+
+If validate_if is specified, the field will only be validated
+if the conditions are met.  Works in JS.
+
+  validate_if => {field => 'name', required => 1, max_len => 30}
+  # Will only validate if the field "name" is present and is less than 30 chars.
+
+  validate_if => 'name',
+  # SAME as
+  validate_if => {field => 'name', required => 1},
+
+  validate_if => '! name',
+  # SAME as
+  validate_if => {field => 'name', max_in_set => '0 of name'},
+
+  validate_if => {field => 'country', compare => "eq US"},
+  # only if country's value is equal to US
+
+  validate_if => {field => 'country', compare => "ne US"},
+  # if country doesn't equal US
+
+  validate_if => {field => 'password', match => 'm/^md5\([a-z0-9]{20}\)$/'},
+  # if password looks like md5(12345678901234567890)
+
+  {
+    field       => 'm/^(\w+)_pass/',
+    validate_if => '$1_user',
+    required    => 1,
+  }
+  # will validate foo_pass only if foo_user was present.
+
+The validate_if may also contain an arrayref of validation items.  So that
+multiple checks can be run.  They will be run in order.  validate_if will
+return true only if all options returned true.
+
+  validate_if => ['email', 'phone', 'fax']
+
+Optionally, if validate_if is an arrayref, it may contain the word
+'OR' as a special keyword.  If the item preceding 'OR' fails validation
+the item after 'OR' will be tested instead.  If the item preceding 'OR'
+passes validation the item after 'OR' will not be tested.
+
+  validate_if => [qw(zip OR postalcode)],
+
+=item C<required_if>
+
+Requires the form field if the condition is satisfied.  The conditions
+available are the same as for validate_if.  This is somewhat the same
+as saying:
+
+  validate_if => 'some_condition',
+  required    => 1
+
+  required_if => 'some_condition',
+
+  {
+    field       => 'm/^(\w+)_pass/',
+    required_if => '$1_user',
+  }
+  
+=item C<required>
+
+Requires the form field to have some value.  If the field is not present,
+no other checks will be run.
+
+=item C<min_values> and C<max_values>
+
+Allows for specifying the maximum number of form elements passed.
+max_values defaults to 1 (You must explicitly set it higher
+to allow more than one item by any given name).
+
+=item C<min_in_set> and C<max_in_set>
+
+Somewhat like min_values and max_values except that you specify the
+fields that participate in the count.  Also - entries that are not
+defined or do not have length are not counted.  An optional "of" can
+be placed after the number for human readibility.
+
+  min_in_set => "2 of foo bar baz",
+    # two of the fields foo, bar or baz must be set
+    # same as
+  min_in_set => "2 foo bar baz",
+    # same as 
+  min_in_set => "2 OF foo bar baz",
+
+  validate_if => {field => 'whatever', max_in_set => '0 of whatever'},
+    # only run validation if there were zero occurances of whatever
+
+=item C<enum>
+
+Allows for checking whether an item matches a set of options.  In perl
+the value may be passed as an arrayref.  In the conf or in perl the
+value may be passed of the options joined with ||.
+
+  {
+    field => 'password_type',
+    enum  => 'plaintext||crypt||md5', # OR enum => [qw(plaintext crypt md5)],
+  }
+
+=item C<equals>
+
+Allows for comparison of two form elements.  Can have an optional !.
+
+  {
+    field  => 'password',
+    equals => 'password_verify',
+  },
+  {
+    field  => 'domain1',
+    equals => '!domain2', # make sure the fields are not the same
+  }
+
+=item C<min_len and max_len>
+
+Allows for check on the length of fields
+
+  {
+    field   => 'site',
+    min_len => 4,
+    max_len => 100,
+  }
+
+=item C<match>
+
+Allows for regular expression comparison.  Multiple matches may
+be concatenated with ||.  Available in JS.
+
+  {
+    field   => 'my_ip',
+    match   => 'm/^\d{1,3}(\.\d{1,3})3$/',
+    match_2 => '!/^0\./ || !/^192\./',
+  }
+
+=item C<compare>
+
+Allows for custom comparisons.  Available types are
+>, <, >=, <=, !=, ==, gt, lt, ge, le, ne, and eq.  Comparisons
+also work in the JS.
+
+  {
+    field    => 'my_number',
+    match    => 'm/^\d+$/',
+    compare1 => '> 100',
+    compare2 => '< 255',
+    compare3 => '!= 150',
+  }
+
+=item C<sql>
+
+SQL query based - not available in JS.  The database handle will be looked
+for in the value $self->{dbhs}->{foo} if sql_db_type is set to 'foo',
+otherwise it will default to $self->{dbh}.  If $self->{dbhs}->{foo} or
+$self->{dbh} is a coderef - they will be called and should return a dbh.
+
+  {
+    field => 'username',
+    sql   => 'SELECT COUNT(*) FROM users WHERE username = ?',
+    sql_error_if => 1, # default is 1 - set to 0 to negate result
+    # sql_db_type  => 'foo', # will look for a dbh under $self->{dbhs}->{foo}
+  }
+
+=item C<custom>
+
+Custom value - not available in JS.  Allows for extra programming types.
+May be either a boolean value predetermined before calling validate, or may be
+a coderef that will be called during validation.  If coderef is called, it will
+be passed the field name, the form value for that name, and a reference to the
+field validation hash.  If the custom type returns false the element fails
+validation and an error is added.
+
+  {
+    field => 'username',
+    custom => sub {
+      my ($key, $val, $type, $field_val_hash) = @_;
+      # do something here
+      return 0;
+    },
+  }
+
+=item C<custom_js>
+
+Custom value - only available in JS.  Allows for extra programming types.
+May be either a boolean value predermined before calling validate, or may be
+section of javascript that will be eval'ed.  The last value (return value) of
+the eval'ed javascript will determine if validation passed.  A false value indicates
+the value did not pass validation.  A true value indicates that it did.  See
+the t/samples/js_validate_3.html page for a sample of usage.
+
+  {
+    field => 'date',
+    required => 1,
+    match    => 'm|^\d\d\d\d/\d\d/\d\d$|',
+    match_error => 'Please enter date in YYYY/MM/DD format',
+    custom_js => "
+      var t=new Date();
+      var y=t.getYear()+1900;
+      var m=t.getMonth() + 1;
+      var d=t.getDate();
+      if (m<10) m = '0'+m;
+      if (d<10) d = '0'+d;
+      (value > ''+y+'/'+m+'/'+d) ? 1 : 0;
+    ",
+    custom_js_error => 'The date was not greater than today.',
+  }
+
+=item C<type>
+Allows for more strict type checking.  Many types will be added and
+will be available from javascript as well.  Currently support types
+are CC.
+
+  {
+    field => 'credit_card',
+    type  => 'CC',
+  }
+
+=back
+
+=head1 SPECIAL VALIDATION TYPES
+
+=over 4
+
+=item C<field>
+
+Specify which field to work on.  Key may be a regex in the form 'm/\w+_user/'.
+This key is required if 'group fields' is used or if validate_if or required_if
+are used.  It can optionally be used with other types to specify a different form
+element to operate on.  On errors, if a non-default error is found, $field
+will be swapped with the value found in field.
+
+The field name may also be a regular expression in the
+form of 'm/somepattern/'.  If a regular expression is used, all keys
+matching that pattern will be validated.
+
+=item C<name>
+
+Name to use for errors.  If a name is not specified, default errors will use
+"The field $field" as the name.  If a non-default error is found, $name
+will be swapped with this name.
+
+=item C<delegate_error>
+
+This option allows for any errors generated on a field to delegate to
+a different field.  If the field name was a regex, any patterns will
+be swapped into the delegate_error value. This option is generally only
+useful with the as_hash method of the error object (for inline errors).
+
+  {
+    field => 'zip',
+    match => 'm/^\d{5}/',
+  },
+  {
+    field => 'zip_plus4',
+    match => 'm/^\d{4}/',
+    delegate_error => 'zip',
+  },
+
+  {
+    field => 'm/^(id_[\d+])_user$/',
+    delegate_error => '$1',
+  },
+
+=item C<exclude_js>
+
+This allows the cgi to do checking while keeping the checks from
+being run in JavaScript
+
+  {
+    field      => 'cgi_var',
+    required   => 1,
+    exclude_js => 1,
+  }
+
+=item C<exclude_cgi>
+
+This allows the js to do checking while keeping the checks from
+being run in the cgi
+
+  {
+    field       => 'js_var',
+    required    => 1,
+    exclude_cgi => 1,
+  }
+
+=back
+
+=head1 MODIFYING VALIDATION TYPES
+
+=over 4
+
+=item C<do_not_trim>
+
+By default, validate will trim leading and trailing whitespace
+from submitted values.  Set do_not_trim to 1 to allow it to
+not trim.
+
+  {field => 'foo', do_not_trim => 1}
+
+=item C<replace>
+
+Pass a swap pattern to change the actual value of the form.
+Any perl regex can be passed.
+
+  {field => 'foo', replace => 's/(\d{3})(\d{3})(\d{3})/($1) $2-$3/'}
+
+=item C<default>
+
+Set item to default value if there is no existing value (undefined
+or zero length string).  Maybe someday well add default_if (but that
+would require some odd syntax for both the conditional and the default).
+
+  {field => 'country', default => 'EN'}
+
+=item C<to_upper_case> and C<to_lower_case>
+
+Do what they say they do.
+
+=item C<untaint>
+
+Requires that the validated field has been also checked with
+an enum, equals, match, compare, custom, or type check.  If the
+field has been checked and there are no errors - the field is "untainted."
+
+This is for use in conjunction with the -T switch.
+
+=back
+
+=head1 ERROR OBJECT
+
+Failed validation results in an error object blessed into the class found in
+$ERROR_PACKAGE - which defaults to CGI::Ex::Validate::Error.
+
+The error object has several methods for determining what the errors were.
+
+=over 4
+
+=item C<as_array>
+
+Returns an array or arrayref (depending on scalar context) of errors that
+occurred in the order that they occured.  Individual groups may have a heading
+and the entire validation will have a heading (the default heading can be changed
+via the 'as_array_title' general option).  Each error that occured is a separate
+item and are prepended with 'as_array_prefix' (which is a general option - default
+is '  ').  The as_array_ options may also be set via a hashref passed to as_array.
+as_array_title defaults to 'Please correct the following items:'.
+
+  ### if this returns the following
+  my $array = $err_obj->as_array;
+  # $array looks like
+  # ['Please correct the following items:', '  error1', '  error2']
+
+  ### then this would return the following
+  my $array = $err_obj->as_array({
+    as_array_prefix => '  - ',
+    as_array_title  => 'Something went wrong:',
+  });
+  # $array looks like
+  # ['Something went wrong:', '  - error1', '  - error2']
+
+=item C<as_string>
+
+Returns values of as_array joined with a newline.  This method is used as
+the stringification for the error object.  Values of as_array are joined with
+'as_string_join' which defaults to "\n".  If 'as_string_header' is set, it will
+be prepended onto the error string.  If 'as_string_footer' is set, it will be
+postpended onto the error string.
+
+  ### if this returns the following
+  my $string = $err_obj->as_string;
+  # $string looks like
+  # "Please correct the following items:\n  error1\n  error2"
+
+  ### then this would return the following
+  my $string = $err_obj->as_string({
+    as_array_prefix  => '  - ',
+    as_array_title   => 'Something went wrong:',
+    as_string_join   => '<br />',
+    as_string_header => '<span class="error">',
+    as_string_footer => '</span>',
+  });
+  # $string looks like
+  # '<span class="error">Something went wrong:<br />  - error1<br />  - error2</span>'
+
+=item C<as_hash>
+
+Returns a hash or hashref (depending on scalar context) of errors that
+occurred.   Each key is the field name of the form that failed validation with
+'as_hash_suffix' added on as a suffix.  as_hash_suffix is available as a general option
+and may also be passed in via a hashref as the only argument to as_hash.
+The default value is '_error'.  The values of the hash are arrayrefs of errors
+that occured to that form element.
+
+By default as_hash will return the values of the hash as arrayrefs (a list of the errors
+that occured to that key).  It is possible to also return the values as strings.
+Three options are available for formatting: 'as_hash_header' which will be prepended
+onto the error string, 'as_hash_footer' which will be postpended, and 'as_hash_join' which
+will be used to join the arrayref.  The only argument required to force the
+stringification is 'as_hash_join'.
+
+  ### if this returns the following
+  my $hash = $err_obj->as_hash;
+  # $hash looks like
+  # {key1_error => ['error1', 'error2']}
+
+  ### then this would return the following
+  my $hash = $err_obj->as_hash({
+    as_hash_suffix => '_foo',
+    as_hash_join   => '<br />',
+    as_hash_header => '<span class="error">'
+    as_hash_footer => '</span>'
+  });
+  # $hash looks like
+  # {key1_foo => '<span class="error">error1<br />error2</span>'}
+
+=back
+
+=head1 GROUP OPTIONS
+
+Any key in a validation hash matching the pattern m/^group\s+(\w+)$/
+is considered a group option.  The current know options are:
+
+=over 4
+
+=item C<'group title'>
+
+Used as a group section heading when as_array or as_string is called
+by the error object.
+
+=item C<'group order'>
+
+Order in which to validate key/value pairs of group.
+
+=item C<'group fields'>
+
+Arrayref of validation items to validate.
+
+=item C<'group validate_if'>
+
+Conditions that will be checked to see if the group should be validated.
+If no validate_if option is found, the group will be validated.
+
+=back
+
+=head1 GENERAL OPTIONS
+
+Any key in a validation hash matching the pattern m/^general\s+(\w+)$/
+is considered a general option.  General options will also be looked
+for in the Validate object ($self) and can be set when instantiating
+the object ($self->{raise_error} is equivalent to
+$valhash->{'general raise_error'}).  The current know options are:
+
+General options may be set in any group using the syntax:
+
+  'general general_option_name' => 'general_option_value'
+
+They will only be set if the group's validate_if is successful or
+if the group does not have a validate_if.  It is also possible to set
+a "group general" option using the following syntax:
+
+  'group general_option_name' => 'general_option_value'
+
+These items will only be set if the group fails validation.
+If a group has a validate_if block and passes validation, the group
+items will not be used.  This is so that a failed section can have
+its own settings.  Note though that the last option found will be
+used and that items set in $self override those set in the validation
+hash.
+
+Options may also be set globally before calling validate by
+populating the %DEFAULT_OPTIONS global hash.
+
+=over 4
+
+=item C<'general raise_error'>
+
+If raise_error is true, any call to validate that fails validation
+will die with an error object as the value.
+
+=item C<'general no_extra_fields'>
+
+If no_extra_fields is true, validate will add errors for any field found
+in form that does not have a field_val hashref in the validation hash.
+Default is false.  If no_extra_fields is set to 'used', it will check for
+any keys that were not in a group that was validated.
+
+An important exception to this is that field_val hashrefs or field names listed
+in a validate_if or required_if statement will not be included.  You must
+have an explicit entry for each key.
+
+=item C<'general \w+_error'>
+
+These items allow for an override of the default errors.
+
+  'general required_error' => '$name is really required',
+  'general max_len_error'  => '$name must be shorter than $value characters',
+    # OR #
+  my $self = CGI::Ex::Validate->new({
+    max_len_error => '$name must be shorter than $value characters',
+  });
+
+=item C<'general as_array_title'>
+
+Used as the section title for all errors that occur, when as_array
+or as_string is called by the error object.
+
+=item C<'general as_array_prefix'>
+
+Used as prefix to individual errors that occur, when as_array
+or as_string is called by the error object.  Each individual error
+will be prefixed with this string.  Headings will not be prefixed.
+Default is '  '.
+
+=item C<'general as_string_join'>
+
+When as_string is called, the values from as_array will be joined with
+as_string_join.  Default value is "\n".
+
+=item C<'general as_string_header'>
+
+If set, will be prepended onto the string when as_string is called.
+
+=item C<'general as_string_footer'>
+
+If set, will be prepended onto the string when as_string is called.
+
+=item C<'general as_hash_suffix'>
+
+Added on to key names during the call to as_hash.  Default is '_error'.
+
+=item C<'general as_hash_join'>
+
+By default, as_hash will return hashref values that are errors joined with
+the default as_hash_join value of <br />.  It can also return values that are
+arrayrefs of the errors.  This can be done by setting as_hash_join to a non-true value
+(for example '')
+
+=item C<'general as_hash_header'>
+
+If as_hash_join has been set to a true value, as_hash_header may be set to
+a string that will be prepended on to the error string.
+
+=item C<'general as_hash_footer'>
+
+If as_hash_join has been set to a true value, as_hash_footer may be set to
+a string that will be postpended on to the error string.
+
+=item C<'general no_inline'>
+
+If set to true, the javascript validation will not attempt to generate inline
+errors.  Default is true.  Inline errors are independent of confirm and alert
+errors.
+
+=item C<'general no_confirm'>
+
+If set to true, the javascript validation will try to use an alert instead
+of a confirm to inform the user of errors.  Alert and confirm are independent
+or inline errors.  Default is false.
+
+=item C<'general no_alert'>
+
+If set to true, the javascript validation will not show an alert box
+when errors occur.  Default is false.  This option only comes into
+play if no_confirm is also set.  This option is independent of inline
+errors.  Although it is possible to turn off all errors by setting
+no_inline, no_confirm, and no_alert all to 1, it is suggested that at
+least one of the error reporting facilities is left on.
+
+=back
+
+It is possible to have a group that contains nothing but general options.
+
+  my $val_hash = [
+    {'general error_title'    => 'The following things went wrong',
+     'general error_prefix'   => '  - ',
+     'general raise_error'    => 1,
+     'general name_suffix'    => '_foo_error',
+     'general required_error' => '$name is required',
+    },
+    {'group title' => 'User Information',
+     username => {required => 1},
+     email    => {required => 1},
+     password => {required => 1},
+    },
+  ];
+
+=head1 JAVASCRIPT
+
+CGI::Ex::Validate provides for having duplicate validation on the
+client side as on the server side.  Errors can be shown in any
+combination of inline and confirm, inline and alert, inline only,
+confirm only, alert only, and none.  These combinations are controlled
+by the general options no_inline, no_confirm, and no_alert.
+Javascript validation can be generated for a page using the
+C<-E<gt>generate_js> Method of CGI::Ex::Validate.  It is also possible
+to store the validation inline with the html.  This can be done by
+giving each of the elements to be validated an attribute called
+"validation", or by setting a global javascript variable called
+"document.validation" or "var validation".  An html file containing this
+validation will be read in using CGI::Ex::Conf::read_handler_html.
+
+All inline html validation must be written in yaml.
+
+It is anticipated that the html will contain something like either of the
+following examples:
+
+  <script src="/cgi-bin/js/CGI/Ex/yaml_load.js"></script>
+  <script src="/cgi-bin/js/CGI/Ex/validate.js"></script>
+  <script>
+  // \n\ allows all browsers to view this as a single string
+  document.validation = "\n\
+  general no_confirm: 1\n\
+  general no_alert: 1\n\
+  group order: [username, password]\n\
+  username:\n\
+    required: 1\n\
+    max_len: 20\n\
+  password:\n\
+    required: 1\n\
+    max_len: 30\n\
+  ";
+  if (document.check_form) document.check_form('my_form_name');
+  </script>
+
+Alternately we can use element attributes:
+
+  <form name="my_form_name">
+
+  Username: <input type=text size=20 name=username validation="
+    required: 1
+    max_len: 20
+  "><br>
+  <span class=error id=username_error>[% username_error %]</span><br>
+
+  Password: <input type=text size=20 name=password validation="
+    required: 1
+    max_len: 30
+  "><br>
+  <span class=error id=password_error>[% password_error %]</span><br>
+
+  <input type=submit>
+
+  </form>
+
+  <script src="/cgi-bin/js/CGI/Ex/yaml_load.js"></script>
+  <script src="/cgi-bin/js/CGI/Ex/validate.js"></script>
+  <script>
+  if (document.check_form) document.check_form('my_form_name');
+  </script>
+
+The read_handler_html from CGI::Ex::Conf will find either of these
+types of validation.
+
+If inline errors are asked for, each error that occurs will attempt
+to find an html element with its name as the id.  For example, if
+the field "username" failed validation and created a "username_error",
+the javascript would set the html of <span id="username_error"></span>
+to the error message.
+
+It is suggested to use something like the following so that you can
+have inline javascript validation as well as report validation errors
+from the server side as well.
+
+   <span class=error id=password_error>[% password_error %]</span><br>
+
+If the javascript fails for some reason, the form should still be able
+to submit as normal (fail gracefully).
+
+If the confirm option is used, the errors will be displayed to the user.
+If they choose OK they will be able to try and fix the errors.  If they
+choose cancel, the form will submit anyway and will rely on the server
+to do the validation.  This is for fail safety to make sure that if the
+javascript didn't validate correctly, the user can still submit the data.
+
+=head1 AUTHOR
+
+Paul Seamons
+
+=head1 LICENSE
+
+This module may be distributed under the same terms as Perl itself.
+
+=cut
+
+
diff --git a/lib/CGI/Ex/md5.js b/lib/CGI/Ex/md5.js
new file mode 100644 (file)
index 0000000..5e7a781
--- /dev/null
@@ -0,0 +1,271 @@
+/*
+ * Code taken directly from source listed below via the BSD license with one
+ * function added at bottom - Paul Seamons - March 2004
+ */
+
+
+/*
+ * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
+ * Digest Algorithm, as defined in RFC 1321.
+ * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002.
+ * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
+ * Distributed under the BSD License
+ * See http://pajhome.org.uk/crypt/md5 for more info.
+ */
+
+/*
+ * Configurable variables. You may need to tweak these to be compatible with
+ * the server-side, but the defaults work in most cases.
+ */
+var hexcase = 0;  /* hex output format. 0 - lowercase; 1 - uppercase        */
+var b64pad  = ""; /* base-64 pad character. "=" for strict RFC compliance   */
+var chrsz   = 8;  /* bits per input character. 8 - ASCII; 16 - Unicode      */
+
+/*
+ * These are the functions you'll usually want to call
+ * They take string arguments and return either hex or base-64 encoded strings
+ */
+function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));}
+function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));}
+function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));}
+function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); }
+function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); }
+function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); }
+
+/* 
+ * Perform a simple self-test to see if the VM is working 
+ */
+function md5_vm_test()
+{
+  return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72";
+}
+
+/*
+ * Calculate the MD5 of an array of little-endian words, and a bit length
+ */
+function core_md5(x, len)
+{
+  /* append padding */
+  x[len >> 5] |= 0x80 << ((len) % 32);
+  x[(((len + 64) >>> 9) << 4) + 14] = len;
+  
+  var a =  1732584193;
+  var b = -271733879;
+  var c = -1732584194;
+  var d =  271733878;
+
+  for(var i = 0; i < x.length; i += 16)
+  {
+    var olda = a;
+    var oldb = b;
+    var oldc = c;
+    var oldd = d;
+    a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
+    d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
+    c = md5_ff(c, d, a, b, x[i+ 2], 17,  606105819);
+    b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
+    a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
+    d = md5_ff(d, a, b, c, x[i+ 5], 12,  1200080426);
+    c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
+    b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
+    a = md5_ff(a, b, c, d, x[i+ 8], 7 ,  1770035416);
+    d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
+    c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
+    b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
+    a = md5_ff(a, b, c, d, x[i+12], 7 ,  1804603682);
+    d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
+    c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
+    b = md5_ff(b, c, d, a, x[i+15], 22,  1236535329);
+
+    a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
+    d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
+    c = md5_gg(c, d, a, b, x[i+11], 14,  643717713);
+    b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
+    a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
+    d = md5_gg(d, a, b, c, x[i+10], 9 ,  38016083);
+    c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
+    b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
+    a = md5_gg(a, b, c, d, x[i+ 9], 5 ,  568446438);
+    d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
+    c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
+    b = md5_gg(b, c, d, a, x[i+ 8], 20,  1163531501);
+    a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
+    d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
+    c = md5_gg(c, d, a, b, x[i+ 7], 14,  1735328473);
+    b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);
+
+    a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
+    d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
+    c = md5_hh(c, d, a, b, x[i+11], 16,  1839030562);
+    b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
+    a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
+    d = md5_hh(d, a, b, c, x[i+ 4], 11,  1272893353);
+    c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
+    b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
+    a = md5_hh(a, b, c, d, x[i+13], 4 ,  681279174);
+    d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
+    c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
+    b = md5_hh(b, c, d, a, x[i+ 6], 23,  76029189);
+    a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
+    d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
+    c = md5_hh(c, d, a, b, x[i+15], 16,  530742520);
+    b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);
+
+    a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
+    d = md5_ii(d, a, b, c, x[i+ 7], 10,  1126891415);
+    c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
+    b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
+    a = md5_ii(a, b, c, d, x[i+12], 6 ,  1700485571);
+    d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
+    c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
+    b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
+    a = md5_ii(a, b, c, d, x[i+ 8], 6 ,  1873313359);
+    d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
+    c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
+    b = md5_ii(b, c, d, a, x[i+13], 21,  1309151649);
+    a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
+    d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
+    c = md5_ii(c, d, a, b, x[i+ 2], 15,  718787259);
+    b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);
+
+    a = safe_add(a, olda);
+    b = safe_add(b, oldb);
+    c = safe_add(c, oldc);
+    d = safe_add(d, oldd);
+  }
+  return Array(a, b, c, d);
+  
+}
+
+/*
+ * These functions implement the four basic operations the algorithm uses.
+ */
+function md5_cmn(q, a, b, x, s, t)
+{
+  return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
+}
+function md5_ff(a, b, c, d, x, s, t)
+{
+  return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
+}
+function md5_gg(a, b, c, d, x, s, t)
+{
+  return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
+}
+function md5_hh(a, b, c, d, x, s, t)
+{
+  return md5_cmn(b ^ c ^ d, a, b, x, s, t);
+}
+function md5_ii(a, b, c, d, x, s, t)
+{
+  return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
+}
+
+/*
+ * Calculate the HMAC-MD5, of a key and some data
+ */
+function core_hmac_md5(key, data)
+{
+  var bkey = str2binl(key);
+  if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz);
+
+  var ipad = Array(16), opad = Array(16);
+  for(var i = 0; i < 16; i++) 
+  {
+    ipad[i] = bkey[i] ^ 0x36363636;
+    opad[i] = bkey[i] ^ 0x5C5C5C5C;
+  }
+
+  var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz);
+  return core_md5(opad.concat(hash), 512 + 128);
+}
+
+/*
+ * Add integers, wrapping at 2^32. This uses 16-bit operations internally
+ * to work around bugs in some JS interpreters.
+ */
+function safe_add(x, y)
+{
+  var lsw = (x & 0xFFFF) + (y & 0xFFFF);
+  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
+  return (msw << 16) | (lsw & 0xFFFF);
+}
+
+/*
+ * Bitwise rotate a 32-bit number to the left.
+ */
+function bit_rol(num, cnt)
+{
+  return (num << cnt) | (num >>> (32 - cnt));
+}
+
+/*
+ * Convert a string to an array of little-endian words
+ * If chrsz is ASCII, characters >255 have their hi-byte silently ignored.
+ */
+function str2binl(str)
+{
+  var bin = Array();
+  var mask = (1 << chrsz) - 1;
+  for(var i = 0; i < str.length * chrsz; i += chrsz)
+    bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32);
+  return bin;
+}
+
+/*
+ * Convert an array of little-endian words to a string
+ */
+function binl2str(bin)
+{
+  var str = "";
+  var mask = (1 << chrsz) - 1;
+  for(var i = 0; i < bin.length * 32; i += chrsz)
+    str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask);
+  return str;
+}
+
+/*
+ * Convert an array of little-endian words to a hex string.
+ */
+function binl2hex(binarray)
+{
+  var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
+  var str = "";
+  for(var i = 0; i < binarray.length * 4; i++)
+  {
+    str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
+           hex_tab.charAt((binarray[i>>2] >> ((i%4)*8  )) & 0xF);
+  }
+  return str;
+}
+
+/*
+ * Convert an array of little-endian words to a base-64 string
+ */
+function binl2b64(binarray)
+{
+  var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+  var str = "";
+  for(var i = 0; i < binarray.length * 4; i += 3)
+  {
+    var triplet = (((binarray[i   >> 2] >> 8 * ( i   %4)) & 0xFF) << 16)
+                | (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 )
+                |  ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF);
+    for(var j = 0; j < 4; j++)
+    {
+      if(i * 8 + j * 6 > binarray.length * 32) str += b64pad;
+      else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);
+    }
+  }
+  return str;
+}
+
+/* simple sub added so we can test for existance */
+document.md5_hex = function (s) {
+  return hex_md5(s);
+}
+
+document.md5_is_functional = function () {
+  return md5_vm_test();
+}
diff --git a/lib/CGI/Ex/sha1.js b/lib/CGI/Ex/sha1.js
new file mode 100644 (file)
index 0000000..ada3825
--- /dev/null
@@ -0,0 +1,217 @@
+/*
+ * Code taken directly from source listed below via the BSD license with two
+ * function added at bottom - Paul Seamons - March 2004
+ */
+
+
+/*
+ * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined
+ * in FIPS PUB 180-1
+ * Version 2.1 Copyright Paul Johnston 2000 - 2002.
+ * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
+ * Distributed under the BSD License
+ * See http://pajhome.org.uk/crypt/md5 for details.
+ */
+
+/*
+ * Configurable variables. You may need to tweak these to be compatible with
+ * the server-side, but the defaults work in most cases.
+ */
+var hexcase = 0;  /* hex output format. 0 - lowercase; 1 - uppercase        */
+var b64pad  = ""; /* base-64 pad character. "=" for strict RFC compliance   */
+var chrsz   = 8;  /* bits per input character. 8 - ASCII; 16 - Unicode      */
+
+/*
+ * These are the functions you'll usually want to call
+ * They take string arguments and return either hex or base-64 encoded strings
+ */
+function hex_sha1(s){return binb2hex(core_sha1(str2binb(s),s.length * chrsz));}
+function b64_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length * chrsz));}
+function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length * chrsz));}
+function hex_hmac_sha1(key, data){ return binb2hex(core_hmac_sha1(key, data));}
+function b64_hmac_sha1(key, data){ return binb2b64(core_hmac_sha1(key, data));}
+function str_hmac_sha1(key, data){ return binb2str(core_hmac_sha1(key, data));}
+
+/*
+ * Perform a simple self-test to see if the VM is working
+ */
+function sha1_vm_test()
+{
+  return hex_sha1("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d";
+}
+
+/*
+ * Calculate the SHA-1 of an array of big-endian words, and a bit length
+ */
+function core_sha1(x, len)
+{
+  /* append padding */
+  x[len >> 5] |= 0x80 << (24 - len % 32);
+  x[((len + 64 >> 9) << 4) + 15] = len;
+
+  var w = Array(80);
+  var a =  1732584193;
+  var b = -271733879;
+  var c = -1732584194;
+  var d =  271733878;
+  var e = -1009589776;
+
+  for(var i = 0; i < x.length; i += 16)
+  {
+    var olda = a;
+    var oldb = b;
+    var oldc = c;
+    var oldd = d;
+    var olde = e;
+
+    for(var j = 0; j < 80; j++)
+    {
+      if(j < 16) w[j] = x[i + j];
+      else w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1);
+      var t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)), 
+                       safe_add(safe_add(e, w[j]), sha1_kt(j)));
+      e = d;
+      d = c;
+      c = rol(b, 30);
+      b = a;
+      a = t;
+    }
+
+    a = safe_add(a, olda);
+    b = safe_add(b, oldb);
+    c = safe_add(c, oldc);
+    d = safe_add(d, oldd);
+    e = safe_add(e, olde);
+  }
+  return Array(a, b, c, d, e);
+  
+}
+
+/*
+ * Perform the appropriate triplet combination function for the current
+ * iteration
+ */
+function sha1_ft(t, b, c, d)
+{
+  if(t < 20) return (b & c) | ((~b) & d);
+  if(t < 40) return b ^ c ^ d;
+  if(t < 60) return (b & c) | (b & d) | (c & d);
+  return b ^ c ^ d;
+}
+
+/*
+ * Determine the appropriate additive constant for the current iteration
+ */
+function sha1_kt(t)
+{
+  return (t < 20) ?  1518500249 : (t < 40) ?  1859775393 :
+         (t < 60) ? -1894007588 : -899497514;
+}  
+
+/*
+ * Calculate the HMAC-SHA1 of a key and some data
+ */
+function core_hmac_sha1(key, data)
+{
+  var bkey = str2binb(key);
+  if(bkey.length > 16) bkey = core_sha1(bkey, key.length * chrsz);
+
+  var ipad = Array(16), opad = Array(16);
+  for(var i = 0; i < 16; i++) 
+  {
+    ipad[i] = bkey[i] ^ 0x36363636;
+    opad[i] = bkey[i] ^ 0x5C5C5C5C;
+  }
+
+  var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * chrsz);
+  return core_sha1(opad.concat(hash), 512 + 160);
+}
+
+/*
+ * Add integers, wrapping at 2^32. This uses 16-bit operations internally
+ * to work around bugs in some JS interpreters.
+ */
+function safe_add(x, y)
+{
+  var lsw = (x & 0xFFFF) + (y & 0xFFFF);
+  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
+  return (msw << 16) | (lsw & 0xFFFF);
+}
+
+/*
+ * Bitwise rotate a 32-bit number to the left.
+ */
+function rol(num, cnt)
+{
+  return (num << cnt) | (num >>> (32 - cnt));
+}
+
+/*
+ * Convert an 8-bit or 16-bit string to an array of big-endian words
+ * In 8-bit function, characters >255 have their hi-byte silently ignored.
+ */
+function str2binb(str)
+{
+  var bin = Array();
+  var mask = (1 << chrsz) - 1;
+  for(var i = 0; i < str.length * chrsz; i += chrsz)
+    bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i%32);
+  return bin;
+}
+
+/*
+ * Convert an array of big-endian words to a string
+ */
+function binb2str(bin)
+{
+  var str = "";
+  var mask = (1 << chrsz) - 1;
+  for(var i = 0; i < bin.length * 32; i += chrsz)
+    str += String.fromCharCode((bin[i>>5] >>> (24 - i%32)) & mask);
+  return str;
+}
+
+/*
+ * Convert an array of big-endian words to a hex string.
+ */
+function binb2hex(binarray)
+{
+  var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
+  var str = "";
+  for(var i = 0; i < binarray.length * 4; i++)
+  {
+    str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) +
+           hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8  )) & 0xF);
+  }
+  return str;
+}
+
+/*
+ * Convert an array of big-endian words to a base-64 string
+ */
+function binb2b64(binarray)
+{
+  var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+  var str = "";
+  for(var i = 0; i < binarray.length * 4; i += 3)
+  {
+    var triplet = (((binarray[i   >> 2] >> 8 * (3 -  i   %4)) & 0xFF) << 16)
+                | (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 )
+                |  ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF);
+    for(var j = 0; j < 4; j++)
+    {
+      if(i * 8 + j * 6 > binarray.length * 32) str += b64pad;
+      else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);
+    }
+  }
+  return str;
+}
+
+/* simple sub added so we can test for existance */
+document.sha1_hex = function (s) {
+  return hex_sha1(s);
+}
+
+document.sha1_is_functional = function () {
+  return sha1_vm_test();
+}
diff --git a/lib/CGI/Ex/validate.js b/lib/CGI/Ex/validate.js
new file mode 100644 (file)
index 0000000..8d4c296
--- /dev/null
@@ -0,0 +1,1041 @@
+/**----------------------------------------------------------------***
+*  Copyright 2004 - Paul Seamons                                     *
+*  Distributed under the Perl Artistic License without warranty      *
+*  Based upon CGI/Ex/Validate.pm v1.14 from Perl                     *
+*  For instructions on usage, see perldoc of CGI::Ex::Validate       *
+***----------------------------------------------------------------**/
+// $Revision: 1.31 $
+
+function Validate () {
+ this.error             = vob_error;
+ this.validate          = vob_validate;
+ this.check_conditional = vob_check_conditional;
+ this.filter_types      = vob_filter_types;
+ this.add_error         = vob_add_error;
+ this.validate_buddy    = vob_validate_buddy;
+ this.check_type        = vob_check_type;
+ this.get_form_value    = vob_get_form_value;
+}
+
+function ValidateError (errors, extra) {
+ this.errors = errors;
+ this.extra  = extra;
+
+ this.as_string = eob_as_string;
+ this.as_array  = eob_as_array;
+ this.as_hash   = eob_as_hash;
+ this.get_error_text = eob_get_error_text;
+ this.first_field    = eob_first_field;
+}
+
+///----------------------------------------------------------------///
+
+function vob_error (err) {
+ alert (err);
+}
+
+function vob_validate (form, val_hash) {
+ if (typeof(val_hash) == 'string') {
+   if (! document.yaml_load)
+     return this.error("Cannot parse yaml string - document.yaml_load is not loaded");
+   val_hash = document.yaml_load(val_hash);
+ }
+
+ var ERRORS = new Array ();
+ var EXTRA  = new Array ();
+ //  var USED_GROUPS = new Array();
+
+ // distinguishing between associative and index based arrays is harder than in perl
+ if (! val_hash.length) val_hash = new Array(val_hash);
+ for (var i = 0; i < val_hash.length; i ++) {
+   var group_val = val_hash[i];
+   if (typeof(group_val) != 'object' || group_val.length) return this.error("Validation groups must be a hash");
+   var title       = group_val['group title'];
+   var validate_if = group_val['group validate_if'];
+
+   if (validate_if && ! this.check_conditional(form, validate_if)) continue;
+   //    USED_GROUPS.push(group_val);
+
+   /// if the validation items were not passed as an arrayref
+   /// look for a group order and then fail back to the keys of the group
+   var fields = group_val['group fields'];
+   var order  = new Array();
+   for (var key in group_val) order[order.length] = key;
+   order = order.sort();
+   if (fields) {
+     if (typeof(fields) != 'object' || ! fields.length)
+       return this.error("'group fields' must be a non-empty array");
+   } else {
+     fields = new Array();
+     var _order = (group_val['group order']) ? group_val['group order'] : order;
+     if (typeof(_order) != 'object' || ! _order.length)
+       return this.error("'group order' must be a non-empty array");
+     for (var j = 0; j < _order.length; j ++) {
+       var field = _order[j];
+       if (field.match('^(group|general)\\s')) continue;
+       var field_val = group_val[field];
+       if (! field_val) {
+         if (field == 'OR') field_val = 'OR';
+         else return this.error('No element found in group for '+field);
+       }
+       if (typeof(field_val) == 'object' && ! field_val['field']) field_val['field'] = field;
+       fields[fields.length] = field_val;
+     }
+   }
+
+   /// check which fields have been used
+   var found = new Array();
+   for (var j = 0; j < fields.length; j ++) {
+     var field_val = fields[j];
+     var field = field_val['field'];
+     if (! field) return this.error("Missing field key in validation");
+     // if (found[field]) return this.error('Duplicate order found for '+field+' in group order or fields');
+     found[field] = 1;
+   }
+
+   /// add any remaining fields from the order
+   for (var j = 0; j < order.length; j ++) {
+     var field = order[j];
+     if (found[field] || field.match('^(group|general)\\s')) continue;
+     var field_val = group_val[field];
+     if (typeof(field_val) != 'object' || field_val.length) return this.error('Found a non-hash value on field '+field);
+     if (! field_val['field']) field_val['field'] = field;
+     fields[fields.length] = field_val;
+   }
+
+   /// now lets do the validation
+   var is_found  = 1;
+   var errors = new Array();
+   var hold_error;
+
+   for (var j = 0; j < fields.length; j ++) {
+     var ref = fields[j];
+     if (typeof(ref) != 'object' && ref == 'OR') {
+       if (is_found) j ++;
+       is_found = 1;
+       continue;
+     }
+     is_found = 1;
+     if (! ref['field']) return this.error("Missing field key during normal validation");
+     var err = this.validate_buddy(form, ref['field'], ref);
+
+     /// test the error - if errors occur allow for OR - if OR fails use errors from first fail
+     if (err.length) {
+       if (j <= fields.length && typeof(fields[j + 1] != 'object') && fields[j + 1] == 'OR') {
+         hold_error = err;
+       } else {
+         if (hold_error) err = hold_error;
+         for (var k = 0; k < err.length; k ++) errors[errors.length] = err[k];
+         hold_error = '';
+       }
+     } else {
+       hold_error = '';
+     }
+   }
+
+   /// add on errors as requested
+   if (errors.length) {
+     if (title) ERRORS[ERRORS.length] = title;
+     for (var j = 0; j < errors.length; j ++) ERRORS[ERRORS.length] = errors[j];
+   }
+
+   /// add on general options, and group options if errors in group occurred
+   var m;
+   for (var j = 0; j < order.length; j ++) {
+     var field = order[j];
+     if (! (m = field.match('^(general|group)\\s+(\\w+)$'))) continue;
+     if (m[1] == 'group' && (errors.length == 0 || m[2].match('^(field|order|title)$'))) continue;
+       EXTRA[m[2]] = group_val[field];
+   }
+ }
+
+ /// store any extra items from self
+ for (var key in this) {
+   if (! key.match('_error$')
+       && ! key.match('^(raise_error|as_hash_\\w+|as_array_\\w+|as_string_\\w+)$')) continue;
+   EXTRA[key] = this[key];
+ }
+
+ /// allow for checking for unused keys
+ // if (EXTRA['no_extra_fields'])
+ // won't do anything about this for now - let the server handle it
+
+ /// return what they want
+ if (ERRORS.length) return new ValidateError(ERRORS, EXTRA);
+ return;
+}
+
+
+/// allow for optional validation on groups and on individual items
+function vob_check_conditional (form, ifs, N_level, ifs_match) {
+
+ if (! N_level) N_level = 0;
+ N_level ++;
+
+ /// can pass a single hash - or an array ref of hashes
+ if (! ifs) {
+   return this.error("Need reference passed to check_conditional");
+ } else if (typeof(ifs) != 'object') {
+   ifs = new Array(ifs);
+ } else if (! ifs.length) { // turn hash into array of hash
+   ifs = new Array(ifs);
+ }
+
+ /// run the if options here
+ /// multiple items can be passed - all are required unless OR is used to separate
+ var is_found = 1;
+ var m;
+ for (var i = 0; i < ifs.length; i ++) {
+   var ref = ifs[i];
+   if (typeof(ref) != 'object') {
+     if (ref == 'OR') {
+       if (is_found) i++;
+       is_found = 1;
+       continue;
+     } else {
+       var field = ref;
+       ref = new Array();
+       if (m = field.match('^(\\s*!\\s*)')) {
+         field = field.substring(m[1].length);
+         ref['max_in_set'] = '0 of ' + field;
+       } else {
+         ref['required'] = 1;
+       }
+       ref['field'] = field;
+     }
+   }
+   if (! is_found) break;
+
+   /// get the field - allow for custom variables based upon a match
+   var field = ref['field'];
+   if (! field) return this.error("Missing field key during validate_if");
+   field = field.replace(new RegExp('\\$(\\d+)','g'), function (all, N) {
+     if (typeof(ifs_match) != 'object'
+         || typeof(ifs_match[N]) == 'undefined') return ''
+     return ifs_match[N];
+   });
+
+   var err = this.validate_buddy(form, field, ref, N_level);
+   if (err.length) is_found = 0;
+ }
+ return is_found;
+}
+
+function vob_filter_types (type, types) {
+ var values = new Array();
+ var regexp = new RegExp('^'+type+'_?\\d*$');
+ for (var i = 0; i < types.length; i++)
+   if (types[i].match(regexp)) values[values.length] = types[i];
+ return values;
+}
+
+function vob_add_error (errors,field,type,field_val,ifs_match) {
+ errors[errors.length] = new Array(field, type, field_val, ifs_match);
+}
+
+/// this is where the main checking goes on
+function vob_validate_buddy (form, field, field_val, N_level, ifs_match) {
+ if (! N_level) N_level = 0;
+ if (++ N_level > 10) return this.error("Max dependency level reached " + N_level);
+ if (! form.elements) return;
+
+ var errors = new Array();
+ var types  = new Array();
+ for (var key in field_val) types[types.length] = key;
+ types = types.sort();
+
+ /// allow for not running some tests in the cgi
+ if (this.filter_types('exclude_js', types).length) return errors;
+
+ /// allow for field names that contain regular expressions
+ var m;
+ if (m = field.match('^(!\\s*|)m([^\\s\\w])(.*)\\2([eigsmx]*)$')) {
+   var not = m[1];
+   var pat = m[3];
+   var opt = m[4];
+   if (opt.indexOf('e') != -1) return this.error("The e option cannot be used on field "+field);
+   opt = opt.replace(new RegExp('[sg]','g'),'');
+   var reg = new RegExp(pat, opt);
+
+   var keys = new Array();
+   for (var i = 0; i < form.elements.length; i ++) {
+     var _field = form.elements[i].name;
+     if (! _field) continue;
+     if ( (not && ! (m = _field.match(reg))) || (m = _field.match(reg))) {
+       var err = this.validate_buddy(form, _field, field_val, N_level, m);
+       for (var j = 0; j < err.length; j ++) errors[errors.length] = err[j];
+     }
+   }
+   return errors;
+ }
+
+ var _value = this.get_form_value(form[field]);
+ var values;
+ if (typeof(_value) == 'object') {
+   values = _value;
+ } else {
+   values = new Array();
+   values[values.length] = _value;
+ }
+ var n_values = (typeof(_value) == 'undefined') ? 0 : values.length;
+
+ /// allow for default value
+ var tests = this.filter_types('default', types);
+ if (n_values == 0 || (n_values == 1 && values[0].length == 0)) {
+   for (var i = 0; i < tests.length; i ++) {
+     var el = form[field];
+     var type = el.type;
+     if (type && (type == 'hidden' || type == 'password' || type == 'text' || type == 'textarea' || type == 'submit')) el.value = values[0] = field_val[tests[i]];
+   }
+ }
+
+ /// allow for a few form modifiers
+ var modified = 0;
+ for (var i = 0; i < values.length; i ++) {
+   if (typeof(values[i]) == 'undefined') continue;
+   if (! this.filter_types('do_not_trim',types).length)
+     values[i] = values[i].replace('^\\s+','').replace(new RegExp('\\s+$',''),'');
+   if (this.filter_types('to_upper_case',types).length) {
+     values[i] = values[i].toUpperCase();
+   } else if (this.filter_types('to_lower_case',types).length) {
+     values[i] = values[i].toLowerCase();
+   }
+ }
+ var tests = this.filter_types('replace', types);
+ for (var i = 0; i < tests.length; i ++) {
+   var ref = field_val[tests[i]];
+   ref = (typeof(ref) == 'object') ? ref : ref.split(new RegExp('\\s*\\|\\|\\s*'));
+   for (var j = 0; j < ref.length; j ++) {
+     if (! (m = ref[j].match('^\\s*s([^\\s\\w])(.+)\\1(.*)\\1([eigmx]*)$')))
+       return this.error("Not sure how to parse that replace "+ref[j]);
+     var pat  = m[2];
+     var swap = m[3];
+     var opt  = m[4];
+     if (opt.indexOf('e') != -1)
+       return this.error("The e option cannot be used on field "+field+", replace "+tests[i]);
+     var regexp = new RegExp(pat, opt);
+     for (var k = 0; k < values.length; k ++) {
+       if (values[k].match(regexp)) modified = 1;
+       values[k] = values[k].replace(regexp,swap);
+     }
+   }
+ }
+ if (modified && n_values == 1) {
+   var el = form[field];
+   var type = el.type;
+   if (! type) return '';
+   if (type == 'hidden' || type == 'password' || type == 'text' || type == 'textarea' || type == 'submit')
+     el.value = values[0];
+ }
+
+ /// only continue if a validate_if is not present or passes test
+ var needs_val = 0;
+ var n_vif = 0;
+ var tests = this.filter_types('validate_if', types);
+ for (var i = 0; i < tests.length; i ++) {
+   n_vif ++;
+   var ifs = field_val[tests[i]];
+   var ret = this.check_conditional(form, ifs, N_level, ifs_match);
+   if (ret) needs_val ++;
+ }
+ if (! needs_val && n_vif) return errors;
+
+ /// check for simple existence
+ /// optionally check only if another condition is met
+ var is_required = '';
+ var tests = this.filter_types('required', types);
+ for (var i = 0; i < tests.length; i ++) {
+   if (! field_val[tests[i]] || field_val[tests[i]] == 0) continue;
+   is_required = tests[i];
+   break;
+ }
+ if (! is_required) {
+   var tests = this.filter_types('required_if', types);
+   for (var i = 0; i < tests.length; i ++) {
+     var ifs = field_val[tests[i]];
+     if (! this.check_conditional(form, ifs, N_level, ifs_match)) continue;
+     is_required = tests[i];
+     break;
+   }
+ }
+ if (is_required && (typeof(_value) == 'undefined'
+                     || ((typeof(_value) == 'object' && _value.length == 0)
+                         || ! _value.length))) {
+   this.add_error(errors, field, is_required, field_val, ifs_match);
+   return errors;
+ }
+
+ /// min values check
+ var tests = this.filter_types('min_values', types);
+ for (var i = 0; i < tests.length; i ++) {
+   var n = field_val[tests[i]];
+   if (n_values < n) {
+     this.add_error(errors, field, tests[i], field_val, ifs_match);
+     return errors;
+   }
+ }
+
+ /// max values check
+ var tests = this.filter_types('max_values', types);
+ if (! tests.length) {
+   tests[tests.length] = 'max_values';
+   field_val['max_values'] = 1;
+ }
+ for (var i = 0; i < tests.length; i ++) {
+   var n = field_val[tests[i]];
+   if (n_values > n) {
+     this.add_error(errors, field, tests[i], field_val, ifs_match);
+     return errors;
+   }
+ }
+
+ /// min_in_set and max_in_set check
+ for (var h = 0; h < 2 ; h++) {
+   var minmax = (h == 0) ? 'min' : 'max';
+   var tests = this.filter_types(minmax+'_in_set', types);
+   for (var i = 0; i < tests.length; i ++) {
+     if (! (m = field_val[tests[i]].match('^\\s*(\\d+)(?:\\s*[oO][fF])?\\s+(.+)\\s*$')))
+       return this.error("Invalid in_set check "+field_val[tests[i]]);
+     var n       = m[1];
+     var _fields = m[2].split(new RegExp('[\\s,]+'));
+     for (var k = 0; k < _fields.length; k ++) {
+       var _value = this.get_form_value(form[_fields[k]]);
+       var _values;
+       if (typeof(_value) == 'undefined') continue;
+       if (typeof(_value) == 'object') {
+         _values = _value;
+       } else {
+         _values = new Array();
+         _values[_values.length] = _value;
+       }
+       for (var l = 0; l < _values.length; l ++) {
+         var _value = _values[l];
+         if (typeof(_value) != 'undefined' && _value.length) n --;
+       }
+     }
+     if (   (minmax == 'min' && n > 0)
+         || (minmax == 'max' && n < 0)) {
+       this.add_error(errors, field, tests[i], field_val, ifs_match);
+       return errors;
+     }
+   }
+ }
+
+ // the remaining tests operate on each value of a field
+ for (var n = 0; n < values.length; n ++) {
+   var value = values[n];
+
+   /// allow for enum types
+   var tests = this.filter_types('enum', types);
+   for (var i = 0; i < tests.length; i ++) {
+     var hold  = field_val[tests[i]];
+     var _enum = (typeof(hold) == 'object') ? hold : hold.split(new RegExp('\\s*\\|\\|\\s*'));
+     var is_found = 0;
+     for (var j = 0; j < _enum.length; j ++) {
+       if (value != _enum[j]) continue;
+       is_found = 1;
+       break;
+     }
+     if (! is_found) this.add_error(errors, field, tests[i], field_val, ifs_match);
+   }
+
+   /// field equality test
+   var tests = this.filter_types('equals', types);
+   for (var i = 0; i < tests.length; i ++) {
+     var field2  = field_val[tests[i]];
+     var not = field2.match('^!\\s*');
+     if (not) field2 = field2.substring(not[0].length);
+     var success = 0;
+     if (m = field2.match('^(["\'])(.*)\\1$')) {
+       if (value == m[2]) success = 1;
+     } else {
+       var value2 = this.get_form_value(form[field2]);
+       if (typeof(value2) == 'undefined') value2 = '';
+       if (value == value2) success = 1;
+     }
+     if (not && success || ! not && ! success)
+       this.add_error(errors, field, tests[i], field_val, ifs_match);
+   }
+
+   /// length min check
+   var tests = this.filter_types('min_len', types);
+   for (var i = 0; i < tests.length; i ++) {
+     var n = field_val[tests[i]];
+     if (value.length < n) this.add_error(errors, field, tests[i], field_val, ifs_match);
+   }
+
+   /// length max check
+   var tests = this.filter_types('max_len', types);
+   for (var i = 0; i < tests.length; i ++) {
+     var n = field_val[tests[i]];
+     if (value.length > n) this.add_error(errors, field, tests[i], field_val, ifs_match);
+   }
+
+   /// now do match types
+   var tests = this.filter_types('match', types);
+   for (var i = 0; i < tests.length; i ++) {
+     var ref = field_val[tests[i]];
+     ref = (typeof(ref) == 'object') ? ref
+       : (typeof(ref) == 'function') ? new Array(ref)
+       : ref.split(new RegExp('\\s*\\|\\|\\s*'));
+     for (var j = 0; j < ref.length; j ++) {
+       if (typeof(ref[j]) == 'function') {
+         if (! value.match(ref[j])) this.add_error(errors, field, tests[i], field_val, ifs_match);
+       } else {
+         if (! (m = ref[j].match('^\\s*(!\\s*|)m([^\\s\\w])(.*)\\2([eigsmx]*)\\s*$')))
+           return this.error("Not sure how to parse that match ("+ref[j]+")");
+         var not = m[1];
+         var pat = m[3];
+         var opt = m[4];
+         if (opt.indexOf('e') != -1)
+           return this.error("The e option cannot be used on field "+field+", test "+tests[i]);
+         opt = opt.replace(new RegExp('[sg]','g'),'');
+         var regexp = new RegExp(pat, opt);
+         if (   (  not &&   value.match(regexp))
+             || (! not && ! value.match(regexp))) {
+           this.add_error(errors, field, tests[i], field_val, ifs_match);
+         }
+       }
+     }
+   }
+
+   /// allow for comparison checks
+   var tests = this.filter_types('compare', types);
+   for (var i = 0; i < tests.length; i ++) {
+     var ref = field_val[tests[i]];
+     ref = (typeof(ref) == 'object') ? ref : ref.split(new RegExp('\\s*\\|\\|\\s*'));
+     for (var j = 0; j < ref.length; j ++) {
+       var comp = ref[j];
+       if (! comp) continue;
+       var hold = false;
+       var copy = value;
+       if (m = comp.match('^\\s*(>|<|[><!=]=)\\s*([\\d\.\-]+)\\s*$')) {
+         if (! copy) copy = 0;
+         copy *= 1;
+         if      (m[1] == '>' ) hold = (copy >  m[2])
+         else if (m[1] == '<' ) hold = (copy <  m[2])
+         else if (m[1] == '>=') hold = (copy >= m[2])
+         else if (m[1] == '<=') hold = (copy <= m[2])
+         else if (m[1] == '!=') hold = (copy != m[2])
+         else if (m[1] == '==') hold = (copy == m[2])
+       } else if (m = comp.match('^\\s*(eq|ne|gt|ge|lt|le)\\s+(.+?)\\s*$')) {
+         m[2] = m[2].replace('^(["\'])(.*)\\1$','$1');
+         if      (m[1] == 'gt') hold = (copy >  m[2])
+         else if (m[1] == 'lt') hold = (copy <  m[2])
+         else if (m[1] == 'ge') hold = (copy >= m[2])
+         else if (m[1] == 'le') hold = (copy <= m[2])
+         else if (m[1] == 'ne') hold = (copy != m[2])
+         else if (m[1] == 'eq') hold = (copy == m[2])
+       } else {
+         return this.error("Not sure how to compare \""+comp+"\"");
+       }
+       if (! hold) this.add_error(errors, field, tests[i], field_val, ifs_match);
+     }
+   }
+
+   /// do specific type checks
+   var tests = this.filter_types('type',types);
+   for (var i = 0; i < tests.length; i ++)
+     if (! this.check_type(value, field_val[tests[i]], field, form))
+       this.add_error(errors, field, tests[i], field_val, ifs_match);
+
+   /// do custom_js type checks
+   // this will allow for a custom piece of javascript
+   // 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
+   var tests = this.filter_types('custom_js',types);
+   for (var i = 0; i < tests.length; i ++)
+     if (! eval(field_val[tests[i]]))
+       this.add_error(errors, field, tests[i], field_val, ifs_match);
+ }
+
+ /// all done - time to return
+ return errors;
+}
+
+/// used to validate specific types
+function vob_check_type (value, type, field, form) {
+ var m;
+
+ /// do valid email address for our system
+ if (type == 'EMAIL') {
+   if (! value) return 0;
+   if (! (m = value.match('^(.+)\@(.+?)$'))) return 0;
+   if (m[1].length > 60)  return 0;
+   if (m[2].length > 100) return 0;
+   if (! this.check_type(m[2],'DOMAIN') && ! this.check_type(m[2],'IP')) return 0;
+   if (! this.check_type(m[1],'LOCAL_PART')) return 0;
+
+ /// the "username" portion of an email address
+ } else if (type == 'LOCAL_PART') {
+   if (typeof(value) == 'undefined' || ! value.length) return 0;
+   if (! value.match('[^a-z0-9.\\-!&]')) return 0;
+   if (! value.match('^[.\\-]'))         return 0;
+   if (! value.match('[.\\-&]$'))        return 0;
+   if (! value.match('(\\.-|-\\.|\\.\\.)')) return 0;
+
+ /// standard IP address
+ } else if (type == 'IP') {
+   if (! value) return 0;
+   var dig = value.split(new RegExp('\\.'));
+   if (dig.length != 4) return 0;
+   for (var i = 0; i < 4; i ++)
+     if (typeof(dig[i]) == 'undefined' || dig[i].match('\\D') || dig[i] > 255) return 0;
+
+ /// domain name - including tld and subdomains (which are all domains)
+ } else if (type == 'DOMAIN') {
+   if (! value) return 0;
+   if (! value.match('^[a-z0-9.-]{4,255}$')) return 0;
+   if (value.match('^[.\\-]'))             return 0;
+   if (value.match('(\\.-|-\\.|\\.\\.)'))  return 0;
+   if (! (m = value.match('\.([a-z]+)$'))) return 0;
+   value = value.substring(0,value.lastIndexOf('.'));
+
+   if (m[1] == 'name') {
+     if (! value.match('^[a-z0-9][a-z0-9\\-]{0,62}\\.[a-z0-9][a-z0-9\\-]{0,62}$')) return 0;
+   } else
+     if (! value.match('^([a-z0-9][a-z0-9\\-]{0,62}\\.)*[a-z0-9][a-z0-9\\-]{0,62}$')) return 0;
+
+ /// validate a url
+ } else if (type == 'URL') {
+   if (! value) return 0;
+   if (! (m = value.match(new RegExp('^https?://([^/]+)','i'),''))) return 0;
+   value = value.substring(m[0].length);
+   if (! this.check_type(m[1],'DOMAIN') && ! this.check_type(m[1],'IP')) return 0;
+   if (value && ! this.check_type(value,'URI')) return 0;
+
+ /// validate a uri - the path portion of a request
+ } else if (type == 'URI') {
+   if (! value) return 0;
+   if (value.match('\\s')) return 0;
+
+ } else if (type == 'CC') {
+   if (! value) return 0;
+   if (value.match('[^\\d\\- ]') || value.length > 16 || value.length < 13) return;
+   /// simple mod10 check
+   value = value.replace(new RegExp('[\\- ]','g'), '');
+   var sum = 0;
+   var swc = 0;
+
+   for (var i = value.length - 1; i >= 0; i --) {
+     if (++ swc > 2) swc = 1;
+     var y = value.charAt(i) * swc;
+     if (y > 9) y -= 9;
+     sum += y;
+   }
+   if (sum % 10) return 0;
+
+ }
+
+ return 1;
+}
+
+// little routine that will get the values from the form
+// it will return multiple values as an array
+function vob_get_form_value (el) {
+ if (! el) return '';
+ if (el.disabled) return '';
+ var type = el.type ? el.type.toLowerCase() : '';
+ if (el.length && type != 'select-one') {
+   var a = new Array();
+   for (var j=0;j<el.length;j++) {
+     if (type.indexOf('multiple') != -1) {
+       if (el[j].selected) a[a.length] = el[j].value;
+     } else {
+       if (el[j].checked)  a[a.length] = vob_get_form_value(el[j]);
+     }
+   }
+   if (a.length == 0) return '';
+   if (a.length == 1) return a[0];
+   return a;
+ }
+ if (! type) return '';
+ if (type == 'hidden' || type == 'password' || type == 'text' || type == 'textarea' || type == 'submit')
+   return el.value;
+ if (type.indexOf('select') != -1) {
+   if (! el.length) return '';
+   return el[el.selectedIndex].value;
+ }
+ if (type == 'checkbox' || type == 'radio') {
+   return el.checked ? el.value : '';
+ }
+ if (type == 'file') {
+   return el.value; // hope this works
+ }
+ alert('Unknown form type for '+el.name+': '+type);
+ return '';
+}
+
+///----------------------------------------------------------------///
+
+function eob_get_val (key, extra2, extra1, _default) {
+ if (typeof(extra2[key]) != 'undefined') return extra2[key];
+ if (typeof(extra1[key]) != 'undefined') return extra1[key];
+ return _default;
+}
+
+function eob_as_string (extra2) {
+ var extra1 = this.extra;
+ if (! extra2) extra2 = new Array();
+
+ var joiner = eob_get_val('as_string_join',   extra2, extra1, '\n');
+ var header = eob_get_val('as_string_header', extra2, extra1, '');
+ var footer = eob_get_val('as_string_footer', extra2, extra1, '');
+
+ return header + this.as_array(extra2).join(joiner) + footer;
+}
+
+/// return an array of applicable errors
+function eob_as_array (extra2) {
+ var errors = this.errors;
+ var extra1 = this.extra;
+ if (! extra2) extra2 = new Array();
+
+ var title = eob_get_val('as_array_title', extra2, extra1, 'Please correct the following items:');
+
+ /// if there are heading items then we may end up needing a prefix
+ var has_headings;
+ if (title) has_headings = 1;
+ else {
+   for (var i = 0; i < errors.length; i ++) {
+     if (typeof(errors[i]) != 'string') continue;
+     has_headings = 1;
+     break;
+   }
+ }
+
+ var prefix = eob_get_val('as_array_prefix', extra2, extra1, has_headings ? '  ' : '');
+
+ /// get the array ready
+ var arr = new Array();
+ if (title && title.length) arr[arr.length] = title;
+ /// add the errors
+ var found = new Array();
+ for (var i = 0; i < errors.length; i ++) {
+   if (typeof(errors[i]) == 'string') {
+     arr[arr.length] = errors[i];
+     found = new Array();
+   } else {
+     var text = this.get_error_text(errors[i]);
+     if (found[text]) continue;
+     found[text] = 1;
+     arr[arr.length] = prefix + text;
+   }
+ }
+
+ return arr;
+}
+
+/// return a hash of applicable errors
+function eob_as_hash (extra2) {
+ var errors = this.errors;
+ var extra1 = this.extra;
+ if (! extra2) extra2 = new Array();
+ var suffix = eob_get_val('as_hash_suffix', extra2, extra1, '_error');
+ var joiner = eob_get_val('as_hash_join',   extra2, extra1, '<br />');
+
+ /// now add to the hash
+ var found = new Array();
+ var ret   = new Array();
+ for (var i = 0; i < errors.length; i ++) {
+   if (typeof(errors[i]) == 'string') continue;
+   if (! errors[i].length) continue;
+
+   var field     = errors[i][0];
+   var type      = errors[i][1];
+   var field_val = errors[i][2];
+   var ifs_match = errors[i][3];
+
+   if (! field) return alert("Missing field name");
+   if (field_val['delegate_error']) {
+     field = field_val['delegate_error'];
+     field = field.replace(new RegExp('\\$(\\d+)','g'), function (all, N) {
+       if (typeof(ifs_match) != 'object'
+           || typeof(ifs_match[N]) == 'undefined') return ''
+       return ifs_match[N];
+     });
+   }
+
+   var text = this.get_error_text(errors[i]);
+   if (! found[field]) found[field] = new Array();
+   if (found[field][text]) continue;
+   found[field][text] = 1;
+
+   field += suffix;
+   if (! ret[field]) ret[field] = new Array();
+   ret[field].push(text);
+ }
+
+ /// allow for elements returned as
+ if (joiner) {
+   var header = eob_get_val('as_hash_header', extra2, extra1, '');
+   var footer = eob_get_val('as_hash_footer', extra2, extra1, '');
+   for (var key in ret) ret[key] = header + ret[key].join(joiner) + footer;
+ }
+
+ return ret;
+}
+
+/// return a user friendly error message
+function eob_get_error_text (err) {
+ var extra     = this.extra;
+ var field     = err[0];
+ var type      = err[1];
+ var field_val = err[2];
+ var ifs_match = err[3];
+ var m;
+
+ var dig = (m = type.match('(_?\\d+)$')) ? m[1] : '';
+ var type_lc = type.toLowerCase();
+
+ /// allow for delegated field names - only used for defaults
+ if (field_val['delegate_error']) {
+   field = field_val['delegate_error'];
+   field = field.replace(new RegExp('\\$(\\d+)','g'), function (all, N) {
+     if (typeof(ifs_match) != 'object'
+         || typeof(ifs_match[N]) == 'undefined') return ''
+     return ifs_match[N];
+   });
+ }
+
+ /// the the name of this thing
+ var name = (field_val['name']) ? field_val['name'] : "The field " +field;
+ name = name.replace(new RegExp('\\$(\\d+)','g'), function (all, N) {
+   if (typeof(ifs_match) != 'object'
+       || typeof(ifs_match[N]) == 'undefined') return ''
+   return ifs_match[N];
+ });
+
+
+ /// type can look like "required" or "required2" or "required100023"
+ /// allow for fallback from required100023_error through required_error
+ var possible_keys = new Array(type + '_error');
+ if (dig.length) possible_keys.unshift(type + dig + '_error');
+
+ /// look in the passed hash or self first
+ for (var i = 0; i < possible_keys.length; i ++) {
+   var key = possible_keys[i];
+   var ret = field_val[key];
+   if (! ret) {
+     if (extra[key]) ret = extra[key];
+     else continue;
+   }
+   ret = ret.replace(new RegExp('\\$(\\d+)','g'), function (all, N) {
+     if (typeof(ifs_match) != 'object'
+         || typeof(ifs_match[N]) == 'undefined') return ''
+     return ifs_match[N];
+   });
+   ret = ret.replace(new RegExp('\\$field','g'), field);
+   ret = ret.replace(new RegExp('\\$name' ,'g'), name);
+   if (field_val[type + dig] && typeof(field_val[type + dig]) == 'string')
+     ret = ret.replace(new RegExp('\\$value' ,'g'), field_val[type + dig]);
+   return ret;
+ }
+
+ /// set default messages
+ if (type == 'required' || type == 'required_if') {
+   return name + " is required.";
+
+ } else if (type == 'min_values') {
+   var n = field_val["min_values" + dig];
+   var values = (n == 1) ? 'value' : 'values';
+   return name + " had less than "+n+" "+values+".";
+
+ } else if (type == 'max_values') {
+   var n = field_val["max_values" + dig];
+   var values = (n == 1) ? 'value' : 'values';
+   return name + " had more than "+n+" "+values+".";
+
+ } else if (type == 'min_in_set') {
+   var set = field_val["min_in_set" + dig];
+   return "Not enough fields were chosen from the set ("+set+")";
+   return "Too many fields were chosen from the set ("+set+")";
+
+ } else if (type == 'max_in_set') {
+   var set = field_val["max_in_set" + dig];
+   return "Too many fields were chosen from the set ("+set+")";
+
+ } else if (type == 'enum') {
+   return name + " is not in the given list.";
+
+ } else if (type == 'equals') {
+   var field2 = field_val["equals" + dig];
+   var name2  = field_val["equals" +dig+ "_name"];
+   if (! name2) name2 = "the field " +field2;
+   name2 = name2.replace(new RegExp('\\$(\\d+)','g'), function (all, N) {
+     if (typeof(ifs_match) != 'object'
+         || typeof(ifs_match[N]) == 'undefined') return ''
+     return ifs_match[N];
+   });
+   return name + " did not equal " + name2 +".";
+
+ } else if (type == 'min_len') {
+   var n = field_val["min_len"+dig];
+   var chars = (n == 1) ? 'character' : 'characters';
+   return name + " was less than "+n+" "+chars+".";
+
+ } else if (type == 'max_len') {
+   var n = field_val["max_len"+dig];
+   var chars = (n == 1) ? 'character' : 'characters';
+   return name + " was more than "+n+" "+chars+".";
+
+ } else if (type == 'match') {
+   return name + " contains invalid characters.";
+
+ } else if (type == 'compare') {
+   return name + " did not fit comparison.";
+
+ } else if (type == 'type') {
+   var _type = field_val["type"+dig];
+   return name + " did not match type "+_type+".";
+
+ } else if (type == 'custom_js') {
+   return name + " did not match custom_js"+dig+" check.";
+
+ }
+
+ return alert("Missing error on field "+field+" for type "+type+dig);
+}
+
+function eob_first_field () {
+ for (var i = 0; i < this.errors.length; i++) {
+   if (typeof(this.errors[i]) != 'object') continue;
+   if (! this.errors[i][0]) continue;
+   return this.errors[i][0];
+ }
+ return;
+}
+
+///----------------------------------------------------------------///
+
+document.validate = function (form, val_hash) {
+ // undo previous inline
+ if (document.did_inline) {
+   for (var key in document.did_inline) {
+     var el = document.getElementById(key);
+     if (el) el.innerHTML = '';
+   }
+   document.did_inline = undefined;
+ }
+
+ // do the validate
+ val_hash = document.load_val_hash(form, val_hash);
+ if (typeof(val_hash) == 'undefined') return true;
+ if (! document.val_obj) document.val_obj = new Validate();
+ var err_obj = document.val_obj.validate(form, val_hash);
+
+ // return success
+ if (! err_obj) return true;
+
+ // focus
+ var field = err_obj.first_field();
+ if (field && form[field] && form[field].focus) form[field].focus();
+
+ // inline
+ if (! err_obj.extra.no_inline) {
+   var d = document.did_inline = new Array();
+   var hash = err_obj.as_hash();
+   for (var key in hash) {
+     var el = document.getElementById(key);
+     if (el) el.innerHTML = hash[key];
+     d[key] = 1;
+   }
+ }
+
+ // alert
+ if (! err_obj.extra.no_confirm) {
+   return confirm(err_obj.as_string()) ? false : true;
+ } else if (! err_obj.extra.no_alert) {
+   alert(err_obj.as_string());
+   return false;
+ } else if (! err_obj.extra.no_inline) {
+   return false;
+ } else {
+   return true;
+ }
+}
+
+document.load_val_hash = function (form, val_hash) {
+ // check the form we are using
+ if (! form) return alert('Missing form or form name');
+ if (typeof(form) == 'string') {
+   if (! document[form]) return alert('No form by name '+form);
+   form = document[form];
+ }
+
+ // if we already have validation - use it
+ if (form.val_hash) return form.val_hash;
+
+ // load in the validation and save it for future use
+ if (typeof(val_hash) != 'object') {
+   // get the hash from a javascript function
+   if (typeof(val_hash) == 'function') {
+     val_hash = val_hash(formname);
+   } else if (typeof(val_hash) == 'undefined') {
+     var el;
+     // get hash from a global js variable
+     if (typeof(document.validation) != 'undefined') {
+       val_hash = document.validation;
+     // get hash from a element by if of validation
+     } else if (el = document.getElementById('validation')) {
+       val_hash = el.innerHTML;
+       val_hash = val_hash.replace(new RegExp('&lt;', 'ig'),'<');
+       val_hash = val_hash.replace(new RegExp('&gt;', 'ig'),'>');
+       val_hash = val_hash.replace(new RegExp('&amp;','ig'),'&');
+     // read hash from <input name=foo validation="">
+     } else {
+       var order = new Array();
+       var str   = '';
+       var yaml  = form.getAttribute('validation');
+       if (yaml) {
+         if (m = yaml.match('^( +)')) yaml = yaml.replace(new RegExp('^'+m[1], 'g'), ''); //unindent
+         yaml = yaml.replace(new RegExp('\\s*$',''),'\n'); // add trailing
+         str += yaml;
+       }
+       var m;
+       for (var i = 0; i < form.elements.length; i ++) {
+         var name = form.elements[i].name;
+         var yaml = form.elements[i].getAttribute('validation');
+         if (! name || ! yaml) continue;
+         yaml = yaml.replace(new RegExp('\\s*$',''),'\n'); // add trailing
+         yaml = yaml.replace(new RegExp('^(.)','mg'),' $1');    // indent all
+         yaml = yaml.replace(new RegExp('^( *[^\\s&*\\[\\{])',''),'\n$1'); // add newline
+         str += name +':' + yaml;
+         order[order.length] = name;
+       }
+       if (str) val_hash = str + "group order: [" + order.join(', ') + "]\n";
+     }
+   }
+   if (typeof(val_hash) == 'string') {
+     if (! document.yaml_load) return;
+     document.hide_yaml_errors = (! document.show_yaml_errors);
+     if (location.search && location.search.indexOf('show_yaml_errors') != -1)
+       document.hide_yaml_errors = 0;
+     val_hash = document.yaml_load(val_hash);
+     if (document.yaml_error_occured) return;
+   }
+ }
+
+ // attach to the form
+ form.val_hash = val_hash;
+ return form.val_hash;
+}
+
+
+document.check_form = function (form, val_hash) {
+ // check the form we are using
+ if (! form) return alert('Missing form or form name');
+ if (typeof(form) == 'string') {
+   if (! document[form]) return alert('No form by name '+form);
+   form = document[form];
+ }
+
+ // void call - allow for getting it at run time rather than later
+ document.load_val_hash(form, val_hash);
+
+ // attach handler
+ form.onsubmit = function () {return document.validate(this)};
+}
+
+// the end //
diff --git a/lib/CGI/Ex/yaml_load.js b/lib/CGI/Ex/yaml_load.js
new file mode 100644 (file)
index 0000000..0dddab4
--- /dev/null
@@ -0,0 +1,542 @@
+/**----------------------------------------------------------------***
+*  Copyright 2003 - Paul Seamons                                     *
+*  Distributed under the Perl Artistic License without warranty      *
+*  Based upon YAML.pm v0.35 from Perl                                *
+***----------------------------------------------------------------**/
+
+// $Revision: 1.16 $
+
+// allow for missing methods in ie 5.0
+
+if (! Array.prototype.unshift)
+  Array.prototype.unshift = function (add) {
+    for (var i=this.length; i > 0; i--) this[i] = this[i - 1];
+    this[0] = add;
+  };
+
+if (!Array.prototype.shift)
+  Array.prototype.shift = function () {
+    var ret = this[0];
+    for (var i=0; i<this.length-1; i++) this[i] = this[i + 1];
+    this.length -= 1;
+    return ret;
+  };
+
+if (!Array.prototype.push)
+  Array.prototype.push = function (add) {
+    this[this.length] = add;
+  };
+
+// and now - the rest of the library
+
+function YAML () {
+  this.parse            = yaml_parse;
+  this.error            = yaml_error;
+  this.warn             = yaml_warn;
+  this.parse_throwaway  = yaml_parse_throwaway;
+  this.parse_node       = yaml_parse_node;
+  this.parse_next_line  = yaml_parse_next_line;
+  this.parse_qualifiers = yaml_parse_qualifiers;
+  this.parse_explicit   = yaml_parse_explicit;
+  this.parse_implicit   = yaml_parse_implicit;
+  this.parse_map        = yaml_parse_map;
+  this.parse_seq        = yaml_parse_seq;
+  this.parse_inline     = yaml_parse_inline;
+}
+
+function yaml_error (err) {
+  err += '\nDocument: '+this.document+'\n';
+  err += '\nLine: '    +this.line    +'\n';
+  if (! document.hide_yaml_errors) alert(err);
+  document.yaml_error_occured = 1;
+  return;
+}
+
+function yaml_warn (err) {
+  if (! document.hide_yaml_errors) alert(err);
+  return;
+}
+
+function yaml_parse (text) {
+  document.yaml_error_occured = undefined;
+
+  // translate line endings down to \012
+  text = text.replace(new RegExp('\015\012','g'), '\012');
+  text = text.replace(new RegExp('\015','g'), '\012');
+  if (text.match('[\\x00-\\x08\\x0B-\\x0D\\x0E-\\x1F]'))
+    return this.error("Bad characters found");
+  if (text.length && ! text.match('\012$'))
+    text += '\012';
+
+  this.line      = 1;
+  this.lines     = text.split("\012");
+  this.document  = 0;
+  this.documents = new Array();
+
+  this.parse_throwaway();
+  if (! this.eoy && ! this.lines[0].match('^---(\\s|$)')) {
+    this.lines.unshift('--- #YAML:1.0');
+    this.line --;
+  }
+
+  // loop looking for data structures
+  while (! this.eoy) {
+    this.anchors = new Array();
+    this.offset  = new Array();
+    this.options = new Array();
+    this.document  ++;
+    this.done      = 0;
+    this.level     = 0;
+    this.offset[0] = -1;
+    this.preface   = '';
+    this.content   = '';
+    this.indent    = -1;
+
+    var m = this.lines[0].match('---\\s*(.*)$')
+    if (! m) return this.error("Missing YAML separator\n("+this.lines[0]+")");
+    var words = m[1].split("\\s+");
+    while (words.length && (m = words[0].match('^#(\\w+):(\\S.*)$'))) {
+      words.shift();
+      if (this.options[m[1]]) {
+        yaml.warn("Parse warn - multiple options " + m[1]);
+        continue;
+      }
+      this.options[m[1]] = m[2];
+    }
+
+    if (this.options['YAML'] && this.options['YAML'] != '1.0')
+      return this.error('Bad YAML version number - must be 1.0');
+    if (this.options['TAB'] && ! this.options['TAB'].match('^(NONE|\\d+)(:HARD)?$'))
+      return this.error('Unrecognized TAB policy');
+
+    this.documents.push(this.parse_node());
+  }
+
+  return this.documents;
+}
+
+function yaml_parse_throwaway () {
+  while (this.lines.length && this.lines[0].match('^\\s*(#|$)')) {
+    this.lines.shift();
+    this.line ++;
+  }
+  this.eoy = this.done = ! this.lines.length;
+}
+
+function yaml_parse_node (no_next) {
+  if (! no_next) this.parse_next_line(2); // COLLECTION
+
+  var preface   = this.preface;
+  this.preface  = '';
+  var node      = '';
+  var type      = '';
+  var indicator = '';
+  var escape    = '';
+  var chomp     = '';
+
+  var info     = this.parse_qualifiers(preface);
+  var anchor   = info[0];
+  var alias    = info[1];
+  var explicit = info[2];
+  var implicit = info[3];
+  var yclass   = info[4];
+  preface      = info[5];
+
+
+  if (alias) {
+    if (! this.anchors[alias]) return this.error("Parse error - missing alias: "+alias);
+    return this.anchors[alias];
+  }
+
+  // see if this is a literal or an unfold block
+  this.inline = '';
+  if (preface.length) {
+    m = preface.match('^([>\\|])([+\\-]?)\\d*\\s*');
+    if (m) {
+      indicator   = m[1];
+      chomp       = m[2];
+      preface     = preface.substring(0,m[0].length);
+    } else {
+      this.inline = preface;
+      preface     = '';
+    }
+  }
+
+  
+  if (this.inline.length) {
+    node = this.parse_inline(1, implicit, explicit, yclass);
+    if (this.inline.length) return this.error("Parse error - must be single line ("+this.inline+')');
+  } else {
+    this.level ++;
+    // block items
+    if (indicator) {
+      node = '';
+      while (! this.done && this.indent == this.offset[this.level]) {
+        node += this.content + '\n';
+        this.parse_next_line(1); // LEAF
+      }
+      if (indicator == '>') {
+        node = node.replace(new RegExp('[ \\t]*\n[ \\t]*(\\S)','gm'), ' $1');
+      }
+      if (! chomp || chomp == '-') node = node.replace(new RegExp('\n$',''),'');
+      if (implicit) node = this.parse_implicit(node);
+
+    } else {
+      if (! this.offset[this.level]) this.offset[this.level] = 0;
+      if (this.indent == this.offset[this.level]) {
+        if (this.content.match('^-( |$)')) {
+          node = this.parse_seq(anchor);
+        } else if (this.content.match('(^\\?|:( |$))')) {
+          node = this.parse_map(anchor);
+        } else if (preface.match('^\\s*$')) {
+          node = ''; //this.parse_implicit('');
+        } else {
+          return this.error('Parse error - bad node +('+this.content+')('+preface+')');
+        }
+      } else {
+        node = '';
+      }
+    }
+    this.level --
+  }
+  this.offset = this.offset.splice(0, this.level + 1);
+
+  if (explicit) {
+    if (yclass) return this.error("Parse error - classes not supported");
+    else node = this.parse_explicit(node, explicit);
+  }
+  if (anchor) this.anchors[anchor] = node;
+
+  return node;
+}
+
+function yaml_parse_next_line (type) {
+  var m;
+  var level  = this.level;
+  var offset = this.offset[level];
+
+  if (offset == undefined) return this.error("Parse error - Bad level " + level);
+
+  // done with the current line - get the next
+  // remove following commented lines
+  this.lines.shift();
+  this.line ++;
+  this.eoy = this.done = ! this.lines.length;
+  if (this.eoy) return;
+  this.parse_throwaway();
+  if (this.eoy) return;
+
+  // Determine the offset for a new leaf node
+  if (this.preface && (m = this.preface.match('[>\\|][+\\-]?(\\d*)\\s*$'))) {
+    if (m[1].length && m[1] == '0') return this.error("Parse error zero indent");
+    type = 1;
+    if (m[1].length) {
+      this.offset[level + 1] = offset + m[1];
+    } else if ((m = this.lines[0].match('^( *)\\S')) && m[1].length > offset) {
+      this.offset[level + 1] = m[1].length;
+    } else {
+      this.offset[level + 1] = offset + 1;
+    }
+    level ++;
+    offset = this.offset[level];
+  }
+
+  // COLLECTION
+  if (type == 2 && this.preface.match('^\\s*(!\\S*|&\\S+)*\\s*$')) {
+    m = this.lines[0].match('^( *)\\S');
+    if (! m) return this.error("Missing leading space on line "+this.lines[0]);
+    this.offset[level + 1] = (m[1].length > offset) ? m[1].length : offset + 1;
+    offset = this.offset[++ level];
+
+  // LEAF
+  } else if (type == 1) {
+    // skip blank lines and comment lines
+    while (this.lines.length && this.lines[0].match('^\\s*(#|$)')) {
+      m = this.lines[0].match('^( *)');
+      if (! m) return this.error("Missing leading space on comment " + this.lines[0]);
+      if (m[1].length > offset) break;
+      this.lines.shift();
+      this.line ++;
+    }
+    this.eoy = this.done = ! this.lines.length;      
+  } else {
+    this.parse_throwaway();
+  }
+
+  if (this.eoy) return;
+  if (this.lines[0].match('^---(\\s|$)')) {
+    this.done = 1;
+    return;
+  }
+
+  if (type == 1 && (m = this.lines[0].match('^ {'+offset+'}(.*)$'))) {
+    this.indent = offset;
+    this.content = m[1];
+  } else if (this.lines[0].match('^\\s*$')) {
+    this.indent = offset;
+    this.content = '';
+  } else {
+    m = this.lines[0].match('^( *)(\\S.*)$');
+    // # yaml.warn("   indent(${\length($1)})  offsets(@{$o->{offset}}) \n");
+    var len = (m) ? m[1].length : 0;
+    while (this.offset[level] > len) level --;
+    if (this.offset[level] != len)
+      return this.error("Parse error inconsitent indentation:\n"
+                        + '(this.lines[0]: '+this.lines[0]+', len: '+len+', level: '+level+', this.offset[level]: '+this.offset[level]+')\n');
+    
+    this.indent  = len;
+    this.content = m ? m[2] : '';
+  }
+
+  if (this.indent - offset > 1)
+    return this.error("Parse error - indentation");
+
+  return;
+}
+
+function yaml_parse_qualifiers (preface) {
+  var info = new Array();
+  // 0 = anchor
+  // 1 = alias
+  // 2 = explicit
+  // 3 = implicit
+  // 4 = class - not used for now
+  // 5 = preface
+
+  var m;
+  while (preface.match('^[&\\*!]')) {
+    // explicit, implicit
+    if (m = preface.match('^\!(\\S*)\\s*')) {
+      preface = preface.substring(m[0].length);
+      if (m[1].length) info[2] = m[1];
+      else info[3] = 1;
+    // anchor, alias
+    } else if (m = preface.match('^([&\\*])([^ ,:]+)\\s*')) {
+      preface = preface.substring(m[0].length);
+      if (! m[2].match('^\\w+$')) return this.error("Bad name "+m[2]);
+      if (info[0] || info[1]) return this.error("Already found anchor or alias "+m[2]);
+      if (m[1] == '&') info[0] = m[2];
+      if (m[1] == '*') info[1] = m[2];
+    }
+  }
+
+  info[5] = preface;
+  return info;
+}
+
+function yaml_parse_explicit (node, explicit) {
+  var m;
+  if (m = explicit.match('^(int|float|bool|date|time|datetime|binary)$')) {
+    // return this.error("No handler yet for explict " + m[1]);
+    // just won't check types for now
+    return node;
+  } else if (m = explicit.match('^perl/(glob|regexp|code|ref):(\\w(\\w|::)*)?$')) {
+    return this.error("No handler yet for perltype " + m[1]);
+  } else if (m = explicit.match('^perl/(\\@|\\$)?([a-zA-Z](\\w|::)+)$')) {
+    return this.error("No handler yet for perl object " + m[1]);
+  } else if (! (m = explicit.match('/'))) {
+    return this.error("Load error - no conversion "+explicit);
+  } else {
+    return this.error("No YAML::Node handler made yet "+explicit);
+  }
+}
+
+function yaml_parse_implicit (value) {
+  value.replace(new RegExp('\\s*$',''),'');
+  if (value == '') return '';
+  if (value.match('^-?\\d+$')) return 0 + value;
+  if (value.match('^[+-]?(\\d*)(\\.\\d*|)?([Ee][+-]?\\d+)?$')) return 1 * value;
+  if (value.match('^\\d{4}\-\\d{2}\-\\d{2}(T\\d{2}:\\d{2}:\\d{2}(\\.\\d*[1-9])?(Z|[-+]\\d{2}(:\\d{2})?))?$')
+      || value.match('^\\w')) return "" + value;
+  if (value == '~') return undefined;
+  if (value == '+') return 1;
+  if (value == '-') return 0;
+  return this.error("Parse Error bad implicit value ("+value+")");
+}
+
+function yaml_parse_map (anchor) {
+  var m;
+  var node = new Array ();
+  if (anchor) this.anchors[anchor] = node;
+  
+  while (! this.done && this.indent == this.offset[this.level]) {
+    var key;
+    if (this.content.match('^\\?\\s*')) {
+      this.preface = this.content;
+      key = '' + this.parse_node();
+    } else if (m = this.content.match('^=\\s*')) {
+      this.content = this.content.substring(m[0].length);
+      key = "\x07YAML\x07VALUE\x07";
+    } else if (m = this.content.match('^//\\s*')) {
+      this.content = this.content.substring(m[0].length);
+      key = "\x07YAML\x07COMMENT\x07";
+    } else {
+
+      this.inline = this.content;
+      key = this.parse_inline();
+      this.content = this.inline;
+      this.inline = '';
+    }
+
+    if (! (m = this.content.match('^:\\s*'))) return this.error("Parse error - bad map element "+this.content);
+    this.content = this.content.substring(m[0].length);
+   
+    this.preface = this.content;
+
+    var value = this.parse_node();
+
+    if (node[key]) this.warn('Warn - duplicate key '+key);
+    else node[key] = value;
+
+  }
+
+  return node;
+}
+
+function yaml_parse_seq (anchor) {
+  var m;
+  var node = new Array ();
+  if (anchor) this.anchors[anchor] = node;
+  while (! this.done && this.indent == this.offset[this.level]) {
+    var m;
+    if ((m = this.content.match('^- (.*)$')) || (m = this.content.match('^-()$'))) {
+      this.preface = m[1];
+    } else return this.error("Parse error - bad seq element "+this.content);
+
+    if (m = this.preface.match('^(\\s*)(\\w.*:( |$).*)$')) {
+      this.indent = this.offset[this.level] + 2 + m[1].length;
+      this.content = m[2];
+      this.offset[++ this.level] = this.indent;
+      this.preface = '';
+      node.push(this.parse_map(''));
+      this.level --;
+      this.offset[this.offset.length - 1] = this.level;
+    } else {
+      node.push(this.parse_node());
+    }
+  }
+
+  return node;
+}
+
+function yaml_parse_inline (top, top_implicit, top_explicit, top_class) {
+  this.inline = this.inline.replace('^\\s+','').replace(new RegExp('\\s+$',''),'');
+
+  var info     = this.parse_qualifiers(this.inline);
+  var anchor   = info[0];
+  var alias    = info[1];
+  var explicit = info[2];
+  var implicit = info[3];
+  var yclass   = info[4];
+  this.inline  = info[5];
+  var node;
+  var m;
+
+  // copy the reference
+  if (alias) {
+    if (! this.anchors[alias]) return this.error("Parse error - missing alias: "+alias);
+    node = this.anchors[alias];
+
+  // new key based array
+  } else if (m = this.inline.match('^\\{\\s*')) {
+    this.inline = this.inline.substring(m[0].length);
+    node = new Array ();
+    while (! (m = this.inline.match('^\\}'))) {
+      var key = this.parse_inline();
+      if (! (m = this.inline.match('^:\\s+'))) return this.error("Parse error - bad map element "+this.inline);
+      this.inline = this.inline.substring(m[0].length);
+      var value = this.parse_inline();
+      if (node[key]) this.warn("Warn - duplicate key found: "+key);
+      else node[key] = value;
+      if (this.inline.match('^\\}')) break;
+      if (! (m = this.inline.match('^,\\s*'))) return this.error("Parse error - missing map comma "+this.inline);
+      this.inline = this.inline.substring(m[0].length);
+    }
+    this.inline = this.inline.substring(m[0].length);    
+
+  // new array
+  } else if (m = this.inline.match('^\\[\\s*')) {
+    this.inline = this.inline.substring(m[0].length);
+    node = new Array ();
+    while (! (m = this.inline.match('^\\]'))) {
+      node.push(this.parse_inline());
+      if (m = this.inline.match('^\\]')) break;
+      if (! (m = this.inline.match('^,\\s*'))) return this.error("Parse error - missing seq comma "+this.inline);
+      this.inline = this.inline.substring(m[0].length);
+    }
+    this.inline = this.inline.substring(m[0].length);
+
+  // double quoted
+  } else if (this.inline.match('^"')) {
+    if (m = this.inline.match('^"((?:"|[^"])*)"\\s*(.*)$')) {
+      this.inline = m[2];
+      m[1] = m[1].replace(new RegExp('\\\\"','g'),'"');
+      node = m[1];
+    } else {
+      return this.error("Bad double quote "+this.inline);
+    }
+    node = unescape(node); // built in
+    if (implicit || top_implicit) node = this.parse_implicit(node);
+
+  // single quoted
+  } else if (this.inline.match("^'")) {
+    if (m = this.inline.match("^'((?:''|[^'])*)'\\s*(.*)$")) { 
+      this.inline = m[2];
+      m[1] = m[1].replace(new RegExp("''",'g'),"'");
+      node = m[1];
+    } else {
+      return this.error("Bad single quote "+this.inline);
+    }
+    node = unescape(node);  // built in
+    if (implicit || top_implicit) node = this.parse_implicit(node);
+
+  // simple
+  } else {
+    if (top) {
+      node = this.inline;
+      this.inline = '';
+    } else {
+      if (m = this.inline.match('^([^!@#%^&*,\\[\\]{}\\:]*)')) {
+        this.inline = this.inline.substring(m[1].length);
+        node = m[1];
+      } else {
+        return this.error ("Bad simple match "+this.inline);
+      }
+      if (! explicit && ! top_explicit) node = this.parse_implicit(node);
+    }
+  }
+  if (explicit || top_explicit) {
+    if (! explicit) explicit = top_explicit;
+    if (yclass) return this.error("Parse error - classes not supported");
+    else node = this.parse_explicit(node, explicit);
+  }
+
+  if (anchor) this.anchors[anchor] = node;
+
+  return node;
+}
+
+document.yaml_load = function (text, anchors) {
+  var yaml = new YAML();
+  return yaml.parse(text, anchors);
+}
+
+document.js_dump = function (obj, name) {
+  var t = '';
+  if (! name) {
+    name = '[obj]';
+    t = 'Dump:\n'
+  }
+  if (typeof(obj) == 'function') return name+'=[FUNCTION]\n'
+  if (typeof(obj) != 'object') return name+'='+obj+'\n';
+  var hold = new Array();
+  for (var i in obj) hold[hold.length] = i;
+  hold = hold.sort();
+  for (var i = 0; i < hold.length; i++) {
+    var n = hold[i];
+    t += document.js_dump(obj[n], name +'.'+n);
+  }
+  return t;
+}
+
+// the end
diff --git a/t/0_ex_00_base.t b/t/0_ex_00_base.t
new file mode 100644 (file)
index 0000000..a31a737
--- /dev/null
@@ -0,0 +1,8 @@
+
+BEGIN {
+       print "1..1\n";
+}
+
+use CGI::Ex;
+
+BEGIN { print "ok 1\n"; }
diff --git a/t/0_ex_01_swap.t b/t/0_ex_01_swap.t
new file mode 100644 (file)
index 0000000..528b1d3
--- /dev/null
@@ -0,0 +1,34 @@
+# -*- Mode: Perl; -*-
+
+use Test;
+
+BEGIN {plan tests => 4};
+
+use CGI::Ex;
+ok(1);
+
+my $cgix = CGI::Ex->new;
+my $form = {foo => 'bar', this => {is => {nested => ['wow', 'wee']}}};
+
+ok('bar' eq $cgix->swap_template("[% foo %]", $form));
+
+ok('wee' eq $cgix->swap_template("[% this.is.nested.1 %]", $form));
+
+my $str = "[% this.is.nested.0 %]";
+$cgix->swap_template(\$str, $form);
+ok('wow' eq $str);
+
+$cgix = CGI::Ex->new;
+$cgix->set_form({
+  foo => 'bar',
+  baz => 'wow',
+  this => 'wee',
+});
+$str = "<html>([% foo %]) <br>
+([% baz %]) <br>
+([% this %]) </html>";
+$cgix->swap_template(\$str);
+print $str;
+ok($str eq "<html>(bar) <br>
+(wow) <br>
+(wee) </html>");
diff --git a/t/1_validate_00_base.t b/t/1_validate_00_base.t
new file mode 100644 (file)
index 0000000..d8ea8df
--- /dev/null
@@ -0,0 +1,8 @@
+
+BEGIN {
+       print "1..1\n";
+}
+
+use CGI::Ex::Validate;
+
+BEGIN { print "ok 1\n"; }
diff --git a/t/1_validate_01_form.t b/t/1_validate_01_form.t
new file mode 100644 (file)
index 0000000..1f15630
--- /dev/null
@@ -0,0 +1,32 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+print "1..2\n";
+
+use CGI::Ex;
+
+print "ok 1\n";
+
+my $form = {
+  user => 'abc',
+  pass => '123',
+};
+my $val = {
+  user => {
+    required => 1,
+  },
+  pass => {
+    required => 1,
+  },
+};
+
+my $err_obj = CGI::Ex->new->validate($form,$val);
+
+if (! $err_obj) {
+  print "ok 2\n";
+} else {
+  print "not ok 2\n";
+}
diff --git a/t/1_validate_02_form_fail.t b/t/1_validate_02_form_fail.t
new file mode 100644 (file)
index 0000000..b833489
--- /dev/null
@@ -0,0 +1,32 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+print "1..2\n";
+
+use CGI::Ex;
+
+print "ok 1\n";
+
+my $form = {
+  user => 'abc',
+#  pass => '123',
+};
+my $val = {
+  user => {
+    required => 1,
+  },
+  pass => {
+    required => 1,
+  },
+};
+
+my $err_obj = CGI::Ex->new->validate($form,$val);
+
+if ($err_obj) {
+  print "ok 2\n";
+} else {
+  print "not ok 2\n";
+}
diff --git a/t/1_validate_03_cgi.t b/t/1_validate_03_cgi.t
new file mode 100644 (file)
index 0000000..32dff8f
--- /dev/null
@@ -0,0 +1,34 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+print "1..2\n";
+
+use CGI::Ex;
+use CGI;
+
+print "ok 1\n";
+
+my $form = CGI->new({
+  user => 'abc',
+  pass => '123',
+});
+my $val = {
+  user => {
+    required => 1,
+  },
+  pass => {
+    required => 1,
+  },
+};
+
+my $err_obj = CGI::Ex->new->validate($form,$val);
+
+if (! $err_obj) {
+  print "ok 2\n";
+} else {
+  warn "$err_obj\n";
+  print "not ok 2\n";
+}
diff --git a/t/1_validate_04_cgi_fail.t b/t/1_validate_04_cgi_fail.t
new file mode 100644 (file)
index 0000000..32f96e1
--- /dev/null
@@ -0,0 +1,33 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+print "1..2\n";
+
+use CGI::Ex;
+use CGI;
+
+print "ok 1\n";
+
+my $form = CGI->new({
+  user => 'abc',
+#  pass => '123',
+});
+my $val = {
+  user => {
+    required => 1,
+  },
+  pass => {
+    required => 1,
+  },
+};
+
+my $err_obj = CGI::Ex->new->validate($form,$val);
+
+if ($err_obj) {
+  print "ok 2\n";
+} else {
+  print "not ok 2\n";
+}
diff --git a/t/1_validate_05_types.t b/t/1_validate_05_types.t
new file mode 100644 (file)
index 0000000..bb87b5b
--- /dev/null
@@ -0,0 +1,378 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+### determine number of tests
+seek(DATA,0,0);
+my $prog  = join "", <DATA>;
+my @tests = ($prog =~ /&print_ok\(/g);
+my $tests = @tests;
+print "1..$tests\n";
+
+require CGI::Ex::Validate;
+
+my ($N, $v, $e, $ok) = (0);
+
+sub validate {
+  return scalar &CGI::Ex::Validate::validate(@_);
+}
+sub print_ok {
+  my $ok = shift;
+  $N ++;
+  warn "Test failed at line ".(caller)[2]."\n" if ! $ok;
+  print "" . ($ok ? "" : "not ") . "ok $N\n";
+}
+&print_ok(1);
+
+
+### required
+$v = {foo => {required => 1}};
+$e = &validate({}, $v);
+&print_ok($e);
+
+$e = &validate({foo => 1}, $v);
+&print_ok(! $e);
+
+### validate_if
+$v = {foo => {required => 1, validate_if => 'bar'}};
+$e = &validate({}, $v);
+&print_ok(! $e);
+
+$e = &validate({bar => 1}, $v);
+&print_ok($e);
+
+### required_if
+$v = {foo => {required_if => 'bar'}};
+$e = &validate({}, $v);
+&print_ok(! $e);
+
+$e = &validate({bar => 1}, $v);
+&print_ok($e);
+
+### max_values
+$v = {foo => {required => 1}};
+$e = &validate({foo => [1,2]}, $v);
+&print_ok($e);
+
+$v = {foo => {max_values => 2}};
+$e = &validate({}, $v);
+&print_ok(! $e);
+
+$e = &validate({foo => "str"}, $v);
+&print_ok(! $e);
+
+$e = &validate({foo => [1]}, $v);
+&print_ok(! $e);
+
+$e = &validate({foo => [1,2]}, $v);
+&print_ok(! $e);
+
+$e = &validate({foo => [1,2,3]}, $v);
+&print_ok($e);
+
+### min_values
+$v = {foo => {min_values => 3, max_values => 10}};
+$e = &validate({foo => [1,2,3]}, $v);
+&print_ok(! $e);
+
+$e = &validate({foo => [1,2,3,4]}, $v);
+&print_ok(! $e);
+
+$e = &validate({foo => [1,2]}, $v);
+&print_ok($e);
+
+$e = &validate({foo => "str"}, $v);
+&print_ok($e);
+
+$e = &validate({}, $v);
+&print_ok($e);
+
+### enum
+$v = {foo => {enum => [1, 2, 3]}, bar => {enum => "1 || 2||3"}};
+$e = &validate({}, $v);
+&print_ok($e);
+
+$e = &validate({foo => 1, bar => 1}, $v);
+&print_ok(! $e);
+
+$e = &validate({foo => 1, bar => 2}, $v);
+&print_ok(! $e);
+
+$e = &validate({foo => 1, bar => 3}, $v);
+&print_ok(! $e);
+
+$e = &validate({foo => 1, bar => 4}, $v);
+&print_ok($e);
+
+# equals
+$v = {foo => {equals => 'bar'}};
+$e = &validate({}, $v);
+&print_ok(! $e);
+
+$e = &validate({foo => 1}, $v);
+&print_ok($e);
+
+$e = &validate({bar => 1}, $v);
+&print_ok($e);
+
+$e = &validate({foo => 1, bar => 2}, $v);
+&print_ok($e);
+
+$e = &validate({foo => 1, bar => 1}, $v);
+&print_ok(! $e);
+
+$v = {foo => {equals => '"bar"'}};
+$e = &validate({foo => 1, bar => 1}, $v);
+&print_ok($e);
+
+$e = &validate({foo => 'bar', bar => 1}, $v);
+&print_ok(! $e);
+
+### min_len
+$v = {foo => {min_len => 10}};
+$e = &validate({}, $v);
+&print_ok($e);
+
+$e = &validate({foo => ""}, $v);
+&print_ok($e);
+
+$e = &validate({foo => "123456789"}, $v);
+&print_ok($e);
+
+$e = &validate({foo => "1234567890"}, $v);
+&print_ok(! $e);
+
+### max_len
+$v = {foo => {max_len => 10}};
+$e = &validate({}, $v);
+&print_ok(! $e);
+
+$e = &validate({foo => ""}, $v);
+&print_ok(! $e);
+
+$e = &validate({foo => "1234567890"}, $v);
+&print_ok(! $e);
+
+$e = &validate({foo => "12345678901"}, $v);
+&print_ok($e);
+
+### match
+$v = {foo => {match => qr/^\w+$/}};
+$e = &validate({foo => "abc"}, $v);
+&print_ok(! $e);
+
+$e = &validate({foo => "abc."}, $v);
+&print_ok($e);
+
+$v = {foo => {match => [qr/^\w+$/, qr/^[a-z]+$/]}};
+$e = &validate({foo => "abc"}, $v);
+&print_ok(! $e);
+
+$e = &validate({foo => "abc1"}, $v);
+&print_ok($e);
+
+$v = {foo => {match => 'm/^\w+$/'}};
+$e = &validate({foo => "abc"}, $v);
+&print_ok(! $e);
+
+$e = &validate({foo => "abc."}, $v);
+&print_ok($e);
+
+$v = {foo => {match => 'm/^\w+$/ || m/^[a-z]+$/'}};
+$e = &validate({foo => "abc"}, $v);
+&print_ok(! $e);
+
+$e = &validate({foo => "abc1"}, $v);
+&print_ok($e);
+
+$v = {foo => {match => '! m/^\w+$/'}};
+$e = &validate({foo => "abc"}, $v);
+&print_ok($e);
+
+$e = &validate({foo => "abc."}, $v);
+&print_ok(! $e);
+
+$v = {foo => {match => 'm/^\w+$/'}};
+$e = &validate({}, $v);
+&print_ok($e);
+
+$v = {foo => {match => '! m/^\w+$/'}};
+$e = &validate({}, $v);
+&print_ok(! $e);
+
+### compare
+$v = {foo => {compare => '> 0'}};
+$e = &validate({}, $v);
+&print_ok($e);
+$v = {foo => {compare => '== 0'}};
+$e = &validate({}, $v);
+&print_ok(! $e);
+$v = {foo => {compare => '< 0'}};
+$e = &validate({}, $v);
+&print_ok($e);
+
+$v = {foo => {compare => '> 10'}};
+$e = &validate({foo => 11}, $v);
+&print_ok(! $e);
+$e = &validate({foo => 10}, $v);
+&print_ok($e);
+
+$v = {foo => {compare => '== 10'}};
+$e = &validate({foo => 11}, $v);
+&print_ok($e);
+$e = &validate({foo => 10}, $v);
+&print_ok(! $e);
+
+$v = {foo => {compare => '< 10'}};
+$e = &validate({foo => 9}, $v);
+&print_ok(! $e);
+$e = &validate({foo => 10}, $v);
+&print_ok($e);
+
+$v = {foo => {compare => '>= 10'}};
+$e = &validate({foo => 10}, $v);
+&print_ok(! $e);
+$e = &validate({foo => 9}, $v);
+&print_ok($e);
+
+$v = {foo => {compare => '!= 10'}};
+$e = &validate({foo => 10}, $v);
+&print_ok($e);
+$e = &validate({foo => 9}, $v);
+&print_ok(! $e);
+
+$v = {foo => {compare => '<= 10'}};
+$e = &validate({foo => 11}, $v);
+&print_ok($e);
+$e = &validate({foo => 10}, $v);
+&print_ok(! $e);
+
+
+$v = {foo => {compare => 'gt ""'}};
+$e = &validate({}, $v);
+&print_ok($e);
+$v = {foo => {compare => 'eq ""'}};
+$e = &validate({}, $v);
+&print_ok(! $e);
+$v = {foo => {compare => 'lt ""'}};
+$e = &validate({}, $v);
+&print_ok($e); # 68
+
+$v = {foo => {compare => 'gt "c"'}};
+$e = &validate({foo => 'd'}, $v);
+&print_ok(! $e);
+$e = &validate({foo => 'c'}, $v);
+&print_ok($e);
+
+$v = {foo => {compare => 'eq c'}};
+$e = &validate({foo => 'd'}, $v);
+&print_ok($e);
+$e = &validate({foo => 'c'}, $v);
+&print_ok(! $e);
+
+$v = {foo => {compare => 'lt c'}};
+$e = &validate({foo => 'b'}, $v);
+&print_ok(! $e);
+$e = &validate({foo => 'c'}, $v);
+&print_ok($e);
+
+$v = {foo => {compare => 'ge c'}};
+$e = &validate({foo => 'c'}, $v);
+&print_ok(! $e);
+$e = &validate({foo => 'b'}, $v);
+&print_ok($e);
+
+$v = {foo => {compare => 'ne c'}};
+$e = &validate({foo => 'c'}, $v);
+&print_ok($e);
+$e = &validate({foo => 'b'}, $v);
+&print_ok(! $e);
+
+$v = {foo => {compare => 'le c'}};
+$e = &validate({foo => 'd'}, $v);
+&print_ok($e);
+$e = &validate({foo => 'c'}, $v);
+&print_ok(! $e); # 80
+
+### sql
+### can't really do anything here without prompting for a db connection
+
+### custom
+my $n = 1;
+$v = {foo => {custom => $n}};
+$e = &validate({}, $v);
+&print_ok(! $e);
+$e = &validate({foo => "str"}, $v);
+&print_ok(! $e);
+
+$n = 0;
+$v = {foo => {custom => $n}};
+$e = &validate({}, $v);
+&print_ok($e);
+$e = &validate({foo => "str"}, $v);
+&print_ok($e);
+
+$n = sub { my ($key, $val) = @_; return defined($val) ? 1 : 0};
+$v = {foo => {custom => $n}};
+$e = &validate({}, $v);
+&print_ok($e);
+$e = &validate({foo => "str"}, $v);
+&print_ok(! $e);
+
+### type checks
+$v = {foo => {type => 'ip'}};
+$e = &validate({foo => '209.108.25'}, $v);
+&print_ok($e);
+$e = &validate({foo => '209.108.25.111'}, $v);
+&print_ok(! $e);
+
+### min_in_set checks
+$v = {foo => {min_in_set => '2 of foo bar baz', max_values => 5}};
+$e = &validate({foo => 1}, $v);
+&print_ok($e);
+$e = &validate({foo => 1, bar => 1}, $v);
+&print_ok(! $e);
+$e = &validate({foo => 1, bar => ''}, $v); # empty string doesn't count as value
+&print_ok($e);
+$e = &validate({foo => 1, bar => 0}, $v);
+&print_ok(! $e);
+$e = &validate({foo => [1, 2]}, $v);
+&print_ok(! $e);
+$e = &validate({foo => [1]}, $v);
+&print_ok($e);
+$v = {foo => {min_in_set => '2 foo bar baz', max_values => 5}};
+$e = &validate({foo => 1, bar => 1}, $v);
+&print_ok(! $e);
+
+### max_in_set checks
+$v = {foo => {max_in_set => '2 of foo bar baz', max_values => 5}};
+$e = &validate({foo => 1}, $v);
+&print_ok(! $e);
+$e = &validate({foo => 1, bar => 1}, $v);
+&print_ok(! $e);
+$e = &validate({foo => 1, bar => 1, baz => 1}, $v);
+&print_ok($e);
+$e = &validate({foo => [1, 2]}, $v);
+&print_ok(! $e);
+$e = &validate({foo => [1, 2, 3]}, $v);
+&print_ok($e);
+
+### validate_if revisited (but negated - uses max_in_set)
+$v = {foo => {required => 1, validate_if => '! bar'}};
+$e = &validate({}, $v);
+&print_ok($e);
+
+$e = &validate({bar => 1}, $v);
+&print_ok(! $e);
+
+### default value
+my $f = {};
+$v = {foo => {required => 1, default => 'hmmmm'}};
+$e = &validate($f, $v);
+&print_ok(! $e);
+
+&print_ok($f->{foo} && $f->{foo} eq 'hmmmm');
+
+__DATA__
diff --git a/t/1_validate_06_groups.t b/t/1_validate_06_groups.t
new file mode 100644 (file)
index 0000000..972624c
--- /dev/null
@@ -0,0 +1,82 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+### determine number of tests
+seek(DATA,0,0);
+my $prog  = join "", <DATA>;
+my @tests = ($prog =~ /&print_ok\(/g);
+my $tests = @tests;
+print "1..$tests\n";
+
+require CGI::Ex::Validate;
+
+my ($N, $v, $e, $ok) = (0);
+
+sub validate {
+  return scalar &CGI::Ex::Validate::validate(@_);
+}
+sub print_ok {
+  my $ok = shift;
+  $N ++;
+  warn "Test failed at line ".(caller)[2]."\n" if ! $ok;
+  print "" . ($ok ? "" : "not ") . "ok $N\n";
+}
+&print_ok(1);
+
+###----------------------------------------------------------------###
+
+### three groups, some with validate_if's
+$v = [{
+  'group validate_if' => 'foo',
+  bar => {required => 1},
+},
+{
+  'group validate_if' => 'hem',
+  haw => {required => 1},
+},
+{
+  raspberry => {required => 1},
+}];
+
+$e = &validate({}, $v);
+&print_ok($e);
+
+$e = &validate({
+  raspberry => 'tart',
+}, $v);
+&print_ok(! $e);
+
+$e = &validate({
+  foo => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok($e);
+
+$e = &validate({
+  foo => 1,
+  bar => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok(! $e);
+
+$e = &validate({
+  foo => 1,
+  bar => 1,
+  hem => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok($e);
+
+$e = &validate({
+  foo => 1,
+  bar => 1,
+  hem => 1,
+  haw => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok(! $e);
+
+__DATA__
diff --git a/t/1_validate_07_yaml.t b/t/1_validate_07_yaml.t
new file mode 100644 (file)
index 0000000..61f9f1e
--- /dev/null
@@ -0,0 +1,150 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+### determine number of tests
+seek(DATA,0,0);
+my $prog  = join "", <DATA>;
+my @tests = ($prog =~ /&print_ok\(/g);
+my $tests = @tests;
+print "1..$tests\n";
+
+require CGI::Ex::Validate;
+
+my ($N, $v, $e, $ok) = (0);
+
+sub validate {
+  return scalar &CGI::Ex::Validate::validate(@_);
+}
+sub print_ok {
+  my $ok = shift;
+  $N ++;
+  warn "Test failed at line ".(caller)[2]."\n" if ! $ok;
+  print "" . ($ok ? "" : "not ") . "ok $N\n";
+}
+&print_ok(1);
+
+###----------------------------------------------------------------###
+
+### single group
+$v = '
+user:
+  required: 1
+foo:
+  required_if: bar
+';
+
+$e = &validate({}, $v);
+&print_ok($e);
+$e = &validate({user => 1}, $v);
+&print_ok(! $e);
+$e = &validate({user => 1, bar => 1}, $v);
+&print_ok($e);
+$e = &validate({user => 1, bar => 1, foo => 1}, $v);
+&print_ok(! $e);
+
+
+### three groups, some with validate_if's - using arrayref
+$v = '
+- group validate_if: foo
+  bar:
+    required: 1
+- group validate_if: hem
+  haw: { required: 1 }
+- raspberry:
+    required: 1
+';
+
+$e = &validate({}, $v);
+&print_ok($e);
+
+$e = &validate({
+  raspberry => 'tart',
+}, $v);
+&print_ok(! $e);
+
+$e = &validate({
+  foo => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok($e);
+
+$e = &validate({
+  foo => 1,
+  bar => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok(! $e);
+
+$e = &validate({
+  foo => 1,
+  bar => 1,
+  hem => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok($e);
+
+$e = &validate({
+  foo => 1,
+  bar => 1,
+  hem => 1,
+  haw => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok(! $e);
+
+
+### three groups, some with validate_if's - using documents
+$v = '---
+group validate_if: foo
+bar:
+  required: 1
+---
+group validate_if: hem
+haw: { required: 1 }
+---
+raspberry:
+  required: 1
+';
+
+$e = &validate({}, $v);
+&print_ok($e);
+
+$e = &validate({
+  raspberry => 'tart',
+}, $v);
+&print_ok(! $e);
+
+$e = &validate({
+  foo => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok($e);
+
+$e = &validate({
+  foo => 1,
+  bar => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok(! $e);
+
+$e = &validate({
+  foo => 1,
+  bar => 1,
+  hem => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok($e);
+
+$e = &validate({
+  foo => 1,
+  bar => 1,
+  hem => 1,
+  haw => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok(! $e);
+
+__DATA__
diff --git a/t/1_validate_08_yaml_file.t b/t/1_validate_08_yaml_file.t
new file mode 100644 (file)
index 0000000..8881462
--- /dev/null
@@ -0,0 +1,146 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+### determine number of tests
+seek(DATA,0,0);
+my $prog  = join "", <DATA>;
+my @tests = ($prog =~ /&print_ok\(/g);
+my $tests = @tests;
+print "1..$tests\n";
+
+require CGI::Ex::Validate;
+
+my ($N, $v, $e, $ok) = (0);
+
+sub validate {
+  return scalar &CGI::Ex::Validate::validate(@_);
+}
+sub print_ok {
+  my $ok = shift;
+  $N ++;
+  warn "Test failed at line ".(caller)[2]."\n" if ! $ok;
+  print "" . ($ok ? "" : "not ") . "ok $N\n";
+}
+&print_ok(1);
+
+###----------------------------------------------------------------###
+
+### where are my samples
+my $dir = __FILE__;
+$dir =~ tr|\\|/|; # should probably use File::Spec
+$dir =~ s|[^/]+$|samples| || die "Couldn't determine dir";
+$dir =~ s|^t/|./t/|; # to satisfy conf
+
+### single group
+$v = "$dir/yaml1.val";
+
+$e = &validate({}, $v);
+&print_ok($e);
+$e = &validate({user => 1}, $v);
+&print_ok(! $e);
+$e = &validate({user => 1, bar => 1}, $v);
+&print_ok($e);
+$e = &validate({user => 1, bar => 1, foo => 1}, $v);
+&print_ok(! $e);
+
+
+### single group - default extension
+$v = "$dir/yaml1";
+
+$e = &validate({}, $v);
+&print_ok($e);
+$e = &validate({user => 1}, $v);
+&print_ok(! $e);
+$e = &validate({user => 1, bar => 1}, $v);
+&print_ok($e);
+$e = &validate({user => 1, bar => 1, foo => 1}, $v);
+&print_ok(! $e);
+
+
+### three groups, some with validate_if's - using arrayref
+$v = "$dir/yaml2.val";
+
+$e = &validate({}, $v);
+&print_ok($e);
+
+$e = &validate({
+  raspberry => 'tart',
+}, $v);
+&print_ok(! $e);
+
+$e = &validate({
+  foo => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok($e);
+
+$e = &validate({
+  foo => 1,
+  bar => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok(! $e);
+
+$e = &validate({
+  foo => 1,
+  bar => 1,
+  hem => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok($e);
+
+$e = &validate({
+  foo => 1,
+  bar => 1,
+  hem => 1,
+  haw => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok(! $e);
+
+
+### three groups, some with validate_if's - using documents
+$v = "$dir/yaml3.val";
+
+$e = &validate({}, $v);
+&print_ok($e);
+
+$e = &validate({
+  raspberry => 'tart',
+}, $v);
+&print_ok(! $e);
+
+$e = &validate({
+  foo => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok($e);
+
+$e = &validate({
+  foo => 1,
+  bar => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok(! $e);
+
+$e = &validate({
+  foo => 1,
+  bar => 1,
+  hem => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok($e);
+
+$e = &validate({
+  foo => 1,
+  bar => 1,
+  hem => 1,
+  haw => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok(! $e);
+
+__DATA__
diff --git a/t/1_validate_09_perl_file.t b/t/1_validate_09_perl_file.t
new file mode 100644 (file)
index 0000000..1faa1e9
--- /dev/null
@@ -0,0 +1,91 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+### determine number of tests
+seek(DATA,0,0);
+my $prog  = join "", <DATA>;
+my @tests = ($prog =~ /&print_ok\(/g);
+my $tests = @tests;
+print "1..$tests\n";
+
+require CGI::Ex::Validate;
+
+my ($N, $v, $e, $ok) = (0);
+
+sub validate {
+  return scalar &CGI::Ex::Validate::validate(@_);
+}
+sub print_ok {
+  my $ok = shift;
+  $N ++;
+  warn "Test failed at line ".(caller)[2]."\n" if ! $ok;
+  print "" . ($ok ? "" : "not ") . "ok $N\n";
+}
+&print_ok(1);
+
+###----------------------------------------------------------------###
+
+### where are my samples
+my $dir = __FILE__;
+$dir =~ tr|\\|/|; # should probably use File::Spec
+$dir =~ s|[^/]+$|samples| || die "Couldn't determine dir";
+$dir =~ s|^t/|./t/|; # to satisfy conf
+
+### single group
+$v = "$dir/perl1.pl";
+
+$e = &validate({}, $v);
+&print_ok($e);
+$e = &validate({user => 1}, $v);
+&print_ok(! $e);
+$e = &validate({user => 1, bar => 1}, $v);
+&print_ok($e);
+$e = &validate({user => 1, bar => 1, foo => 1}, $v);
+&print_ok(! $e);
+
+
+### three groups, some with validate_if's - using arrayref
+$v = "$dir/perl2.pl";
+
+$e = &validate({}, $v);
+&print_ok($e);
+
+$e = &validate({
+  raspberry => 'tart',
+}, $v);
+&print_ok(! $e);
+
+$e = &validate({
+  foo => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok($e);
+
+$e = &validate({
+  foo => 1,
+  bar => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok(! $e);
+
+$e = &validate({
+  foo => 1,
+  bar => 1,
+  hem => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok($e);
+
+$e = &validate({
+  foo => 1,
+  bar => 1,
+  hem => 1,
+  haw => 1,
+  raspberry => 'tart',
+}, $v);
+&print_ok(! $e);
+
+__DATA__
diff --git a/t/1_validate_10_storable_file.t b/t/1_validate_10_storable_file.t
new file mode 100644 (file)
index 0000000..73d8865
--- /dev/null
@@ -0,0 +1,61 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+### determine number of tests
+seek(DATA,0,0);
+my $prog  = join "", <DATA>;
+my @tests = ($prog =~ /&print_ok\(/g);
+my $tests = @tests;
+print "1..$tests\n";
+
+require CGI::Ex::Validate;
+
+my ($N, $v, $e, $ok) = (0);
+
+sub validate {
+  return scalar &CGI::Ex::Validate::validate(@_);
+}
+sub print_ok {
+  my $ok = shift;
+  $N ++;
+  warn "Test failed at line ".(caller)[2]."\n" if ! $ok;
+  print "" . ($ok ? "" : "not ") . "ok $N\n";
+}
+&print_ok(1);
+
+###----------------------------------------------------------------###
+
+### where are my samples
+my $dir = __FILE__;
+$dir =~ tr|\\|/|; # should probably use File::Spec
+$dir =~ s|[^/]+$|samples| || die "Couldn't determine dir";
+$dir =~ s|^t/|./t/|; # to satisfy conf
+
+### single group
+$v = "$dir/storable1.storable";
+
+### don't use the included binary - write our own - for portable tests
+my $val = {
+  user => {
+    required => 1,
+  },
+  foo => {
+    required_if => 'bar',
+  },
+};
+&print_ok(eval {require Storable});
+&print_ok(&Storable::store($val, $v));
+
+$e = &validate({}, $v);
+&print_ok($e);
+$e = &validate({user => 1}, $v);
+&print_ok(! $e);
+$e = &validate({user => 1, bar => 1}, $v);
+&print_ok($e);
+$e = &validate({user => 1, bar => 1, foo => 1}, $v);
+&print_ok(! $e);
+
+__DATA__
diff --git a/t/1_validate_11_no_extra.t b/t/1_validate_11_no_extra.t
new file mode 100644 (file)
index 0000000..46ca0e6
--- /dev/null
@@ -0,0 +1,148 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+### determine number of tests
+seek(DATA,0,0);
+my $prog  = join "", <DATA>;
+my @tests = ($prog =~ /&print_ok\(/g);
+my $tests = @tests;
+print "1..$tests\n";
+
+require CGI::Ex::Validate;
+
+my ($N, $v, $e, $ok) = (0);
+
+sub validate {
+  return scalar &CGI::Ex::Validate::validate(@_);
+}
+sub print_ok {
+  my $ok = shift;
+  $N ++;
+  warn "Test failed at line ".(caller)[2]."\n" if ! $ok;
+  print "" . ($ok ? "" : "not ") . "ok $N\n";
+}
+&print_ok(1);
+
+###----------------------------------------------------------------###
+
+### test single group for extra fields
+$v = [
+{
+  'general no_extra_fields' => 'all',
+  foo => {max_len => 10},
+},
+];
+
+$e = &validate({}, $v);
+&print_ok(! $e);
+
+$e = &validate({foo => "foo"}, $v);
+&print_ok(! $e);
+
+$e = &validate({foo => "foo", bar => "bar"}, $v);
+&print_ok($e);
+
+$e = &validate({bar => "bar"}, $v);
+&print_ok($e);
+
+
+### test on failed validate if
+$v = [
+{
+  'general no_extra_fields' => 'all',
+  'group validate_if' => 'baz',
+  foo => {max_len => 10},
+},
+];
+
+$e = &validate({}, $v);
+&print_ok(! $e);
+
+$e = &validate({foo => "foo"}, $v);
+&print_ok(! $e);
+
+$e = &validate({foo => "foo", bar => "bar"}, $v);
+&print_ok(! $e);
+
+$e = &validate({bar => "bar"}, $v);
+&print_ok(! $e);
+
+### test on successful validate if
+$v = [
+{
+  'general no_extra_fields' => 'all',
+  'group validate_if' => 'baz',
+  foo => {max_len => 10},
+  baz => {max_len => 10},
+},
+];
+
+$e = &validate({baz => 1}, $v);
+&print_ok(! $e);
+
+$e = &validate({baz => 1, foo => "foo"}, $v);
+&print_ok(! $e);
+
+$e = &validate({baz => 1, foo => "foo", bar => "bar"}, $v);
+&print_ok($e);
+
+$e = &validate({baz => 1, bar => "bar"}, $v);
+&print_ok($e);
+
+### test on multiple groups, some with validate if
+$v = [
+{
+  'general no_extra_fields' => 'all',
+  'group validate_if' => 'baz',
+  foo => {max_len => 10},
+  baz => {max_len => 10},
+},
+{
+  'group validate_if' => 'hem',
+  haw => {max_len => 10},
+},
+];
+
+$e = &validate({haw => 1, baz => 1}, $v);
+&print_ok(! $e);
+
+$e = &validate({haw => 1, baz => 1, foo => "foo"}, $v);
+&print_ok(! $e);
+
+$e = &validate({haw => 1, baz => 1, foo => "foo", bar => "bar"}, $v);
+&print_ok($e);
+
+$e = &validate({haw => 1, baz => 1, bar => "bar"}, $v);
+&print_ok($e);
+
+
+### test on multiple groups, some with validate if
+$v = [
+{
+  'general no_extra_fields' => 'used',
+  'group validate_if' => 'baz',
+  foo => {max_len => 10},
+  baz => {max_len => 10},
+},
+{
+  'group validate_if' => 'hem',
+  haw => {max_len => 10},
+},
+];
+
+$e = &validate({haw => 1, baz => 1}, $v);
+&print_ok($e);
+
+$e = &validate({haw => 1, baz => 1, foo => "foo"}, $v);
+&print_ok($e);
+
+$e = &validate({haw => 1, baz => 1, foo => "foo", bar => "bar"}, $v);
+&print_ok($e);
+
+$e = &validate({haw => 1, baz => 1, bar => "bar"}, $v);
+&print_ok($e);
+
+__DATA__
diff --git a/t/1_validate_12_change.t b/t/1_validate_12_change.t
new file mode 100644 (file)
index 0000000..e577b0d
--- /dev/null
@@ -0,0 +1,81 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+### determine number of tests
+seek(DATA,0,0);
+my $prog  = join "", <DATA>;
+my @tests = ($prog =~ /&print_ok\(/g);
+my $tests = @tests;
+print "1..$tests\n";
+
+require CGI::Ex::Validate;
+
+my ($N, $v, $e, $ok) = (0);
+
+sub validate {
+  return scalar &CGI::Ex::Validate::validate(@_);
+}
+sub print_ok {
+  my $ok = shift;
+  $N ++;
+  warn "Test failed at line ".(caller)[2]."\n" if ! $ok;
+  print "" . ($ok ? "" : "not ") . "ok $N\n";
+}
+&print_ok(1);
+
+###----------------------------------------------------------------###
+
+$v = [
+{
+  foo => {
+    max_len => 10,
+    replace => 's/[^\d]//g',
+  },
+},
+];
+
+$e = &validate({
+  foo => '123-456-7890',
+}, $v);
+&print_ok(! $e);
+
+
+my $form = {
+  key1 => 'Bu-nch @of characte#rs^',
+  key2 => '123 456 7890',
+};
+
+
+$v = {
+  key1 => {
+    replace => 's/[^\s\w]//g',
+  },
+};
+
+$e = &validate($form, $v);
+&print_ok(! $e && $form->{key1} eq 'Bunch of characters');
+
+$v = {
+  key2 => {
+    replace => 's/(\d{3})\D*(\d{3})\D*(\d{4})/($1) $2-$3/g',
+  },
+};
+
+$e = &validate($form, $v);
+&print_ok(! $e && $form->{key2} eq '(123) 456-7890');
+
+
+$v = {
+  key2 => {
+    replace => 's/.+//g',
+    required => 1,
+  },
+};
+
+$e = &validate($form, $v);
+&print_ok($e && $form->{key2} eq '');
+
+__DATA__
diff --git a/t/1_validate_13_html_file.t b/t/1_validate_13_html_file.t
new file mode 100644 (file)
index 0000000..5596f61
--- /dev/null
@@ -0,0 +1,62 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+### determine number of tests
+seek(DATA,0,0);
+my $prog  = join "", <DATA>;
+my @tests = ($prog =~ /&print_ok\(/g);
+my $tests = @tests;
+print "1..$tests\n";
+
+require CGI::Ex::Validate;
+
+my ($N, $v, $e, $ok) = (0);
+
+sub validate {
+  return scalar &CGI::Ex::Validate::validate(@_);
+}
+sub print_ok {
+  my $ok = shift;
+  $N ++;
+  warn "Test failed at line ".(caller)[2]."\n" if ! $ok;
+  print "" . ($ok ? "" : "not ") . "ok $N\n";
+}
+&print_ok(1);
+
+###----------------------------------------------------------------###
+
+### where are my samples
+my $dir = __FILE__;
+$dir =~ tr|\\|/|; # should probably use File::Spec
+$dir =~ s|[^/]+$|samples| || die "Couldn't determine dir";
+$dir =~ s|^t/|./t/|; # to satisfy conf
+
+### single group
+$v = "$dir/html1.htm";
+
+$e = &validate({}, $v);
+&print_ok($e);
+$e = &validate({user => 1}, $v);
+&print_ok(! $e);
+$e = &validate({user => 1, bar => 1}, $v);
+&print_ok($e);
+$e = &validate({user => 1, bar => 1, foo => 1}, $v);
+&print_ok(! $e);
+
+
+### three groups, some with validate_if's - using arrayref
+$v = "$dir/html2.htm";
+
+$e = &validate({}, $v);
+&print_ok($e);
+$e = &validate({user => 1}, $v);
+&print_ok(! $e);
+$e = &validate({user => 1, bar => 1}, $v);
+&print_ok($e);
+$e = &validate({user => 1, bar => 1, foo => 1}, $v);
+&print_ok(! $e);
+
+__DATA__
diff --git a/t/1_validate_14_untaint.t b/t/1_validate_14_untaint.t
new file mode 100644 (file)
index 0000000..59d256d
--- /dev/null
@@ -0,0 +1,105 @@
+#!perl -T
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+### Set up taint checking
+sub is_tainted { local $^W = 0; ! eval { eval("#" . substr(join("", @_), 0, 0)); 1 } }
+
+my $taint = join(",", $0, %ENV, @ARGV);
+if (! is_tainted($taint) && open(my $fh, "/dev/urandom")) {
+  sysread($fh, $taint, 1);
+}
+$taint = substr($taint, 0, 0);
+if (! is_tainted($taint)) {
+  print "1..1\nok 1 # skip Couldn't get any tainted data or we aren't in taint mode\n";
+  exit;
+}
+
+### make sure tainted hash values don't bleed into other values
+my $form = {};
+$form->{'foo'} = "123$taint";
+$form->{'bar'} = "456$taint";
+$form->{'baz'} = "789";
+if (!  is_tainted($form->{'foo'})
+    || is_tainted($form->{'baz'})) {
+  # untaint checking doesn't really work
+  print "1..1\nok 1 # skip Hashes with mixed taint don't work right (older perls ?)\n";
+  exit;
+}
+
+###----------------------------------------------------------------###
+### Looks good - here we go
+
+### determine number of tests
+seek(DATA,0,0);
+my $prog  = join "", <DATA>;
+my @tests = ($prog =~ /print_ok\(/g);
+my $tests = @tests;
+print "1..$tests\n";
+
+require CGI::Ex::Validate;
+
+my ($N, $v, $e, $ok) = (0);
+
+
+print_ok(is_tainted($taint));
+print_ok(is_tainted($form->{'foo'}));
+print_ok(! is_tainted($form->{'baz'}));
+print_ok(! is_tainted($form->{'non_existent_key'}));
+
+sub validate {
+  return scalar &CGI::Ex::Validate::validate(@_);
+}
+sub print_ok {
+  my $ok = shift;
+  $N ++;
+  warn "Test failed at line ".(caller)[2]."\n" if ! $ok;
+  print "" . ($ok ? "" : "not ") . "ok $N\n";
+}
+&print_ok(1);
+
+###----------------------------------------------------------------###
+
+$e = &validate($form, {
+  foo => {
+    match   => 'm/^\d+$/',
+    untaint => 1,
+  },
+});
+
+print_ok(! $e);
+print_ok(! is_tainted($form->{foo}));
+
+###----------------------------------------------------------------###
+
+$e = &validate($form, {
+  bar => {
+    match   => 'm/^\d+$/',
+  },
+});
+
+print_ok(! $e);
+print_ok(is_tainted($form->{bar}));
+
+###----------------------------------------------------------------###
+
+$e = &validate($form, {
+  bar => {
+    untaint => 1,
+  },
+});
+
+print_ok($e);
+#print $e if $e;
+print_ok(is_tainted($form->{bar}));
+
+###----------------------------------------------------------------###
+
+print_ok(!is_tainted($form->{foo}));
+print_ok( is_tainted($form->{bar}));
+print_ok(!is_tainted($form->{baz}));
+
+__DATA__
diff --git a/t/2_fill_00_base.t b/t/2_fill_00_base.t
new file mode 100644 (file)
index 0000000..bb1348f
--- /dev/null
@@ -0,0 +1,8 @@
+
+BEGIN {
+       print "1..1\n";
+}
+
+use CGI::Ex::Fill;
+
+BEGIN { print "ok 1\n"; }
diff --git a/t/2_fill_01_form.t b/t/2_fill_01_form.t
new file mode 100644 (file)
index 0000000..e6309e5
--- /dev/null
@@ -0,0 +1,30 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+print "1..2\n";
+
+use CGI::Ex;
+
+print "ok 1\n";
+
+my $hidden_form_in = '
+<INPUT TYPE="TEXT" NAME="foo1" value="nada">
+<input type="hidden" name="foo2"/>
+';
+
+my %fdat = (foo1 => 'bar1',
+            foo2 => '"bar2"');
+
+
+my $fif = new CGI::Ex;
+my $output = $fif->fill(scalarref => \$hidden_form_in,
+                       fdat      => \%fdat);
+if ($output =~ m/^\s*<input( (type="TEXT"|name="foo1"|value="bar1")){3}>\s*<input( (type="hidden"|name="foo2"|value="&quot;bar2&quot;")){3}\s*\/>\s*$/i){
+       print "ok 2\n";
+} else {
+       print "Got unexpected out for $hidden_form_in:\n$output\n";
+       print "not ok 2\n";
+}
diff --git a/t/2_fill_02_hidden.t b/t/2_fill_02_hidden.t
new file mode 100644 (file)
index 0000000..49c6c75
--- /dev/null
@@ -0,0 +1,27 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+print "1..2\n";
+
+use CGI::Ex;
+
+print "ok 1\n";
+
+my $hidden_form_in = qq{<input type="hidden" name="foo1">
+<input type="hidden" name="foo2" value="ack">};
+
+my %fdat = (foo1a => 'bar1a',
+       foo2 => ['bar2','bar3']);
+
+my $fif = new CGI::Ex;
+my $output = $fif->fill(scalarref => \$hidden_form_in,
+                       fdat => \%fdat);
+if ($output =~ m/^<input( (type="hidden"|name="foo1"|value="")){3}>\s*<input( (type="hidden"|name="foo2"|value="bar2")){3}>$/){
+       print "ok 2\n";
+} else {
+       print "Got unexpected out for hidden form:\n$output\n";
+       print "not ok 2\n";
+}
diff --git a/t/2_fill_03_checkbox.t b/t/2_fill_03_checkbox.t
new file mode 100644 (file)
index 0000000..c3ba792
--- /dev/null
@@ -0,0 +1,49 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+print "1..2\n";
+
+use CGI::Ex;
+
+print "ok 1\n";
+
+my $hidden_form_in = qq{<input type="checkbox" name="foo1" value="bar1">
+<input type="checkbox" name="foo1" value="bar2">
+<input type="checkbox" name="foo1" value="bar3">
+<input type="checkbox" name="foo2" value="bar1">
+<input type="checkbox" name="foo2" value="bar2">
+<input type="checkbox" name="foo2" value="bar3">
+<input type="checkbox" name="foo3" value="bar1">
+<input type="checkbox" name="foo3" checked value="bar2">
+<input type="checkbox" name="foo3" value="bar3">
+<input type="checkbox" name="foo4" value="bar1">
+<input type="checkbox" name="foo4" checked value="bar2">
+<input type="checkbox" name="foo4" value="bar3">
+<input type="checkbox" name="foo5">
+<input type="checkbox" name="foo6">
+<input type="checkbox" name="foo7" checked>
+<input type="checkbox" name="foo8" checked>};
+
+my %fdat = (foo1 => 'bar1',
+           foo2 => ['bar1', 'bar2',],
+          foo3 => '',
+          foo5 => 'on',
+          foo6 => '',
+          foo7 => 'on',
+          foo8 => '');
+
+my $fif = new CGI::Ex;
+my $output = $fif->fill(scalarref => \$hidden_form_in,
+                       fdat => \%fdat);
+
+my $is_checked = join(" ",map { m/checked/i ? "yes" : "no" } split ("\n",$output));
+
+if ($is_checked eq "yes no no yes yes no no no no no yes no yes no yes no"){
+       print "ok 2\n";
+} else {
+       print "Got unexpected is_checked for checkboxes:\n$is_checked\n";
+       print "not ok 2\n";
+}
diff --git a/t/2_fill_04_select.t b/t/2_fill_04_select.t
new file mode 100644 (file)
index 0000000..1b8f1d4
--- /dev/null
@@ -0,0 +1,114 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+print "1..5\n";
+
+use CGI::Ex;
+use CGI;
+
+print "ok 1\n";
+
+my $hidden_form_in = qq{<select multiple name="foo1">
+       <option value="0">bar1</option>
+       <option value="bar2">bar2</option>
+       <option value="bar3">bar3</option>
+</select>
+<select multiple name="foo2">
+       <option value="bar1">bar1</option>
+       <option value="bar2">bar2</option>
+       <option value="bar3">bar3</option>
+</select>
+<select multiple name="foo3">
+       <option value="bar1">bar1</option>
+       <option selected value="bar2">bar2</option>
+       <option value="bar3">bar3</option>
+</select>
+<select multiple name="foo4">
+       <option value="bar1">bar1</option>
+       <option selected value="bar2">bar2</option>
+       <option value="bar3">bar3</option>
+</select>};
+my $q = new CGI( { foo1 => '0',
+           foo2 => ['bar1', 'bar2',],
+          foo3 => '' }
+       );
+
+my $fif = new CGI::Ex;
+my $output = $fif->fill(scalarref => \$hidden_form_in,
+                       fobject => $q);
+
+my $is_selected = join(" ",map { m/selected/ ? "yes" : "no" } grep /option/, split ("\n",$output));
+
+if ($is_selected eq "yes no no yes yes no no no no no yes no"){
+       print "ok 2\n";
+} else {
+       print "Got unexpected is_seleced for select menus:\n$is_selected\n$output\n";
+       print "not ok 2\n";
+}
+
+$hidden_form_in = qq{<select multiple name="foo1">
+       <option>bar1</option>
+       <option>bar2</option>
+       <option>bar3</option>
+</select>
+<select multiple name="foo2">
+       <option> bar1</option>
+       <option> bar2</option>
+       <option>bar3</option>
+</select>
+<select multiple name="foo3">
+       <option>bar1</option>
+       <option selected>bar2</option>
+       <option>bar3</option>
+</select>
+<select multiple name="foo4">
+       <option>bar1</option>
+       <option selected>bar2</option>
+       <option>bar3  </option>
+</select>};
+
+$q = new CGI( { foo1 => 'bar1',
+           foo2 => ['bar1', 'bar2',],
+          foo3 => '' }
+       );
+
+$fif = new CGI::Ex;
+$output = $fif->fill(scalarref => \$hidden_form_in,
+                       fobject => $q);
+
+$is_selected = join(" ",map { m/selected/ ? "yes" : "no" } grep /option/, split ("\n",$output));
+
+if ($is_selected eq "yes no no yes yes no no no no no yes no"){
+       print "ok 3\n";
+} else {
+       print "Got unexpected is_seleced for select menus:\n$is_selected\n$output\n";
+       print "not ok 3\n";
+}
+
+# test empty option tag
+
+$hidden_form_in = qq{<select name="x"><option></select>};
+$fif = new CGI::Ex;
+$output = $fif->fill(scalarref => \$hidden_form_in,
+                       fobject => $q);
+if ($output eq qq{<select name="x"><option></select>}){
+       print "ok 4\n";
+} else {
+       print "Got unexpected output for empty option:\n$output\n";
+       print "not ok 4\n";
+}
+
+$hidden_form_in = qq{<select name="foo1"><option><option value="bar1"></select>};
+$fif = new CGI::Ex;
+$output = $fif->fill(scalarref => \$hidden_form_in,
+                       fobject => $q);
+if ($output =~ m!^<select name="foo1"><option><option( selected(="selected")?| value="bar1"){2}></select>$!){
+       print "ok 5\n";
+} else {
+       print "Got unexpected output for empty option:\n$output\n";
+       print "not ok 5\n";
+}
+
diff --git a/t/2_fill_05_textarea.t b/t/2_fill_05_textarea.t
new file mode 100644 (file)
index 0000000..9aa7e59
--- /dev/null
@@ -0,0 +1,39 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+print "1..3\n";
+
+use CGI::Ex;
+
+print "ok 1\n";
+
+my $hidden_form_in = qq{<TEXTAREA NAME="foo">blah</TEXTAREA>};
+
+my %fdat = (foo => 'bar>bar');
+
+my $fif = new CGI::Ex;
+my $output = $fif->fill(scalarref => \$hidden_form_in,
+                       fdat => \%fdat);
+if ($output eq '<TEXTAREA NAME="foo">bar&gt;bar</TEXTAREA>'){
+       print "ok 2\n";
+} else {
+       print "Got unexpected out for $hidden_form_in:\n$output\n";
+       print "not ok 2\n";
+}
+
+# empty fdat test
+
+%fdat = (foo => '');
+
+$fif = new CGI::Ex;
+$output = $fif->fill(scalarref => \$hidden_form_in,
+                       fdat => \%fdat);
+if ($output eq '<TEXTAREA NAME="foo"></TEXTAREA>'){
+       print "ok 3\n";
+} else {
+       print "Got unexpected out for $hidden_form_in:\n$output\n";
+       print "not ok 3\n";
+}
diff --git a/t/2_fill_06_radio.t b/t/2_fill_06_radio.t
new file mode 100644 (file)
index 0000000..53ba4ad
--- /dev/null
@@ -0,0 +1,29 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+print "1..2\n";
+
+use CGI::Ex;
+
+print "ok 1\n";
+
+my $hidden_form_in = qq{<INPUT TYPE="radio" NAME="foo1" value="bar1">
+<input type="radio" name="foo1" value="bar2">
+<input type="radio" name="foo1" value="bar3">
+<input type="radio" name="foo1" checked value="bar4">};
+
+my %fdat = (foo1 => 'bar2');
+
+my $fif = new CGI::Ex;
+my $output = $fif->fill(scalarref => \$hidden_form_in,
+                       fdat => \%fdat);
+my $is_checked = join(" ",map { m/checked/ ? "yes" : "no" } split ("\n",$output));
+if ($is_checked eq 'no yes no no'){
+       print "ok 2\n";
+} else {
+       print "Got unexpected is_checked:\n$is_checked\n";
+       print "not ok 2\n";
+}
diff --git a/t/2_fill_07_reuse.t b/t/2_fill_07_reuse.t
new file mode 100644 (file)
index 0000000..56d97ea
--- /dev/null
@@ -0,0 +1,29 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+print "1..2\n";
+
+use CGI::Ex;
+
+print "ok 1\n";
+
+my $hidden_form_in = qq{<INPUT TYPE="TEXT" NAME="foo1" value="nada">
+<input type="hidden" name="foo2">};
+
+my %fdat = (foo1 => ['bar1'],
+       foo2 => 'bar2');
+
+my $fif = new CGI::Ex;
+my $output = $fif->fill(scalarref => \$hidden_form_in,
+                       fdat => \%fdat);
+my $output2 = $fif->fill(scalarref => \$output,
+                       fdat => \%fdat);
+if ($output2 =~ m/^<input( (type="TEXT"|name="foo1"|value="bar1")){3}>\s*<input( (type="hidden"|name="foo2"|value="bar2")){3}>$/i){
+       print "ok 2\n";
+} else {
+       print "Got unexpected out for $hidden_form_in:\n$output2\n";
+       print "not ok 2\n";
+}
diff --git a/t/2_fill_08_multiple_objects.t b/t/2_fill_08_multiple_objects.t
new file mode 100644 (file)
index 0000000..c26cf13
--- /dev/null
@@ -0,0 +1,31 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+print "1..2\n";
+
+use CGI::Ex;
+use CGI;
+
+print "ok 1\n";
+
+my $hidden_form_in = qq{<INPUT TYPE="TEXT" NAME="foo1" value="nada">
+<input type="hidden" name="foo2">};
+
+my %fdat = (foo1 => 'bar1',
+       foo2 => 'bar2');
+
+my $q1 = new CGI( { foo1 => 'bar1' });
+my $q2 = new CGI( { foo2 => 'bar2' });
+
+my $fif = new CGI::Ex;
+my $output = $fif->fill(scalarref => \$hidden_form_in,
+                       fobject => [$q1, $q2]);
+if ($output =~ m/^<input( (type="TEXT"|name="foo1"|value="bar1")){3}>\s*<input( (type="hidden"|name="foo2"|value="bar2")){3}>$/i){
+       print "ok 2\n";
+} else {
+       print "Got unexpected out for $hidden_form_in:\n$output\n";
+       print "not ok 2\n";
+}
diff --git a/t/2_fill_09_default_type.t b/t/2_fill_09_default_type.t
new file mode 100644 (file)
index 0000000..5db1f59
--- /dev/null
@@ -0,0 +1,27 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+print "1..2\n";
+
+use CGI::Ex;
+
+print "ok 1\n";
+
+my $hidden_form_in = qq{<INPUT NAME="foo1" value="nada">
+<input type="hidden" name="foo2">};
+
+my %fdat = (foo1 => 'bar1',
+       foo2 => 'bar2');
+
+my $fif = new CGI::Ex;
+my $output = $fif->fill(scalarref => \$hidden_form_in,
+                       fdat => \%fdat);
+if ($output =~ m/^<input( (name="foo1"|value="bar1")){2}>\s*<input( (type="hidden"|name="foo2"|value="bar2")){3}>$/i){
+       print "ok 2\n";
+} else {
+       print "Got unexpected out for $hidden_form_in:\n$output\n";
+       print "not ok 2\n";
+}
diff --git a/t/2_fill_10_escape.t b/t/2_fill_10_escape.t
new file mode 100644 (file)
index 0000000..fbacf04
--- /dev/null
@@ -0,0 +1,43 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+print "1..1\n";
+use CGI::Ex;
+my $html =<<"__HTML__";
+<HTML>
+<BODY>
+<FORM action="test.cgi" method="POST">
+<INPUT type="hidden" name="hidden" value="&gt;&quot;">
+<INPUT type="text" name="text" value="&lt;&gt;&quot;&otilde;"><BR>
+<INPUT type="radio" name="radio" value="&quot;&lt;&gt;">test<BR>
+<INPUT type="checkbox" name="checkbox" value="&quot;&lt;&gt;">test<BR>
+<INPUT type="checkbox" name="checkbox" value="&quot;&gt;&lt;&gt;">test<BR>
+<SELECT name="select">
+<OPTION value="&lt;&gt;">&lt;&gt;
+<OPTION value="&gt;&gt;">&gt;&gt;
+<OPTION value="&otilde;">&lt;&lt;
+<OPTION>&gt;&gt;&gt;
+</SELECT><BR>
+<TEXTAREA name="textarea" rows="5">&lt;&gt;&quot;</TEXTAREA><P>
+<INPUT type="submit" value=" OK ">
+</FORM>
+</BODY>
+</HTML>
+__HTML__
+
+my %fdat = ();
+
+my $fif = CGI::Ex->new;
+my $output = $fif->fill(scalarref => \$html,
+                       fdat => \%fdat);
+
+# FIF changes order of HTML attributes, so split strings and sort
+my $strings_output = join("\n", sort split(/[\s><]+/, lc($output)));
+my $strings_html = join("\n", sort split(/[\s><]+/, lc($html)));
+
+unless ($strings_output eq $strings_html){
+       print "not ";
+}
+print "ok 1";
diff --git a/t/2_fill_11_target.t b/t/2_fill_11_target.t
new file mode 100644 (file)
index 0000000..4a270de
--- /dev/null
@@ -0,0 +1,38 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+use Test;
+BEGIN { plan tests => 3 }
+
+use CGI::Ex;
+
+my $form = <<EOF;
+<FORM name="foo1">
+<INPUT TYPE="TEXT" NAME="foo1" value="nada">
+</FORM>
+<FORM name="foo2">
+<INPUT TYPE="TEXT" NAME="foo2" value="nada">
+</FORM>
+<FORM>
+<INPUT TYPE="TEXT" NAME="foo3" value="nada">
+</FORM>
+EOF
+  ;
+  
+my %fdat = (
+  foo1 => 'bar1',
+  foo2 => 'bar2',
+  foo3 => 'bar3',
+);
+
+my $fif = new CGI::Ex;
+my $output = $fif->fill(
+  scalarref => \$form,
+  fdat => \%fdat,
+  target => 'foo2',
+);
+
+my @v = $output =~ m/<input .*?value="(.*?)"/ig;
+ok($v[0], 'nada');
+ok($v[1], 'bar2');
+ok($v[2], 'nada');
diff --git a/t/2_fill_12_mult.t b/t/2_fill_12_mult.t
new file mode 100644 (file)
index 0000000..91755f5
--- /dev/null
@@ -0,0 +1,37 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+print "1..3\n";
+
+use CGI::Ex;
+
+print "ok 1\n";
+
+my $hidden_form_in = qq{<INPUT TYPE="TEXT" NAME="foo1" value="cat1">
+<input type="text" name="foo1" value="cat2"/>};
+
+my %fdat = (foo1 => ['bar1','bar2']);
+
+my $fif = new CGI::Ex;
+my $output = $fif->fill(scalarref => \$hidden_form_in,
+                       fdat => \%fdat);
+if ($output =~ m/^<input( (type="TEXT"|name="foo1"|value="bar1")){3}>\s*<input( (type="text"|name="foo1"|value="bar2")){3}\s*\/>$/i){
+       print "ok 2\n";
+} else {
+       print "Got unexpected out for $hidden_form_in:\n$output\n";
+       print "not ok 2\n";
+}
+
+%fdat = (foo1 => ['bar1']);
+
+$output = $fif->fill(scalarref => \$hidden_form_in,
+                       fdat => \%fdat);
+if ($output =~ m/^<input( (type="TEXT"|name="foo1"|value="bar1")){3}>\s*<input( (type="text"|name="foo1"|value="")){3}\s*\/>$/i){
+       print "ok 3\n";
+} else {
+       print "Got unexpected out for $hidden_form_in:\n$output\n";
+       print "not ok 3\n";
+}
diff --git a/t/2_fill_13_warning.t b/t/2_fill_13_warning.t
new file mode 100644 (file)
index 0000000..882df15
--- /dev/null
@@ -0,0 +1,25 @@
+# -*- Mode: Perl; -*-
+
+#!/usr/bin/perl -w
+
+# emits warnings for HTML::FIF <= 0.22
+
+use CGI qw(:no_debug);
+use CGI::Ex;
+use Test;
+
+BEGIN { plan tests => 1 }
+
+local $/;
+my $html = qq{<input type="submit" value="Commit">};
+my $q = new CGI;
+$q->param( "name", "John Smith" );
+my $fif = new CGI::Ex;
+my $output = $fif->fill(
+                         scalarref => \$html,
+                         fobject   => $q
+);
+
+ok($html =~ m!<input( type="submit"| value="Commit"){2}>!);
diff --git a/t/2_fill_14_password.t b/t/2_fill_14_password.t
new file mode 100644 (file)
index 0000000..ddb56f6
--- /dev/null
@@ -0,0 +1,39 @@
+# -*- Mode: Perl; -*-
+
+#!/usr/bin/perl -w
+
+use CGI qw(:no_debug);
+use CGI::Ex;
+use Test;
+
+BEGIN { plan tests => 2 }
+
+local $/;
+my $html = qq{<input type="password" name="foo">};
+my $q = new CGI;
+$q->param( foo => 'bar' );
+
+{
+    my $fif = new CGI::Ex;
+    my $output = $fif->fill(
+       scalarref => \$html,
+       fobject   => $q,
+       fill_password => 0,
+    );
+
+    ok($output !~ /value="bar"/);
+}
+
+
+{
+    my $fif = new CGI::Ex;
+    my $output = $fif->fill(
+       scalarref => \$html,
+       fobject   => $q,
+#      fill_password => 1,
+    );
+
+    ok($output =~ /value="bar"/);
+}
+
+
diff --git a/t/2_fill_15_multiple_fields.t b/t/2_fill_15_multiple_fields.t
new file mode 100644 (file)
index 0000000..2fd7e86
--- /dev/null
@@ -0,0 +1,25 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+print "1..2\n";
+
+use CGI::Ex;
+print "ok 1\n";
+
+my $hidden_form_in = qq{<input type="hidden" name="foo">
+<input type="hidden" name="foo" value="ack">};
+
+my %fdat = (foo => 'bar1a');
+
+my $fif = new CGI::Ex;
+my $output = $fif->fill(scalarref => \$hidden_form_in,
+                       fdat => \%fdat);
+if ($output =~ m/^<input( (type="hidden"|name="foo"|value="bar1a")){3}>\s*<input( (type="hidden"|name="foo"|value="bar1a")){3}>$/){
+       print "ok 2\n";
+} else {
+       print "Got unexpected out for hidden form:\n$output\n";
+       print "not ok 2\n";
+}
diff --git a/t/2_fill_16_ignore_fields.t b/t/2_fill_16_ignore_fields.t
new file mode 100644 (file)
index 0000000..56ffe44
--- /dev/null
@@ -0,0 +1,52 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+print "1..2\n";
+
+use CGI::Ex;
+use CGI;
+
+print "ok 1\n";
+
+my $hidden_form_in = qq{<select multiple name="foo1">
+       <option value="0">bar1</option>
+       <option value="bar2">bar2</option>
+       <option value="bar3">bar3</option>
+</select>
+<select multiple name="foo2">
+       <option value="bar1">bar1</option>
+       <option value="bar2">bar2</option>
+       <option value="bar3">bar3</option>
+</select>
+<select multiple name="foo3">
+       <option value="bar1">bar1</option>
+       <option selected value="bar2">bar2</option>
+       <option value="bar3">bar3</option>
+</select>
+<select multiple name="foo4">
+       <option value="bar1">bar1</option>
+       <option selected value="bar2">bar2</option>
+       <option value="bar3">bar3</option>
+</select>};
+my $q = new CGI( { foo1 => '0',
+           foo2 => ['bar1', 'bar2',],
+          foo3 => '' }
+       );
+
+my $fif = new CGI::Ex;
+my $output = $fif->fill(scalarref => \$hidden_form_in,
+                       fobject => $q,
+                       ignore_fields => ['asdf','foo1','asdf']);
+
+my $is_selected = join(" ",map { m/selected/ ? "yes" : "no" } grep /option/, split ("\n",$output));
+
+if ($is_selected eq "no no no yes yes no no no no no yes no"){
+       print "ok 2\n";
+} else {
+       print "Got unexpected is_seleced for select menus:\n$is_selected\n$output\n";
+       print "not ok 2\n";
+}
+
diff --git a/t/2_fill_17_xhtml.t b/t/2_fill_17_xhtml.t
new file mode 100644 (file)
index 0000000..5fde93b
--- /dev/null
@@ -0,0 +1,49 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+print "1..1\n";
+
+use CGI::Ex;
+use CGI;
+
+my $html = <<EOF;
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+        "http://www.w3.org/TR/1999/REC-html401-19991224/loose.dtd">
+<html>
+<body>
+    <input type="radio" name="status" value=0 />Canceled<br>
+    <input type="radio" name="status" value=1 />Confirmed<br>
+    <input type="radio" name="status" value=2 />Wait List<br>
+
+    <input type="radio" name="status" value=3 />No Show<br>
+    <input type="radio" name="status" value=4 />Moved to Another Class<br>
+    <input type="radio" name="status" value=5 />Late Cancel<br>
+</body>
+</html>
+EOF
+
+my $q = CGI->new;
+$q->param('status', 1 );
+
+my $fif = CGI::Ex->new;
+
+my $output = $fif->fill(
+    scalarref => \$html,
+    fobject => $q
+);
+
+my $matches;
+while ($output =~ m!( />)!g) {
+  $matches++;
+}
+
+if ($matches == 6) {
+  print "ok 1\n";
+} else {
+  print "not ok 1\n";
+}
+
+print $output;
diff --git a/t/2_fill_18_coderef.t b/t/2_fill_18_coderef.t
new file mode 100644 (file)
index 0000000..f09b3b0
--- /dev/null
@@ -0,0 +1,39 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+print "1..4\n";
+
+use CGI::Ex;
+
+print "ok 1\n";
+
+my $ok2 = 0;
+my $ok3 = 0;
+
+my $hidden_form_in = qq{<input type="hidden" name="foo1">
+<input type="hidden" name="foo2" value="ack">};
+
+my %fdat = (foo1 => sub { $ok2 ++; return 'bar1' },
+            );
+my $cdat = sub {
+  $ok3 ++;
+  my $key = shift;
+  return ($key eq 'foo2') ? 'bar2' : '';
+};
+
+my $fif = new CGI::Ex;
+my $output = $fif->fill(scalarref => \$hidden_form_in,
+                       fdat => [\%fdat, $cdat]);
+
+print "" . ($ok2 ? "" : "not ") . "ok 2\n";
+print "" . ($ok3 ? "" : "not ") . "ok 3\n";
+
+if ($output =~ m/^<input( (type="hidden"|name="foo1"|value="bar1")){3}>\s*<input( (type="hidden"|name="foo2"|value="bar2")){3}>$/){
+  print "ok 4\n";
+} else {
+  print "Got unexpected out for hidden form:\n$output\n";
+  print "not ok 4\n";
+}
diff --git a/t/2_fill_19_complex.t b/t/2_fill_19_complex.t
new file mode 100644 (file)
index 0000000..7a86735
--- /dev/null
@@ -0,0 +1,30 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+
+$^W = 1;
+
+print "1..2\n";
+
+use CGI::Ex;
+
+print "ok 1\n";
+
+my $string = qq{
+<input attr="<br value='waw'>
+<br>" type="hidden" name="foo1">
+};
+
+my %fdat = (foo1 => 'bar1');
+
+
+my $cgix = new CGI::Ex;
+$cgix->fill(text => \$string,
+            form => \%fdat,
+            );
+
+if ($string =~ m/ value="bar1"/) {
+  print "ok 2\n";
+} else {
+  print "not ok 2\n";
+}
diff --git a/t/2_fill_20_switcharoo.t b/t/2_fill_20_switcharoo.t
new file mode 100644 (file)
index 0000000..fac4441
--- /dev/null
@@ -0,0 +1,132 @@
+# -*- Mode: Perl; -*-
+
+use strict;
+$^W = 1;
+print "1..27\n";
+use CGI::Ex;
+print "ok 1\n";
+
+
+my $string;
+my %fdat = (foo1 => 'bar1');
+my $cgix = new CGI::Ex;
+my $n    = 1;
+my $dook = sub {
+  $n ++;
+  print "$n - ($string)\n";
+  my @a;
+  if ($string =~ m/ value=([\"\'])bar1\1/i
+      && 1 == scalar(@a=$string =~ m/(value)/gi)) {
+    print "ok $n\n";
+  } else {
+    print "not ok $n\n";
+  }
+};
+
+###----------------------------------------------------------------###
+
+$string = qq{<input name="foo1">};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<input name=foo1>};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<input name=foo1 />};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<input value name="foo1">};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<input value value name="foo1">};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<input value value="" name="foo1">};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<input grrr name="foo1" value="">};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<input value= name="foo1">};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<input type=hidden value= name="foo1">};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<input value= type="hidden" name="foo1">};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<input value="" name="foo1">};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<input value='' name="foo1">};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<input value='one' name="foo1">};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<input Value="one" name="foo1">};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<input VALUE="one" name="foo1">};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<input name="foo1" value="one">};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<INPUT NAME="foo1" VALUE="one">};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<INPUT NAME="foo1" VALUE="one" >};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<INPUT NAME="foo1" VALUE="" >};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<INPUT NAME="foo1" VALUE= >};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<INPUT NAME="foo1" VALUE >};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<INPUT NAME="foo1" VALUE />};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<INPUT NAME="foo1" VALUE= />};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<INPUT NAME="foo1" VALUE="" />};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<INPUT NAME="foo1" VALUE="one" />};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+$string = qq{<INPUT NAME="foo1" VALUE="one" />};
+$cgix->fill(text => \$string, form => \%fdat);
+&$dook();
+
+
diff --git a/t/3_conf_00_base.t b/t/3_conf_00_base.t
new file mode 100644 (file)
index 0000000..31591c0
--- /dev/null
@@ -0,0 +1,69 @@
+# -*- Mode: Perl; -*-
+
+use Test;
+
+BEGIN {plan tests => 24};
+
+use CGI::Ex::Conf;
+ok(1);
+
+my $dir = $0;
+$dir =~ tr|\\|/|; # should probably use File::Spec
+$dir =~ s|/[^/]+$||;
+$dir = '.' if ! length $dir;
+$dir .= '/samples';
+my $obj = CGI::Ex::Conf->new({
+  paths => ["$dir/conf_path_1", "$dir/conf_path_3"],
+});
+
+### most test for the reading of files
+### are actually performed in the validation tests
+
+ok($obj);
+
+my $hash = $obj->read('apples.pl');
+ok($hash);
+ok($hash->{quantity});
+
+$hash = $obj->read('apples.pl');
+ok($hash);
+ok($hash->{quantity});
+
+
+local $CGI::Ex::Conf::DIRECTIVE = 'FIRST';
+$hash = $obj->read('apples.pl');
+ok($hash);
+ok($hash->{quantity} == 20);
+ok($hash->{foo} eq 'file1');
+
+local $CGI::Ex::Conf::DIRECTIVE = 'LAST';
+$hash = $obj->read('apples.pl');
+ok($hash);
+ok($hash->{quantity} == 30);
+ok($hash->{foo} eq 'file2');
+
+$hash = $obj->read('apples.pl', {directive => 'MERGE'});
+ok($hash);
+ok($hash->{quantity} == 30);
+ok($hash->{foo} eq 'file1'); # has immutable value
+
+
+local $obj->{directive} = 'FIRST';
+$hash = $obj->read('oranges.pl');
+ok($hash);
+ok($hash->{quantity} == 20);
+ok($hash->{foo} eq 'file1');
+
+local $obj->{directive} = 'LAST';
+$hash = $obj->read('oranges.pl');
+ok($hash);
+ok($hash->{quantity} == 30);
+ok($hash->{foo} eq 'file2');
+
+local $obj->{directive} = 'MERGE';
+$hash = $obj->read('oranges.pl');
+ok($hash);
+ok($hash->{quantity} == 20); # has immutable key so all values are immutable
+ok($hash->{foo} eq 'file1'); # has immutable key so all values are immutable
+
+
diff --git a/t/3_conf_01_write.t b/t/3_conf_01_write.t
new file mode 100644 (file)
index 0000000..d77c83a
--- /dev/null
@@ -0,0 +1,71 @@
+# -*- Mode: Perl; -*-
+
+use Test;
+
+BEGIN {plan tests => 12};
+
+use CGI::Ex::Conf;
+ok(1);
+
+my $dir = $0;
+$dir =~ tr|\\|/|; # should probably use File::Spec
+$dir =~ s|/[^/]+$||;
+$dir = '.' if ! length $dir;
+$dir .= '/samples';
+my $obj = CGI::Ex::Conf->new({
+  paths => ["$dir/conf_path_1", "$dir/conf_path_3"],
+});
+
+my $tmpfile = "$obj->{paths}->[0]/write_test";
+### most test for the reading of files
+### are actually performed in the validation tests
+
+ok($obj);
+
+my $hash = {
+  one => 1,
+  two => 2,
+  three => {
+    foo => 'Foo',
+    bar => 'Bar',
+  },
+};
+
+my $file = $tmpfile .'.yaml';
+ok( eval { $obj->write_ref($file, $hash) } );
+my $in = $obj->read_ref($file);
+ok($in->{'three'}->{'foo'} eq 'Foo');
+unlink $file;
+
+$file = $tmpfile .'.sto';
+ok( eval { $obj->write_ref($file, $hash) } );
+$in = $obj->read_ref($file);
+ok($in->{'three'}->{'foo'} eq 'Foo');
+unlink $file;
+
+$file = $tmpfile .'.pl';
+ok( eval { $obj->write_ref($file, $hash) } );
+$in = $obj->read_ref($file);
+ok($in->{'three'}->{'foo'} eq 'Foo');
+unlink $file;
+
+#$file = $tmpfile .'.xml';
+#ok( eval { $obj->write_ref($file, $hash) } );
+#$in = $obj->read_ref($file);
+#ok($in->{'three'}->{'foo'} eq 'Foo');
+#unlink $file;
+#
+#### ini likes hash O' hashes
+#$hash->{'one'} = {};
+#$hash->{'two'} = {};
+#$file = $tmpfile .'.ini';
+#ok( eval { $obj->write_ref($file, $hash) } );
+#$in = $obj->read_ref($file);
+#ok($in->{'three'}->{'foo'} eq 'Foo');
+#unlink $file;
+
+ok (eval { $obj->write('FooSpace', $hash) });
+ok (unlink $obj->{'paths'}->[1] . '/FooSpace.conf');
+
+ok (eval { $obj->write('FooSpace', $hash, {directive => 'FIRST'}) });
+ok (unlink $obj->{'paths'}->[0] . '/FooSpace.conf');
diff --git a/t/4_app_00_base.t b/t/4_app_00_base.t
new file mode 100644 (file)
index 0000000..2e3170a
--- /dev/null
@@ -0,0 +1,12 @@
+# -*- Mode: Perl; -*-
+
+use Test;
+
+BEGIN {plan tests => 2};
+
+use CGI::Ex::App;
+ok(1);
+
+my $obj = CGI::Ex::App->new({
+});
+ok($obj);
diff --git a/t/5_dump_00_base.t b/t/5_dump_00_base.t
new file mode 100644 (file)
index 0000000..50cd4a2
--- /dev/null
@@ -0,0 +1,9 @@
+# -*- Mode: Perl; -*-
+
+use Test;
+
+BEGIN {plan tests => 1};
+
+use CGI::Ex::Dump ();
+ok(1);
+
diff --git a/t/6_die_00_base.t b/t/6_die_00_base.t
new file mode 100644 (file)
index 0000000..cca35e7
--- /dev/null
@@ -0,0 +1,13 @@
+# -*- Mode: Perl; -*-
+
+use Test;
+
+BEGIN {plan tests => 2};
+
+use CGI::Ex::Die;
+ok(1);
+
+ok(eval {
+  import CGI::Ex::Die register => 1;
+  $SIG{__DIE__} eq \&CGI::Ex::Die::die_handler;
+});
diff --git a/t/samples/bench_cgix_hfif.pl b/t/samples/bench_cgix_hfif.pl
new file mode 100755 (executable)
index 0000000..081f9ec
--- /dev/null
@@ -0,0 +1,106 @@
+#!/usr/bin/perl -w
+
+# [pauls@localhost lib]$ perl ../t/samples/bench_cgix_hfif.pl
+# Benchmark: timing 1000 iterations of cgix_func, cgix_meth, hfif...
+#  cgix_func:  1 wallclock secs ( 1.41 usr +  0.01 sys =  1.42 CPU) @ 704.23/s (n=1000)
+#  cgix_meth:  2 wallclock secs ( 1.47 usr +  0.00 sys =  1.47 CPU) @ 680.27/s (n=1000)
+#  hfif:  8 wallclock secs ( 8.34 usr +  0.04 sys =  8.38 CPU) @ 119.33/s (n=1000)
+#            Rate      hfif cgix_meth cgix_func
+# hfif      119/s        --      -82%      -83%
+# cgix_meth 680/s      470%        --       -3%
+# cgix_func 704/s      490%        4%        --
+
+use strict;
+
+use Benchmark qw(cmpthese);
+use HTML::FillInForm;
+use CGI::Ex;
+
+my $n = 1000;
+
+my $t = q{
+
+<!-- This is another thing -->
+<html>
+<form name=foo>
+
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
+
+<input type=text name=foo value="wow">
+
+<input type=password name="pass" value="">
+
+<select name=garbage>
+  <option value=lid>Lid</option>
+  <option value=can>Can</option>
+  <option value=wheel>Wheel</option>
+  <option value=truck>Truck</option>
+</select>
+
+<!-- </form> -->
+
+<textarea name=Mighty></textarea>
+
+</form>
+
+</html>
+};
+
+my $form = {
+  foo => "bar",
+  pass => "word",
+  garbage => ['can','lid'],
+  Mighty  => 'ducks',
+};
+
+
+my $fif = HTML::FillInForm->new;
+my $fo  = CGI::Ex->new;
+$fo->{remove_comments} = 1;
+
+my $x = $fo->fill(scalarref => \$t,
+                  fdat => $form,
+                  target => 'foo',
+                  );
+#print $x;
+#exit;
+
+cmpthese($n, {
+  hfif => sub {
+    my $copy = $t;
+    my $new = $fif->fill(scalarref => \$copy,
+                         fdat => $form,
+                         target => 'foo',
+                         );
+  },
+  cgix_meth => sub {
+    my $copy = $t;
+    $fo->fill(scalarref => \$copy,
+              fdat => $form,
+              target => 'foo',
+              );
+  },
+  cgix_func => sub {
+    my $copy = $t;
+    &CGI::Ex::Fill::form_fill(\$copy, $form, 'foo');
+  },
+});
diff --git a/t/samples/bench_conf_readers.pl b/t/samples/bench_conf_readers.pl
new file mode 100644 (file)
index 0000000..4a6e319
--- /dev/null
@@ -0,0 +1,402 @@
+#!/usr/bin/perl -w
+
+use strict;
+use vars qw($PLACEHOLDER);
+use Benchmark qw(cmpthese);
+use CGI::Ex::Conf;
+use POSIX qw(tmpnam);
+
+$PLACEHOLDER = chr(186).'~'.chr(186);
+
+my $n = -2;
+
+my $cob   = CGI::Ex::Conf->new;
+my %files = ();
+
+###----------------------------------------------------------------###
+
+#           Rate  yaml2   yaml    xml    ini g_conf     pl    sto   sto2  yaml3
+#yaml2     159/s     --    -1%   -72%   -80%   -91%   -95%   -98%   -98%  -100%
+#yaml      160/s     1%     --   -72%   -80%   -91%   -95%   -98%   -98%  -100%
+#xml       565/s   255%   253%     --   -28%   -68%   -84%   -93%   -94%  -100%
+#ini       785/s   393%   391%    39%     --   -55%   -78%   -90%   -91%   -99%
+#g_conf   1756/s  1004%   998%   211%   124%     --   -50%   -78%   -80%   -98%
+#pl       3524/s  2115%  2103%   524%   349%   101%     --   -55%   -61%   -97%
+#sto      7838/s  4826%  4799%  1288%   898%   346%   122%     --   -12%   -93%
+#sto2     8924/s  5508%  5477%  1480%  1037%   408%   153%    14%     --   -92%
+#yaml3  113328/s 71115% 70730% 19961% 14336%  6353%  3116%  1346%  1170%     -- #memory
+
+my $str = '{
+  foo     => {key1 => "bar",   key2 => "ralph"},
+  pass    => {key1 => "word",  key2 => "ralph"},
+  garbage => {key1 => "can",   key2 => "ralph"},
+  mighty  => {key1 => "ducks", key2 => "ralph"},
+  quack   => {key1 => "moo",   key2 => "ralph"},
+  one1    => {key1 => "val1",  key2 => "ralph"},
+  one2    => {key1 => "val2",  key2 => "ralph"},
+  one3    => {key1 => "val3",  key2 => "ralph"},
+  one4    => {key1 => "val4",  key2 => "ralph"},
+  one5    => {key1 => "val5",  key2 => "ralph"},
+  one6    => {key1 => "val6",  key2 => "ralph"},
+  one7    => {key1 => "val7",  key2 => "ralph"},
+  one8    => {key1 => "val8",  key2 => "ralph"},
+}';
+
+###----------------------------------------------------------------###
+
+#           Rate   yaml  yaml2    xml g_conf     pl    sto   sto2  yaml3
+#yaml      431/s     --    -2%   -61%   -91%   -94%   -97%   -98%  -100%
+#yaml2     438/s     2%     --   -60%   -91%   -94%   -97%   -98%  -100%
+#xml      1099/s   155%   151%     --   -78%   -85%   -92%   -94%   -99%
+#g_conf   4990/s  1057%  1038%   354%     --   -33%   -64%   -72%   -96%
+#pl       7492/s  1637%  1609%   582%    50%     --   -46%   -58%   -93%
+#sto     13937/s  3130%  3078%  1169%   179%    86%     --   -22%   -88%
+#sto2    17925/s  4055%  3988%  1532%   259%   139%    29%     --   -84%
+#yaml3  114429/s 26423% 25996% 10316%  2193%  1427%   721%   538%     -- # memory
+
+#$str = '{
+#  foo     => "bar",
+#  pass    => "word",
+#  garbage => "can",
+#  mighty  => "ducks",
+#  quack   => "moo",
+#  one1    => "val1",
+#  one2    => "val2",
+#  one3    => "val3",
+#  one4    => "val4",
+#  one5    => "val5",
+#  one6    => "val6",
+#  one7    => "val7",
+#  one8    => "val8",
+#}';
+
+###----------------------------------------------------------------###
+
+my $conf = eval $str;
+
+my %TESTS = ();
+
+### do perl
+my $file = tmpnam(). '.pl';
+open OUT, ">$file";
+print OUT $str;
+close OUT;
+$TESTS{pl} = sub {
+  my $hash = $cob->read_ref($file);
+};
+$files{pl} = $file;
+
+### do a generic conf_write
+my $file2 = tmpnam(). '.g_conf';
+&generic_conf_write($file2, $conf);
+local $CGI::Ex::Conf::EXT_READERS{g_conf} = \&generic_conf_read;
+$TESTS{g_conf} = sub {
+  my $hash = $cob->read_ref($file2);
+};
+$files{g_conf} = $file2;
+
+
+### load in the rest of the tests that we support
+if (eval {require Storable}) {
+  my $_file = tmpnam(). '.sto';
+  &Storable::store($conf, $_file);
+  $TESTS{sto} = sub {
+    my $hash = $cob->read_ref($_file);
+  };
+  $files{sto} = $_file;
+}
+
+if (eval {require Storable}) {
+  my $_file = tmpnam(). '.sto2';
+  &Storable::store($conf, $_file);
+  $TESTS{sto2} = sub {
+    my $hash = &Storable::retrieve($_file);
+  };
+  $files{sto2} = $_file;
+}
+
+if (eval {require YAML}) {
+  my $_file = tmpnam(). '.yaml';
+  &YAML::DumpFile($_file, $conf);
+  $TESTS{yaml} = sub {
+    my $hash = $cob->read_ref($_file);
+  };
+  $files{yaml} = $_file;
+}
+
+if (eval {require YAML}) {
+  my $_file = tmpnam(). '.yaml2';
+  &YAML::DumpFile($_file, $conf);
+  $TESTS{yaml2} = sub {
+    my $hash = &YAML::LoadFile($_file);
+  };
+  $files{yaml2} = $_file;
+}
+
+if (eval {require YAML}) {
+  my $_file = tmpnam(). '.yaml';
+  &YAML::DumpFile($_file, $conf);
+  $cob->preload_files($_file);
+  $TESTS{yaml3} = sub {
+    my $hash = $cob->read_ref($_file);
+  };
+  $files{yaml3} = $_file;
+}
+
+if (eval {require Config::IniHash}) {
+  my $_file = tmpnam(). '.ini';
+  &Config::IniHash::WriteINI($_file, $conf);
+  $TESTS{ini} = sub {
+    local $^W = 0;
+    my $hash = $cob->read_ref($_file);
+  };
+  $files{ini} = $_file;
+}
+
+if (eval {require XML::Simple}) {
+  my $_file = tmpnam(). '.xml';
+  my $xml = XML::Simple->new->XMLout($conf);
+  open  OUT, ">$_file" || die $!;
+  print OUT $xml;
+  close OUT;
+  $TESTS{xml} = sub {
+    my $hash = $cob->read_ref($_file);
+  };
+  $files{xml} = $_file;
+}
+
+### tell file locations
+foreach my $key (sort keys %files) {
+  print "$key => $files{$key}\n";
+}
+
+cmpthese($n, \%TESTS);
+
+### comment out this line to inspect files
+unlink $_ foreach values %files;
+
+###----------------------------------------------------------------###
+
+sub generic_conf_read {
+  my $_file = shift || die "No filename supplied";
+  my $sep_by_newlines = ($_[0] && lc($_[0]) eq 'sep_by_newlines') ? 1 : 0;
+
+  ### fh will now lose scope and close itself if necessary
+  my $FH = do { local *FH; *FH };
+  open ($FH, $_file) || return {};
+
+  my $x = 0;
+  my $conf = {};
+  my $key  = '';
+  my $val;
+  my $line;
+  my ($is_array,$is_hash,$is_multiline);
+  my $order;
+  $order = [] if wantarray;
+
+  while( defined($line = <$FH>) ){
+    last if ! defined $line;
+    last if $x++ > 10000;
+
+    next if index($line,'#') == 0;
+
+    if ($line =~ /^\s/ && ($is_multiline || $line ne "\n")){
+      next if ! length($key);
+      $conf->{$key} .= $line;
+      $is_multiline = 1;
+
+    }else{
+      ### duplicate trim section
+      if( length($key) ){
+        $conf->{$key} =~ s/\s+$//;
+        if( $is_array || $is_hash ){
+          $conf->{$key} =~ s/^\s+//;
+          my $urldec = (index($conf->{$key},'%')>-1 || index($conf->{$key},'+')>-1);
+          my @pieces;
+          if ($sep_by_newlines) {
+            @pieces = split(/\s*\n\s*/,$conf->{$key});
+            @pieces = map {split(/\s+/,$_,2)} @pieces if $is_hash;
+          } else {
+            @pieces = split(/\s+/,$conf->{$key});
+          }
+          if( $urldec ){
+            foreach my $_val (@pieces){
+              $_val =~ y/+/ / if ! $sep_by_newlines;
+              $_val =~ s/%([a-f0-9]{2})/chr(hex($1))/egi;
+            }
+          }
+          if( $is_array ){
+            foreach (@pieces){ $_="" if index($_,$PLACEHOLDER)>-1 }
+            $conf->{$key} = \@pieces;
+          }elsif( $is_hash ){
+            foreach (@pieces){ $_="" if index($_,$PLACEHOLDER)>-1 }
+            shift(@pieces) if scalar(@pieces) % 2;
+            $conf->{$key} = {@pieces};
+          }
+        }elsif( ! $is_multiline ){
+          $conf->{$key} =~ y/+/ / if ! $sep_by_newlines;
+          $conf->{$key} =~ s/%([a-f0-9]{2})/chr(hex($1))/egi;
+        }
+      }
+
+      ($key,$val) = split(/\s+/,$line,2);
+      $is_array = 0;
+      $is_hash = 0;
+      $is_multiline = 0;
+      if (! length($key)) {
+        next;
+      } elsif (index($key,'array:') == 0) {
+        $is_array = $key =~ s/^array://i;
+      } elsif (index($key,'hash:') == 0) {
+        $is_hash = $key =~ s/^hash://i;
+      }
+      $key =~ y/+/ / if ! $sep_by_newlines;
+      $key =~ s/%([a-f0-9]{2})/chr(hex($1))/egi;
+      $conf->{$key} = $val;
+      push @$order, $key if $order;
+    }
+  }
+
+  ### duplicate trim section
+  if( length($key) && defined($conf->{$key}) ){
+    $conf->{$key} =~ s/\s+$//;
+    if( $is_array || $is_hash ){
+      $conf->{$key} =~ s/^\s+//;
+      my $urldec = (index($conf->{$key},'%')>-1 || index($conf->{$key},'+')>-1);
+      my @pieces;
+      if ($sep_by_newlines) {
+        @pieces = split(/\s*\n\s*/,$conf->{$key});
+        @pieces = map {split(/\s+/,$_,2)} @pieces if $is_hash;
+      } else {
+        @pieces = split(/\s+/,$conf->{$key});
+      }
+      if( $urldec ){
+        foreach my $_val (@pieces){
+          $_val =~ y/+/ / if ! $sep_by_newlines;
+          $_val =~ s/%([a-f0-9]{2})/chr(hex($1))/egi;
+        }
+      }
+      if( $is_array ){
+        foreach (@pieces){ $_="" if index($_,$PLACEHOLDER)>-1 }
+        $conf->{$key} = \@pieces;
+      }elsif( $is_hash ){
+        foreach (@pieces){ $_="" if index($_,$PLACEHOLDER)>-1 }
+        shift(@pieces) if scalar(@pieces) % 2;
+        $conf->{$key} = {@pieces};
+      }
+    }elsif( ! $is_multiline ){
+      $conf->{$key} =~ y/+/ / if ! $sep_by_newlines;
+      $conf->{$key} =~ s/%([a-f0-9]{2})/chr(hex($1))/egi;
+    }
+  }
+
+
+  close($FH);
+  return $order ? ($conf,$order) : $conf;
+}
+
+
+sub generic_conf_write{
+  my $_file = shift || die "No filename supplied";
+
+  if (! @_) {
+    return;
+  }
+
+  my $new_conf = shift || die "Missing update hashref";
+  return if ! keys %$new_conf;
+
+
+  ### do we allow writing out hashes in a nice way
+  my $sep_by_newlines = ($_[0] && lc($_[0]) eq 'sep_by_newlines') ? 1 : 0;
+
+  ### touch the file if necessary
+  if( ! -e $_file ){
+    open(TOUCH,">$_file") || die "Conf file \"$_file\" could not be opened for writing: $!";
+    close(TOUCH);
+  }
+
+  ### read old values
+  my $conf = &generic_conf_read($_file) || {};
+  my $key;
+  my $val;
+
+  ### remove duplicates and undefs
+  while (($key,$val) = each %$new_conf){
+    $conf->{$key} = $new_conf->{$key};
+  }
+
+  ### prepare output
+  my $output = '';
+  my $qr = qr/([^\ \!\"\$\&-\*\,-\~])/;
+  foreach $key (sort keys %$conf){
+    next if ! defined $conf->{$key};
+    $val = delete $conf->{$key};
+    $key =~ s/([^\ \!\"\$\&-\*\,-9\;-\~\/])/sprintf("%%%02X",ord($1))/eg;
+    $key =~ tr/\ /+/;
+    my $ref = ref($val);
+    if( $ref ){
+      if( $ref eq 'HASH' ){
+        $output .= "hash:$key\n";
+        foreach my $_key (sort keys %$val){
+          my $_val = $val->{$_key};
+          next if ! defined $_val;
+          $_val =~ s/$qr/sprintf("%%%02X",ord($1))/ego;
+          $_key =~ s/$qr/sprintf("%%%02X",ord($1))/ego;
+          if ($sep_by_newlines) {
+            $_val =~ s/^(\s)/sprintf("%%%02X",ord($1))/ego;
+            $_val =~ s/(\s)$/sprintf("%%%02X",ord($1))/ego;
+            $_key =~ s/\ /%20/g;
+          } else {
+            $_val =~ tr/\ /+/;
+            $_key =~ tr/\ /+/;
+          }
+          $_val = $PLACEHOLDER if ! length($_val);
+          $output .= "\t$_key\t$_val\n";
+        }
+      }elsif( $ref eq 'ARRAY' ){
+        $output .= "array:$key\n";
+        foreach (@$val){
+          my $_val = $_;
+          $_val =~ s/$qr/sprintf("%%%02X",ord($1))/ego;
+          if ($sep_by_newlines) {
+            $_val =~ s/^(\s)/sprintf("%%%02X",ord($1))/ego;
+            $_val =~ s/(\s)$/sprintf("%%%02X",ord($1))/ego;
+          } else {
+            $_val =~ tr/\ /+/;
+          }
+          $_val = $PLACEHOLDER if ! length($_val);
+          $output .= "\t$_val\n";
+        }
+      }else{
+        $output .= "$key\tbless('$val','$ref')\n"; # stringify the ref
+      }
+    }else{
+      if( $val =~ /\n/ ){ # multiline values that are indented properly don't need encoding
+        if( $val =~ /^\s/ || $val =~ /\s$/ || $val =~ /\n\n/ || $val =~ /\n([^\ \t])/ ){
+          if ($sep_by_newlines) {
+            $val =~ s/([^\!\"\$\&-\~])/sprintf("%%%02X",ord($1))/eg;
+          } else {
+            $val =~ s/([^\ \!\"\$\&-\*\,-\~])/sprintf("%%%02X",ord($1))/eg;
+            $val =~ y/ /+/;
+          }
+        }
+      }else{
+        $val =~ s/([^\ \t\!\"\$\&-\*\,-\~])/sprintf("%%%02X",ord($1))/eg;
+        $val =~ s/^(\s)/sprintf("%%%02X",ord($1))/eg;
+        $val =~ s/(\s)$/sprintf("%%%02X",ord($1))/eg;
+      }
+      $output .= "$key\t$val\n";
+    }
+  }
+
+  open (CONF,"+<$_file") || die "Could not open the file for writing ($_file) -- [$!]";
+  print CONF $output;
+  truncate CONF, length($output);
+  close CONF;
+
+  return 1;
+}
+
+1;
+
diff --git a/t/samples/bench_conf_writers.pl b/t/samples/bench_conf_writers.pl
new file mode 100644 (file)
index 0000000..ac6438c
--- /dev/null
@@ -0,0 +1,391 @@
+#!/usr/bin/perl -w
+
+use strict;
+use vars qw($PLACEHOLDER);
+use Benchmark qw(cmpthese);
+use CGI::Ex::Conf;
+use POSIX qw(tmpnam);
+
+$PLACEHOLDER = chr(186).'~'.chr(186);
+
+my $n = -2;
+
+my $cob   = CGI::Ex::Conf->new;
+my %files = ();
+
+###----------------------------------------------------------------###
+
+# [pauls@localhost lib]$ perl ../t/samples/bench_conf_readers.pl
+#         Rate   yaml  yaml2    sto     pl    xml g_conf    ini   sto2
+#yaml    250/s     --    -1%   -14%   -14%   -61%   -77%   -95%   -95%
+#yaml2   254/s     1%     --   -13%   -13%   -60%   -77%   -95%   -95%
+#sto     292/s    17%    15%     --    -0%   -54%   -73%   -94%   -95%
+#pl      292/s    17%    15%     0%     --   -54%   -73%   -94%   -95%
+#xml     636/s   155%   151%   118%   118%     --   -42%   -88%   -88%
+#g_conf 1088/s   335%   329%   273%   272%    71%     --   -79%   -80%
+#ini    5144/s  1958%  1929%  1662%  1660%   708%   373%     --    -3%
+#sto2   5321/s  2029%  1999%  1723%  1721%   736%   389%     3%     --
+
+my $str = {
+  foo     => {key1 => "bar",   key2 => "ralph"},
+  pass    => {key1 => "word",  key2 => "ralph"},
+  garbage => {key1 => "can",   key2 => "ralph"},
+  mighty  => {key1 => "ducks", key2 => "ralph"},
+  quack   => {key1 => "moo",   key2 => "ralph"},
+  one1    => {key1 => "val1",  key2 => "ralph"},
+  one2    => {key1 => "val2",  key2 => "ralph"},
+  one3    => {key1 => "val3",  key2 => "ralph"},
+  one4    => {key1 => "val4",  key2 => "ralph"},
+  one5    => {key1 => "val5",  key2 => "ralph"},
+  one6    => {key1 => "val6",  key2 => "ralph"},
+  one7    => {key1 => "val7",  key2 => "ralph"},
+  one8    => {key1 => "val8",  key2 => "ralph"},
+};
+
+###----------------------------------------------------------------###
+
+#         Rate   yaml  yaml2     pl    sto    xml g_conf   sto2
+#yaml    736/s     --    -3%   -20%   -21%   -62%   -72%   -89%
+#yaml2   755/s     3%     --   -18%   -19%   -61%   -71%   -89%
+#pl      923/s    25%    22%     --    -1%   -53%   -65%   -86%
+#sto     928/s    26%    23%     1%     --   -53%   -65%   -86%
+#xml    1961/s   166%   160%   113%   111%     --   -26%   -71%
+#g_conf 2635/s   258%   249%   185%   184%    34%     --   -61%
+#sto2   6824/s   827%   803%   639%   635%   248%   159%     --
+
+#$str = {
+#  foo     => "bar",
+#  pass    => "word",
+#  garbage => "can",
+#  mighty  => "ducks",
+#  quack   => "moo",
+#  one1    => "val1",
+#  one2    => "val2",
+#  one3    => "val3",
+#  one4    => "val4",
+#  one5    => "val5",
+#  one6    => "val6",
+#  one7    => "val7",
+#  one8    => "val8",
+#};
+
+###----------------------------------------------------------------###
+
+my $conf = eval $str;
+
+my %TESTS = ();
+
+### do perl
+my $dir = tmpnam;
+mkdir $dir, 0755;
+my $tmpnam = "$dir/bench";
+my $file = $tmpnam. '.pl';
+$TESTS{pl} = sub {
+  $cob->write_ref($file, $str);
+};
+$files{pl} = $file;
+
+### do a generic conf_write
+my $file2 = $tmpnam. '.g_conf';
+local $CGI::Ex::Conf::EXT_WRITERS{g_conf} = \&generic_conf_write;
+$TESTS{g_conf} = sub {
+  $cob->write_ref($file2, $str);
+};
+$files{g_conf} = $file2;
+
+
+### load in the rest of the tests that we support
+if (eval {require Storable}) {
+  my $_file = $tmpnam. '.sto';
+  $TESTS{sto} = sub {
+    $cob->write_ref($file, $str);
+  };
+  $files{sto} = $_file;
+}
+
+if (eval {require Storable}) {
+  my $_file = $tmpnam. '.sto2';
+  $TESTS{sto2} = sub {
+    &Storable::store($str, $_file);
+  };
+  $files{sto2} = $_file;
+}
+
+if (eval {require YAML}) {
+  my $_file = $tmpnam. '.yaml';
+  $TESTS{yaml} = sub {
+    $cob->write_ref($_file, $str);
+  };
+  $files{yaml} = $_file;
+}
+
+if (eval {require YAML}) {
+  my $_file = $tmpnam. '.yaml2';
+  $TESTS{yaml2} = sub {
+    &YAML::DumpFile($_file, $str);
+  };
+  $files{yaml2} = $_file;
+}
+
+if (eval {require Config::IniHash}) {
+  my $_file = $tmpnam. '.ini';
+  $TESTS{ini} = sub {
+    local $^W = 0;
+    $cob->write_ref($_file, $str);
+  };
+  $files{ini} = $_file;
+}
+
+if (eval {require XML::Simple}) {
+  my $_file = $tmpnam. '.xml';
+  $TESTS{xml} = sub {
+    $cob->write_ref($_file, $str);
+  };
+  $files{xml} = $_file;
+}
+
+### tell file locations
+foreach my $key (sort keys %files) {
+  print "$key => $files{$key}\n";
+}
+
+foreach my $key (keys %TESTS) {
+  eval { &{ $TESTS{$key} } };
+  if ($@) {
+    warn "Test for $key failed - skipping";
+    delete $TESTS{$key};
+  }
+}
+
+
+cmpthese($n, \%TESTS);
+
+### comment out this line to inspect files
+unlink $_ foreach values %files;
+rmdir $dir;
+
+###----------------------------------------------------------------###
+
+sub generic_conf_read {
+  my $_file = shift || die "No filename supplied";
+  my $sep_by_newlines = ($_[0] && lc($_[0]) eq 'sep_by_newlines') ? 1 : 0;
+
+  ### fh will now lose scope and close itself if necessary
+  my $FH = do { local *FH; *FH };
+  open ($FH, $_file) || return {};
+
+  my $x = 0;
+  my $conf = {};
+  my $key  = '';
+  my $val;
+  my $line;
+  my ($is_array,$is_hash,$is_multiline);
+  my $order;
+  $order = [] if wantarray;
+
+  while( defined($line = <$FH>) ){
+    last if ! defined $line;
+    last if $x++ > 10000;
+
+    next if index($line,'#') == 0;
+
+    if ($line =~ /^\s/ && ($is_multiline || $line ne "\n")){
+      next if ! length($key);
+      $conf->{$key} .= $line;
+      $is_multiline = 1;
+
+    }else{
+      ### duplicate trim section
+      if( length($key) ){
+        $conf->{$key} =~ s/\s+$//;
+        if( $is_array || $is_hash ){
+          $conf->{$key} =~ s/^\s+//;
+          my $urldec = (index($conf->{$key},'%')>-1 || index($conf->{$key},'+')>-1);
+          my @pieces;
+          if ($sep_by_newlines) {
+            @pieces = split(/\s*\n\s*/,$conf->{$key});
+            @pieces = map {split(/\s+/,$_,2)} @pieces if $is_hash;
+          } else {
+            @pieces = split(/\s+/,$conf->{$key});
+          }
+          if( $urldec ){
+            foreach my $_val (@pieces){
+              $_val =~ y/+/ / if ! $sep_by_newlines;
+              $_val =~ s/%([a-f0-9]{2})/chr(hex($1))/egi;
+            }
+          }
+          if( $is_array ){
+            foreach (@pieces){ $_="" if index($_,$PLACEHOLDER)>-1 }
+            $conf->{$key} = \@pieces;
+          }elsif( $is_hash ){
+            foreach (@pieces){ $_="" if index($_,$PLACEHOLDER)>-1 }
+            shift(@pieces) if scalar(@pieces) % 2;
+            $conf->{$key} = {@pieces};
+          }
+        }elsif( ! $is_multiline ){
+          $conf->{$key} =~ y/+/ / if ! $sep_by_newlines;
+          $conf->{$key} =~ s/%([a-f0-9]{2})/chr(hex($1))/egi;
+        }
+      }
+
+      ($key,$val) = split(/\s+/,$line,2);
+      $is_array = 0;
+      $is_hash = 0;
+      $is_multiline = 0;
+      if (! length($key)) {
+        next;
+      } elsif (index($key,'array:') == 0) {
+        $is_array = $key =~ s/^array://i;
+      } elsif (index($key,'hash:') == 0) {
+        $is_hash = $key =~ s/^hash://i;
+      }
+      $key =~ y/+/ / if ! $sep_by_newlines;
+      $key =~ s/%([a-f0-9]{2})/chr(hex($1))/egi;
+      $conf->{$key} = $val;
+      push @$order, $key if $order;
+    }
+  }
+
+  ### duplicate trim section
+  if( length($key) && defined($conf->{$key}) ){
+    $conf->{$key} =~ s/\s+$//;
+    if( $is_array || $is_hash ){
+      $conf->{$key} =~ s/^\s+//;
+      my $urldec = (index($conf->{$key},'%')>-1 || index($conf->{$key},'+')>-1);
+      my @pieces;
+      if ($sep_by_newlines) {
+        @pieces = split(/\s*\n\s*/,$conf->{$key});
+        @pieces = map {split(/\s+/,$_,2)} @pieces if $is_hash;
+      } else {
+        @pieces = split(/\s+/,$conf->{$key});
+      }
+      if( $urldec ){
+        foreach my $_val (@pieces){
+          $_val =~ y/+/ / if ! $sep_by_newlines;
+          $_val =~ s/%([a-f0-9]{2})/chr(hex($1))/egi;
+        }
+      }
+      if( $is_array ){
+        foreach (@pieces){ $_="" if index($_,$PLACEHOLDER)>-1 }
+        $conf->{$key} = \@pieces;
+      }elsif( $is_hash ){
+        foreach (@pieces){ $_="" if index($_,$PLACEHOLDER)>-1 }
+        shift(@pieces) if scalar(@pieces) % 2;
+        $conf->{$key} = {@pieces};
+      }
+    }elsif( ! $is_multiline ){
+      $conf->{$key} =~ y/+/ / if ! $sep_by_newlines;
+      $conf->{$key} =~ s/%([a-f0-9]{2})/chr(hex($1))/egi;
+    }
+  }
+
+
+  close($FH);
+  return $order ? ($conf,$order) : $conf;
+}
+
+
+sub generic_conf_write{
+  my $_file = shift || die "No filename supplied";
+
+  if (! @_) {
+    return;
+  }
+
+  my $new_conf = shift || die "Missing update hashref";
+  return if ! keys %$new_conf;
+
+
+  ### do we allow writing out hashes in a nice way
+  my $sep_by_newlines = ($_[0] && lc($_[0]) eq 'sep_by_newlines') ? 1 : 0;
+
+  ### touch the file if necessary
+  if( ! -e $_file ){
+    open(TOUCH,">$_file") || die "Conf file \"$_file\" could not be opened for writing: $!";
+    close(TOUCH);
+  }
+
+  ### read old values
+  my $conf = &generic_conf_read($_file) || {};
+  my $key;
+  my $val;
+
+  ### remove duplicates and undefs
+  while (($key,$val) = each %$new_conf){
+    $conf->{$key} = $new_conf->{$key};
+  }
+
+  ### prepare output
+  my $output = '';
+  my $qr = qr/([^\ \!\"\$\&-\*\,-\~])/;
+  foreach $key (sort keys %$conf){
+    next if ! defined $conf->{$key};
+    $val = delete $conf->{$key};
+    $key =~ s/([^\ \!\"\$\&-\*\,-9\;-\~\/])/sprintf("%%%02X",ord($1))/eg;
+    $key =~ tr/\ /+/;
+    my $ref = ref($val);
+    if( $ref ){
+      if( $ref eq 'HASH' ){
+        $output .= "hash:$key\n";
+        foreach my $_key (sort keys %$val){
+          my $_val = $val->{$_key};
+          next if ! defined $_val;
+          $_val =~ s/$qr/sprintf("%%%02X",ord($1))/ego;
+          $_key =~ s/$qr/sprintf("%%%02X",ord($1))/ego;
+          if ($sep_by_newlines) {
+            $_val =~ s/^(\s)/sprintf("%%%02X",ord($1))/ego;
+            $_val =~ s/(\s)$/sprintf("%%%02X",ord($1))/ego;
+            $_key =~ s/\ /%20/g;
+          } else {
+            $_val =~ tr/\ /+/;
+            $_key =~ tr/\ /+/;
+          }
+          $_val = $PLACEHOLDER if ! length($_val);
+          $output .= "\t$_key\t$_val\n";
+        }
+      }elsif( $ref eq 'ARRAY' ){
+        $output .= "array:$key\n";
+        foreach (@$val){
+          my $_val = $_;
+          $_val =~ s/$qr/sprintf("%%%02X",ord($1))/ego;
+          if ($sep_by_newlines) {
+            $_val =~ s/^(\s)/sprintf("%%%02X",ord($1))/ego;
+            $_val =~ s/(\s)$/sprintf("%%%02X",ord($1))/ego;
+          } else {
+            $_val =~ tr/\ /+/;
+          }
+          $_val = $PLACEHOLDER if ! length($_val);
+          $output .= "\t$_val\n";
+        }
+      }else{
+        $output .= "$key\tbless('$val','$ref')\n"; # stringify the ref
+      }
+    }else{
+      if( $val =~ /\n/ ){ # multiline values that are indented properly don't need encoding
+        if( $val =~ /^\s/ || $val =~ /\s$/ || $val =~ /\n\n/ || $val =~ /\n([^\ \t])/ ){
+          if ($sep_by_newlines) {
+            $val =~ s/([^\!\"\$\&-\~])/sprintf("%%%02X",ord($1))/eg;
+          } else {
+            $val =~ s/([^\ \!\"\$\&-\*\,-\~])/sprintf("%%%02X",ord($1))/eg;
+            $val =~ y/ /+/;
+          }
+        }
+      }else{
+        $val =~ s/([^\ \t\!\"\$\&-\*\,-\~])/sprintf("%%%02X",ord($1))/eg;
+        $val =~ s/^(\s)/sprintf("%%%02X",ord($1))/eg;
+        $val =~ s/(\s)$/sprintf("%%%02X",ord($1))/eg;
+      }
+      $output .= "$key\t$val\n";
+    }
+  }
+
+  open (CONF,"+<$_file") || die "Could not open the file for writing ($_file) -- [$!]";
+  print CONF $output;
+  truncate CONF, length($output);
+  close CONF;
+
+  return 1;
+}
+
+1;
+
diff --git a/t/samples/bench_method_calling.pl b/t/samples/bench_method_calling.pl
new file mode 100755 (executable)
index 0000000..a65afbc
--- /dev/null
@@ -0,0 +1,111 @@
+#!/usr/bin/perl -w
+
+use strict;
+use Benchmark qw(cmpthese);
+use CGI::Ex::Dump qw(debug);
+
+my $n = 500_000;
+
+{
+  package A;
+  use vars qw($AUTOLOAD);
+  sub AUTOLOAD {
+    my $self = shift;
+    my $meth = ($AUTOLOAD =~ /::(\w+)$/) ? $1 : die "Bad method $AUTOLOAD";
+    die "Unknown property $meth" if ! exists $self->{$meth};
+    if ($#_ != -1) {
+      $self->{$meth} = shift;
+    } else {
+      return $self->{$meth}
+    }
+  }
+  sub DETROY {}
+}
+
+{
+  package B;
+  sub add_property {
+    my $self = shift;
+    my $prop = shift;
+    no strict 'refs';
+    * {"B::$prop"} = sub {
+      my $self = shift;
+      if ($#_ != -1) {
+        $self->{$prop} = shift;
+      } else {
+        return $self->{$prop};
+      }
+    };
+    $self->$prop(@_) if $#_ != -1;
+  }
+}
+
+{
+  package C;
+  sub add_property {
+    my $self = shift;
+    my $prop = shift;
+    no strict 'refs';
+    my $name = __PACKAGE__ ."::". $prop;
+    *$name = sub : lvalue {
+      my $self = shift;
+      $self->{$prop} = shift() if $#_ != -1;
+      $self->{$prop};
+    } if ! defined &$name;
+    $self->$prop() = shift() if $#_ != -1;
+  }
+}
+
+my $a = bless {}, 'A';
+$a->{foo} = 1;
+#debug $a->foo();
+#$a->foo(2);
+#debug $a->foo();
+
+my $b = bless {}, 'B';
+$b->add_property('foo', 1);
+#debug $b->foo();
+#$b->foo(2);
+#debug $b->foo();
+
+my $c = bless {}, 'C';
+$c->add_property('foo', 1);
+#debug $c->foo();
+#$c->foo(2);
+#debug $c->foo();
+
+my $d = bless {}, 'C';
+$d->add_property('foo', 1);
+#debug $d->foo();
+#$d->foo = 2;
+#debug $d->foo();
+
+
+use constant do_set => 1;
+
+cmpthese($n, {
+  autoloadonly => sub {
+    my $v = $a->foo();
+    if (do_set) {
+      $a->foo(2);
+    }
+  },
+  addproperty => sub {
+    my $v = $b->foo();
+    if (do_set) {
+      $b->foo(2);
+    }
+  },
+  addproperty_withlvalue => sub {
+    my $v = $c->foo();
+    if (do_set) {
+      $c->foo(2);
+    }
+  },
+  addproperty_withlvalue2 => sub {
+    my $v = $d->foo();
+    if (do_set) {
+      $d->foo = 2;
+    }
+  },
+});
diff --git a/t/samples/cgi_ex_1.cgi b/t/samples/cgi_ex_1.cgi
new file mode 100755 (executable)
index 0000000..18aa11b
--- /dev/null
@@ -0,0 +1,181 @@
+#!/usr/bin/perl -w
+
+if (__FILE__ eq $0) {
+  &handler();
+}
+
+###----------------------------------------------------------------###
+
+use strict;
+use CGI::Ex;
+use CGI::Ex::Validate ();
+use CGI::Ex::Fill ();
+use CGI::Ex::Dump qw(debug);
+
+###----------------------------------------------------------------###
+
+sub handler {
+  my $cgix = CGI::Ex->new();
+  my $vob  = CGI::Ex::Validate->new();
+  my $form = $cgix->get_form();
+
+  ### allow for js validation libraries
+  ### path_info should contain something like /CGI/Ex/yaml_load.js
+  ### see the line with 'js_val' below
+  my $info = $ENV{PATH_INFO} || '';
+  if ($info =~ m|^(/\w+)+.js$|) {
+    $info =~ s|^/+||;
+    CGI::Ex->new->print_js($info);
+    return;
+  }
+
+
+  debug $form;
+
+
+  ### check for errors - if they have submitted information
+  my $has_info = ($form->{processing}) ? 1 : 0;
+  my $errob = $has_info ? $vob->validate($form, &validation_hash()) : undef;
+  my $form_name = 'formfoo';
+
+  ### failed validation - send out the template
+  if (! $has_info || $errob) {
+
+    ### get a template and swap defaults
+    my $swap = &defaults_hash();
+
+    ### add errors to the swap (if any)
+    if ($errob) {
+      my $hash = $errob->as_hash();
+      $swap->{$_} = delete($hash->{$_}) foreach keys %$hash;
+      $swap->{'error_header'} = 'Please correct the form information below';
+    }
+
+    ### get js validation ready
+    $swap->{'form_name'} = $form_name;
+    $swap->{'js_val'} = $vob->generate_js(&validation_hash(), # filename or valhash
+                                          $form_name,         # name of form
+                                          $ENV{SCRIPT_NAME}); # browser path to cgi that calls print_js
+
+    ### swap in defaults, errors and js_validation
+    my $content = $cgix->swap_template(&get_content_form(), $swap);
+
+    ### fill form fields
+    $cgix->fill(\$content, $form);
+    #debug $content;
+
+    ### print it out
+    &CGI::Ex::print_content_type();
+    print $content;
+    return;
+  }
+
+
+  ### show some sort of success if there were no errors
+  &CGI::Ex::print_content_type();
+  my $content = $cgix->swap_template(&get_content_success(), &defaults_hash());
+  print $content;
+  return;
+
+}
+
+###----------------------------------------------------------------###
+
+sub validation_hash {
+  return {
+    'group order' => ['username', 'password'],
+    username => {
+      required => 1,
+      min_len  => 3,
+      max_len  => 30,
+      match    => 'm/^\w+$/',
+      # could probably all be done with match => 'm/^\w{3,30}$/'
+    },
+    password => {
+      required => 1,
+      max_len  => 20,
+    },
+    password_verify => {
+      validate_if => 'password',
+      equals      => 'password',
+    },
+  };
+}
+
+sub defaults_hash {
+  return {
+    title  => 'My Application',
+    script => $ENV{SCRIPT_NAME},
+    color  => ['#ccf', '#aaf'],
+  }
+}
+
+###----------------------------------------------------------------###
+
+sub get_content_form {
+  return qq{
+    <html>
+    <head>
+      <title>[% title %]</title>
+      <style>
+      .error {
+        display: block;
+        color: red;
+        font-weight: bold;
+      }
+      </style>
+    </head>
+    <body>
+    <h1 style='color:blue'>Please Enter information</h1>
+    <span style='color:red'>[% error_header %]</span>
+    <br>
+
+    <form name="[% form_name %]">
+    <input type=hidden name=processing value=1>
+
+    <table>
+    <tr bgcolor=[% color.0 %]>
+      <td>Username:</td>
+      <td>
+        <input type=text size=30 name=username>
+        <span class=error id=username_error>[% username_error %]</span></td>
+    </tr>
+    <tr bgcolor=[% color.1 %]>
+      <td>Password:</td>
+      <td><input type=password size=20 name=password>
+        <span class=error id=password_error>[% password_error %]</span></td>
+    </tr>
+    <tr bgcolor=[% color.0 %]>
+      <td>Password Verify:</td>
+      <td><input type=password size=20 name=password_verify>
+        <span class=error id=password_verify_error>[% password_verify_error %]</span></td>
+    </tr>
+    <tr bgcolor=[% color.1 %]>
+      <td colspan=2 align=right><input type=submit value=Submit></td>
+    </tr>
+
+    </table>
+
+    </form>
+
+    [% js_val %]
+    </body>
+    </html>
+  };
+}
+
+sub get_content_success {
+  return qq{
+    <html>
+    <head><title>[% title %]</title></head>
+    <body>
+    <h1 style='color:green'>Success</h1>
+    <br>
+    print "I can now continue on with the rest of my script!";
+    </body>
+    </html>
+  };
+}
+
+
+1;
diff --git a/t/samples/cgi_ex_2.cgi b/t/samples/cgi_ex_2.cgi
new file mode 100755 (executable)
index 0000000..73e37e2
--- /dev/null
@@ -0,0 +1,171 @@
+#!/usr/bin/perl -w
+
+if (__FILE__ eq $0) {
+  &handler();
+}
+
+sub handler {
+  MyCGI->navigate();
+}
+
+###----------------------------------------------------------------###
+
+package MyCGI;
+
+use strict;
+use base CGI::Ex::App;
+use CGI::Ex::Dump qw(debug);
+
+###----------------------------------------------------------------###
+
+sub pre_loop {
+  my $self = shift;
+  my $path = shift;
+  if ($#$path == -1) {
+    push @$path, 'userinfo';
+  }
+}
+
+sub userinfo_ready_validate {
+  my $self = shift;
+  return $self->form->{processing} ? 1 : 0;
+}
+
+sub userinfo_hash_validation {
+  return {
+    'group order' => ['username', 'password'],
+    username => {
+      required => 1,
+      min_len  => 3,
+      max_len  => 30,
+      match    => 'm/^\w+$/',
+      # could probably all be done with match => 'm/^\w{3,30}$/'
+    },
+    password => {
+      required => 1,
+      max_len  => 20,
+    },
+    password_verify => {
+      validate_if => 'password',
+      equals      => 'password',
+    },
+  };
+}
+
+sub userinfo_hash_form {
+  my $self = shift;
+  my $hash = $self->form;
+  $hash->{form_name} = 'formfoo';
+  $hash->{js_val}    = $self->vob->generate_js($self->userinfo_hash_validation(),
+                                               $hash->{form_name},
+                                               "$ENV{SCRIPT_NAME}/js");
+  return $hash;
+}
+
+sub hash_common {
+  return {
+    title  => 'My Application',
+    script => $ENV{SCRIPT_NAME},
+    color  => ['#ccf', '#aaf'],
+  }
+}
+
+sub print {
+  my $self = shift;
+  my $step = shift;
+  my $form = shift;
+  my $fill = shift;
+
+  my $content = ($step eq 'userinfo') ? &get_content_form()
+    : ($step eq 'main') ? &get_content_success()
+    : "Don't have content for step \"$step\"";
+
+  $self->cgix->swap_template(\$content, $form);
+  $self->cgix->fill(\$content, $fill);
+
+  $self->cgix->print_content_type();
+  print $content;
+}
+
+### this works because we added /js onto $ENV{SCRIPT_NAME} above near js_val
+sub js_pre_step {
+  my $self = shift;
+  my $info = $ENV{PATH_INFO} || '';
+  if ($info =~ m|^/js(/\w+)+.js$|) {
+    $info =~ s|^/+js/+||;
+    $self->cgix->print_js($info);
+    return 1;
+  }
+  return 0;
+}
+
+
+###----------------------------------------------------------------###
+
+sub get_content_form {
+  return qq{
+    <html>
+    <head>
+      <title>[% title %]</title>
+      <style>
+      .error {
+        display: block;
+        color: red;
+        font-weight: bold;
+      }
+      </style>
+    </head>
+    <body>
+    <h1 style='color:blue'>Please Enter information</h1>
+    <span style='color:red'>[% error_header %]</span>
+    <br>
+
+    <form name="[% form_name %]">
+    <input type=hidden name=processing value=1>
+
+    <table>
+    <tr bgcolor=[% color.0 %]>
+      <td>Username:</td>
+      <td>
+        <input type=text size=30 name=username>
+        <span class=error id=username_error>[% username_error %]</span></td>
+    </tr>
+    <tr bgcolor=[% color.1 %]>
+      <td>Password:</td>
+      <td><input type=password size=20 name=password>
+        <span class=error id=password_error>[% password_error %]</span></td>
+    </tr>
+    <tr bgcolor=[% color.0 %]>
+      <td>Password Verify:</td>
+      <td><input type=password size=20 name=password_verify>
+        <span class=error id=password_verify_error>[% password_verify_error %]</span></td>
+    </tr>
+    <tr bgcolor=[% color.1 %]>
+      <td colspan=2 align=right><input type=submit value=Submit></td>
+    </tr>
+
+    </table>
+
+    </form>
+
+    [% js_val %]
+    </body>
+    </html>
+  };
+}
+
+sub get_content_success {
+  return qq{
+    <html>
+    <head><title>[% title %]</title></head>
+    <body>
+    <h1 style='color:green'>Success</h1>
+    <br>
+    print "I can now continue on with the rest of my script!";
+    </body>
+    </html>
+  };
+}
+
+
+1;
diff --git a/t/samples/conf_path_1/apples.pl b/t/samples/conf_path_1/apples.pl
new file mode 100644 (file)
index 0000000..56856ae
--- /dev/null
@@ -0,0 +1,5 @@
+{
+  quantity => 20,
+  color    => 'red',
+  foo_immutable => 'file1',
+};
diff --git a/t/samples/conf_path_1/oranges.pl b/t/samples/conf_path_1/oranges.pl
new file mode 100644 (file)
index 0000000..03fc08b
--- /dev/null
@@ -0,0 +1,6 @@
+{
+  immutable => 1,
+  quantity => 20,
+  color    => 'orange',
+  foo => 'file1',
+};
diff --git a/t/samples/conf_path_3/apples.pl b/t/samples/conf_path_3/apples.pl
new file mode 100644 (file)
index 0000000..e72f0ee
--- /dev/null
@@ -0,0 +1,5 @@
+{
+  quantity => 30,
+  color    => 'green',
+  foo      => 'file2',
+};
diff --git a/t/samples/conf_path_3/oranges.pl b/t/samples/conf_path_3/oranges.pl
new file mode 100644 (file)
index 0000000..f02324d
--- /dev/null
@@ -0,0 +1,5 @@
+{
+  quantity => 30,
+  color    => 'orange',
+  foo      => 'file2',
+};
diff --git a/t/samples/generate_js.pl b/t/samples/generate_js.pl
new file mode 100644 (file)
index 0000000..aeb1ecb
--- /dev/null
@@ -0,0 +1,48 @@
+#!/usr/bin/perl -w
+
+use strict;
+use CGI::Ex::Validate;
+
+### sampe
+my $js_path   = "/cgi-bin/js";
+my $form_name = "my_form";
+my $val_hash  = {
+  'general as_hash_join' => "<br>\n<br>",
+  'general group_order'  => [qw(username password)],
+  username => {
+    required => 1,
+    match    => 'm/^\w+$/',
+    max_len  => 20,
+  },
+  password => {
+    match => ['m/\d/', 'm/[a-z]/'],
+    match_error => "\$name Must contain a letter and a number",
+  },
+};
+
+
+### generate the js
+my $val_obj = CGI::Ex::Validate->new;
+my $val = $val_obj->generate_js($val_hash, $form_name, $js_path);
+
+
+### sample document out put
+### not that you should ever inline your html
+$val_obj->cgix->content_type;
+print "<html>
+<body>
+<form name='my_form'>
+
+Username: <input type=text size=20 name=username><br>
+<span class=error id=username_error></span><br>
+Password: <input type=text size=20 name=password><br>
+<span class=error id=password_error></span><br>
+<input type=submit>
+
+</form>
+
+$val
+
+</body>
+</html>
+";
diff --git a/t/samples/html1.htm b/t/samples/html1.htm
new file mode 100644 (file)
index 0000000..9441558
--- /dev/null
@@ -0,0 +1,14 @@
+<form name="foo">
+<input type=text name=user>
+<input type=text name=foo>
+<input type=hidden name=bar value=1>
+</form>
+
+
+<script>
+document.validation="\n\
+  user:\n\
+    required: 1\n\
+  foo: {required_if: 'bar'}\n\
+";
+</script>
diff --git a/t/samples/html2.htm b/t/samples/html2.htm
new file mode 100644 (file)
index 0000000..1d8a41c
--- /dev/null
@@ -0,0 +1,10 @@
+<form name="foo">
+<input type=text name=user validation="
+ required: 1
+">
+<input type=text name=foo validation="
+ required_if: 'bar'
+">
+<input type=hidden name=bar value=1>
+</form>
+
diff --git a/t/samples/js_validate_1.html b/t/samples/js_validate_1.html
new file mode 100644 (file)
index 0000000..d906442
--- /dev/null
@@ -0,0 +1,203 @@
+<html>
+<style>
+.error {
+  color: red;
+  font-size: 75%;
+}
+</style>
+
+<script src="../../lib/CGI/Ex/yaml_load.js"></script>
+<script src="../../lib/CGI/Ex/validate.js"></script>
+<script>
+if (! document.yaml_load) {
+  document.writeln('<span style="color:red"><h1>Missing document.yaml_load</h1>Path to ../../lib/CGI/Ex/yaml_load.js may be invalid.</span>');
+} else {
+  document.writeln('<span style="color:green"><h1>Found document.yaml_load</h1></span>');
+}
+
+if (! document.validate) {
+  document.writeln('<span style="color:red"><h1>Missing document.validate</h1>Path to ../../lib/CGI/Ex/validate.js may be invalid.</span>');
+} else {
+  document.writeln('<span style="color:green"><h1>Found document.validate</h1></span>');
+}
+
+</script>
+
+
+<form name=a>
+<table>
+<tr>
+  <td valign=top>Username:</td>
+  <td>
+    <input type=text size=20 name=username><br>
+    <span id=username_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Password:</td>
+  <td>
+    <input type=password size=20 name=password><br>
+    <span id=password_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Verify Password:</td>
+  <td>
+    <input type=password size=20 name=password2><br>
+    <span id=password2_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Email:</td>
+  <td>
+    <input type=text size=40 name=email><br>
+    <span id=email_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Verify Email:</td>
+  <td>
+    <input type=text size=40 name=email2><br>
+    <span id=email2_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>State/Region:</td>
+  <td>
+    Specify State <input type=text size=2 name=state><br>
+    OR Region <input type=text size=20 name=region>
+    <span id=state_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Enum Check:</td>
+  <td>
+    <input type=text size=10 name=enum><br>
+    <span id=enum_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Compare Check:</td>
+  <td>
+    <input type=text size=10 name=compare><br>
+    <span id=compare_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Check one:</td>
+  <td>
+    <input type=checkbox name=checkone value=1> Foo<br>
+    <input type=checkbox name=checkone value=2> Bar<br>
+    <input type=checkbox name=checkone value=3> Baz<br>
+    <span id=checkone_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Check two:</td>
+  <td>
+    <input type=checkbox name=checktwo value=1> Foo<br>
+    <input type=checkbox name=checktwo value=2> Bar<br>
+    <input type=checkbox name=checktwo value=3> Baz<br>
+    <span id=checktwo_error class=error></span>
+  </td>
+</tr>
+<tr><td colspan=2><hr></td></tr>
+<tr>
+  <td valign=top>Fill In two:</td>
+  <td>
+    <span id=foo_error class=error></span><br>
+    <input type=text name=foo value="" size=30> Foo<br>
+    <input type=text name=bar value="" size=30> Bar<br>
+    <input type=text name=baz value="" size=30> Baz<br>
+  </td>
+</tr>
+<tr>
+  <td colspan=2 align=right>
+    <input type=submit>
+  </td>
+</tr>
+</table>
+</form>
+
+<script src="../../lib/CGI/Ex/yaml_load.js"></script>
+<script src="../../lib/CGI/Ex/validate.js"></script>
+<script>
+document.validation = "\n\
+#general no_inline: 1\n\
+general no_confirm: 1\n\
+general no_alert: 1\n\
+general as_array_prefix: ' -- '\n\
+#general as_hash_header: '<ul><li>'\n\
+#general as_hash_join:   '</li><li>'\n\
+#general as_hash_footer: '</li></ul>'\n\
+group order: [username, password, password2, email, email2, state, region, s_r_combo, enum, compare, checkone, checktwo, foo]\n\
+username:\n\
+ name: Username\n\
+ required: 1\n\
+ min_len: 3\n\
+ max_len: 30\n\
+password: &pa\n\
+ name: Password\n\
+ required: 1\n\
+ min_len: 6\n\
+ max_len: 30\n\
+ match: [m/\\d/, 'm/[a-z]/']\n\
+ match_error: '$name must contain both a letter and a number.'\n\
+password2:\n\
+ name: Verify password\n\
+ validate_if: *pa\n\
+ equals: password\n\
+ equals_name: password\n\
+email: &em\n\
+ name: Email\n\
+ required: 1\n\
+ max_len: 100\n\
+ match: 'm/^[^@]+@([\\w-]+\.)+\\w+$/'\n\
+ match_error: '$name must be a valid email address.'\n\
+email2:\n\
+ name: Verify email\n\
+ validate_if: *em\n\
+ equals: email\n\
+ equals_name: email\n\
+state:\n\
+ validate_if: [state, '! region']\n\
+ match: 'm/^\\w{2}$/'\n\
+ match_error: Please type a two letter state code.\n\
+region:\n\
+ validate_if: [region, '! state']\n\
+ delegate_error: state\n\
+ compare: 'eq Manitoba'\n\
+ compare_error: For this test - the region should be Manitoba.\n\
+s_r_combo:\n\
+ field: state\n\
+ delegate_error: state\n\
+ max_in_set: 1 of state region\n\
+ max_in_set_error: Specify only one of state and region.\n\
+ min_in_set: 1 of state region\n\
+ min_in_set_error: Specify one of state and region.\n\
+enum:\n\
+ name: Enum check\n\
+ enum: [one, two, three, four]\n\
+ enum_error: '$name must be one of one, two, three, or four.'\n\
+compare:\n\
+ required: 1\n\
+ replace: 's/\\D//g'\n\
+ name: Compare check\n\
+ compare: ['> 99', '< 1000']\n\
+ compare_error: '$name must be greater than 99 and less than 1000.'\n\
+checkone:\n\
+ name: Check one\n\
+ required: 1\n\
+ max_values: 1\n\
+checktwo:\n\
+ name: Check two\n\
+ min_values: 2\n\
+ max_values: 2\n\
+foo:\n\
+ min_in_set: 2 of foo bar baz\n\
+ max_in_set: 2 of foo bar baz\n\
+";
+if (document.check_form) document.check_form('a');
+</script>
+
+</html>
\ No newline at end of file
diff --git a/t/samples/js_validate_2.html b/t/samples/js_validate_2.html
new file mode 100644 (file)
index 0000000..00c012e
--- /dev/null
@@ -0,0 +1,116 @@
+<html>
+<style>
+.error {
+  color: red;
+  font-size: 75%;
+}
+</style>
+
+<script src="../../lib/CGI/Ex/yaml_load.js"></script>
+<script src="../../lib/CGI/Ex/validate.js"></script>
+<script>
+if (! document.yaml_load) {
+  document.writeln('<span style="color:red"><h1>Missing document.yaml_load</h1>Path to ../../lib/CGI/Ex/yaml_load.js may be invalid.</span>');
+} else {
+  document.writeln('<span style="color:green"><h1>Found document.yaml_load</h1></span>');
+}
+
+if (! document.validate) {
+  document.writeln('<span style="color:red"><h1>Missing document.validate</h1>Path to ../../lib/CGI/Ex/validate.js may be invalid.</span>');
+} else {
+  document.writeln('<span style="color:green"><h1>Found document.validate</h1></span>');
+}
+
+</script>
+
+
+<form name=a validation="
+general no_confirm: 1
+general no_alert: 1
+general as_array_prefix: ' -- '
+">
+<table>
+<tr>
+  <td valign=top>Username:</td>
+  <td>
+    <input type=text size=20 name=username validation="
+ name: Username
+ required: 1
+ min_len: 3
+ max_len: 30
+ match: 'm/^\w/'
+ match_error: '$name may contain only letters and numbers'
+"><br>
+    <span id=username_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Password:</td>
+  <td>
+    <input type=password size=20 name=password validation=" &pa
+ name: Password
+ required: 1
+ min_len: 6
+ max_len: 30
+ match: [m/\d/, 'm/[a-z]/']
+ match_error: '$name must contain both a letter and a number.'
+"><br>
+    <span id=password_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Verify Password:</td>
+  <td>
+    <input type=password size=20 name=password2 validation="{name: Verify password, validate_if: *pa,  equals: password, equals_name: password}"><br>
+    <span id=password2_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Email:</td>
+  <td>
+    <input type=text size=40 name=email validation="&em
+name: Email
+required: 1
+min_len: 6
+max_len: 100
+"><br>
+    <span id=email_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Verify Email:</td>
+  <td>
+    <input type=text size=40 name=email2 validation="
+name: Verify email
+validate_if: *em
+equals: email
+equals_name: email
+"><br>
+    <span id=email2_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Random Association:</td>
+  <td>
+    <input type=text size=40 name=random validation="
+name: random
+default: bull sun orange
+"><br> (type anything - will fill in default if none)<br>
+    <span id=email2_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td colspan=2 align=right>
+    <input type=submit>
+  </td>
+</tr>
+</table>
+</form>
+
+<script src="../../lib/CGI/Ex/yaml_load.js"></script>
+<script src="../../lib/CGI/Ex/validate.js"></script>
+<script>
+if (document.check_form) document.check_form('a');
+</script>
+
+</html>
\ No newline at end of file
diff --git a/t/samples/js_validate_3.html b/t/samples/js_validate_3.html
new file mode 100644 (file)
index 0000000..0ae65c8
--- /dev/null
@@ -0,0 +1,70 @@
+<html>
+<style>
+.error {
+  color: red;
+  font-size: 75%;
+}
+</style>
+
+<script src="../../lib/CGI/Ex/yaml_load.js"></script>
+<script src="../../lib/CGI/Ex/validate.js"></script>
+<script>
+if (! document.yaml_load) {
+  document.writeln('<span style="color:red"><h1>Missing document.yaml_load</h1>Path to ../../lib/CGI/Ex/yaml_load.js may be invalid.</span>');
+} else {
+  document.writeln('<span style="color:green"><h1>Found document.yaml_load</h1></span>');
+}
+
+if (! document.validate) {
+  document.writeln('<span style="color:red"><h1>Missing document.validate</h1>Path to ../../lib/CGI/Ex/validate.js may be invalid.</span>');
+} else {
+  document.writeln('<span style="color:green"><h1>Found document.validate</h1></span>');
+}
+
+</script>
+
+
+<form name=a validation="
+general no_confirm: 1
+general no_alert: 1
+general as_array_prefix: ' -- '
+">
+<table>
+<tr>
+  <td valign=top>Enter a date (YYYY/MM/DD) greater than today:<br>
+    (<script>var t=new Date();document.writeln(t.toGMTString())</script>)
+  </td>
+  <td>
+    <input type=text size=20 name=date validation="
+ name: 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.
+"><br>
+    <span id=date_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td colspan=2 align=right>
+    <input type=submit>
+  </td>
+</tr>
+</table>
+</form>
+
+<script src="../../lib/CGI/Ex/yaml_load.js"></script>
+<script src="../../lib/CGI/Ex/validate.js"></script>
+<script>
+if (document.check_form) document.check_form('a');
+</script>
+
+</html>
\ No newline at end of file
diff --git a/t/samples/perl1.pl b/t/samples/perl1.pl
new file mode 100644 (file)
index 0000000..4b41e16
--- /dev/null
@@ -0,0 +1,11 @@
+### this file is very simplistic
+### but it shows how easy the file can be
+{
+  user => {
+    required => 1,
+  },
+  foo => {
+    required_if => 'bar',
+  },
+}
+# last item returned must be the ref
diff --git a/t/samples/perl2.pl b/t/samples/perl2.pl
new file mode 100644 (file)
index 0000000..4f388d3
--- /dev/null
@@ -0,0 +1,20 @@
+### this yaml will return an arrayref containing three hashrefs
+### this shows three groups
+### the first two groups have validate_if's
+[
+   {
+     'group validate_if' => 'foo',
+     bar => {
+       required => 1,
+     },
+   },
+   {
+     'group validate_if' => 'hem',
+     haw => { required => 1 },
+   },
+   {
+     raspberry => {
+       required => 1,
+     },
+   },
+];
diff --git a/t/samples/storable1.storable b/t/samples/storable1.storable
new file mode 100644 (file)
index 0000000..f9d6640
Binary files /dev/null and b/t/samples/storable1.storable differ
diff --git a/t/samples/yaml1.val b/t/samples/yaml1.val
new file mode 100644 (file)
index 0000000..5fd169c
--- /dev/null
@@ -0,0 +1,10 @@
+### this file is very simplistic
+### but it shows how easy the file can be
+user:
+  required: 1
+foo:
+  required_if: bar
+
+### you could also do
+# user: {required: 1}
+# foo:  {required: 1}
diff --git a/t/samples/yaml2.val b/t/samples/yaml2.val
new file mode 100644 (file)
index 0000000..16c208f
--- /dev/null
@@ -0,0 +1,10 @@
+### this yaml will return an arrayref containing three hashrefs
+### this shows three groups
+### the first two groups have validate_if's
+- group validate_if: foo
+  bar:
+    required: 1
+- group validate_if: hem
+  haw: { required: 1 }
+- raspberry:
+    required: 1
diff --git a/t/samples/yaml3.val b/t/samples/yaml3.val
new file mode 100644 (file)
index 0000000..07a621a
--- /dev/null
@@ -0,0 +1,17 @@
+### this file is the same as yaml2.val
+### except that the groups are separated as documents rather than as
+### an arrayref
+
+---
+group validate_if: foo
+bar:
+  required: 1
+
+---
+group validate_if: hem
+haw: { required: 1 }
+
+---
+raspberry:
+  required: 1
+
diff --git a/t/samples/yaml_js_1.html b/t/samples/yaml_js_1.html
new file mode 100644 (file)
index 0000000..7b972b4
--- /dev/null
@@ -0,0 +1,62 @@
+<html>
+<title>Yaml Test</title>
+<body>
+
+<table border=1 cellspacing=0>
+  <tr>
+    <td colspan=2>
+<script src="../../lib/CGI/Ex/yaml_load.js"></script>
+<script>
+
+if (! document.yaml_load) {
+  document.writeln('<span style="color:red"><h1>Missing document.yaml_load</h1>Path to ../../lib/CGI/Ex/yaml_load.js may be invalid.</span>');
+} else {
+  document.writeln('<span style="color:green"><h1>Found document.yaml_load</h1></span>');
+}
+
+</script>
+    </td>
+  </tr>
+
+  <tr><th colspan=2>YAML text</th></tr>
+  <tr>
+    <td colspan=2>
+<pre><script>
+var yaml = "foo: bar\nbaz: bee\nhem: haw\n";
+document.write(yaml)
+</script></pre>
+    </td>
+  </tr>
+
+
+  <tr>
+    <th>Produces</th><th>Should look like</th>
+  </tr>
+  <tr>
+    <td>
+<pre><script>
+var t1 = new Date();
+var y  = document.yaml_load(yaml)
+var t2 = new Date();
+document.write(document.js_dump(y));
+</script></pre>
+    </td>
+    <td>
+<pre>Dump:
+[obj].0.baz=bee
+[obj].0.foo=bar
+[obj].0.hem=haw
+</pre>
+    </td>
+  </tr>
+
+  <tr>
+    <td colspan=2>
+<script>
+document.write("Elapsed time: "+((t2.getTime() - t1.getTime())/1000)+" seconds");
+</script>
+    </td>
+  </tr>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/t/samples/yaml_js_2.html b/t/samples/yaml_js_2.html
new file mode 100644 (file)
index 0000000..c651eae
--- /dev/null
@@ -0,0 +1,114 @@
+<html>
+<title>Yaml Test</title>
+<body>
+
+<table border=1 cellspacing=0>
+  <tr>
+    <td colspan=2>
+<script src="../../lib/CGI/Ex/yaml_load.js"></script>
+<script>
+
+if (! document.yaml_load) {
+  document.writeln('<span style="color:red"><h1>Missing document.yaml_load</h1>Path to ../../lib/CGI/Ex/yaml_load.js may be invalid.</span>');
+} else {
+  document.writeln('<span style="color:green"><h1>Found document.yaml_load</h1></span>');
+}
+
+</script>
+    </td>
+  </tr>
+
+  <tr><th colspan=2>YAML text</th></tr>
+  <tr>
+    <td colspan=2>
+<pre><script>
+var yaml = "--- #YAML:1.0\n\
+- foo1: bar1\n\
+  foo2: {key1: val1, key2: 'value 2'}\n\
+  foo3:\n\
+   - a\n\
+   - list\n\
+   - of\n\
+   - items\n\
+   - 'with the last item being a long string'\n\
+  foo4: [another, list, of, values]\n\
+  foo5a: |\n\
+   A block of text\n\
+   that is on multiple lines.\n\
+  foo5b: |+\n\
+   A block\n\
+   of text\n\
+    that is on\n\
+   multiple lines.\n\
+  foo6a: >\n\
+   A block\n\
+   of text\n\
+    that is on\n\
+   multiple lines and is folded.\n\
+  foo6b: >+\n\
+   A block\n\
+   of text\n\
+    that is on\n\
+   multiple lines and is folded.\n\
+  foo7: 'singlequoted''with embedded quote'\n\
+  foo8: \"doublequoted\\\"with embedded quote\"\n\
+";
+//"
+document.write(yaml)
+</script></pre>
+    </td>
+  </tr>
+
+
+  <tr>
+    <th>Produces</th><th>Should look like</th>
+  </tr>
+  <tr>
+    <td>
+<pre><script>
+var t1 = new Date();
+var y  = document.yaml_load(yaml)
+var t2 = new Date();
+document.write(document.js_dump(y));
+</script></pre>
+    </td>
+    <td>
+<pre>Dump:
+[obj].0.0.foo1=bar1
+[obj].0.0.foo2.key1=val1
+[obj].0.0.foo2.key2=value 2
+[obj].0.0.foo3.0=a
+[obj].0.0.foo3.1=list
+[obj].0.0.foo3.2=of
+[obj].0.0.foo3.3=items
+[obj].0.0.foo3.4=with the last item being a long string
+[obj].0.0.foo4.0=another
+[obj].0.0.foo4.1=list
+[obj].0.0.foo4.2=of
+[obj].0.0.foo4.3=values
+[obj].0.0.foo5a=A block of text
+that is on multiple lines.
+[obj].0.0.foo5b=A block
+of text
+ that is on
+multiple lines.
+
+[obj].0.0.foo6a=A block of text that is on multiple lines and is folded.
+[obj].0.0.foo6b=A block of text that is on multiple lines and is folded.
+
+[obj].0.0.foo7=singlequoted'with embedded quote
+[obj].0.0.foo8=doublequoted"with embedded quote
+</pre>
+    </td>
+  </tr>
+
+  <tr>
+    <td colspan=2>
+<script>
+document.write("Elapsed time: "+((t2.getTime() - t1.getTime())/1000)+" seconds");
+</script>
+    </td>
+  </tr>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/t/samples/yaml_js_3.html b/t/samples/yaml_js_3.html
new file mode 100644 (file)
index 0000000..acdd323
--- /dev/null
@@ -0,0 +1,89 @@
+<html>
+<title>Yaml Test</title>
+<body>
+
+<table border=1 cellspacing=0>
+  <tr>
+    <td colspan=2>
+<script src="../../lib/CGI/Ex/yaml_load.js"></script>
+<script>
+
+if (! document.yaml_load) {
+  document.writeln('<span style="color:red"><h1>Missing document.yaml_load</h1>Path to ../../lib/CGI/Ex/yaml_load.js may be invalid.</span>');
+} else {
+  document.writeln('<span style="color:green"><h1>Found document.yaml_load</h1></span>');
+}
+
+</script>
+    </td>
+  </tr>
+
+  <tr><th colspan=2>YAML text</th></tr>
+  <tr>
+    <td colspan=2>
+<pre><script>
+var yaml = "\n\
+key1_a: &foo1 val1\n\
+key2_a: &foo2 { skey2_1: sval2_1 }\n\
+key3_a: &foo3\n\
+  skey3_1: sval3_1\n\
+key4_a: &foo4 [ sval4_1, sval4_2 ]\n\
+key5_a: &foo5\n\
+  - sval5_1\n\
+  - sval5_2\n\
+\n\
+key1_b: *foo1\n\
+key2_b: *foo2\n\
+key3_b: *foo3\n\
+key4_b: *foo4\n\
+key5_b: *foo5\n\
+";
+
+document.write(yaml)
+</script></pre>
+    </td>
+  </tr>
+
+
+  <tr>
+    <th>Produces</th><th>Should look like</th>
+  </tr>
+  <tr>
+    <td>
+<pre><script>
+var t1 = new Date();
+var y  = document.yaml_load(yaml)
+var t2 = new Date();
+document.write(document.js_dump(y));
+</script></pre>
+    </td>
+    <td>
+<pre>Dump:
+[obj].0.key1_a=val1
+[obj].0.key1_b=val1
+[obj].0.key2_a.skey2_1=sval2_1 
+[obj].0.key2_b.skey2_1=sval2_1 
+[obj].0.key3_a.skey3_1=sval3_1
+[obj].0.key3_b.skey3_1=sval3_1
+[obj].0.key4_a.0=sval4_1
+[obj].0.key4_a.1=sval4_2 
+[obj].0.key4_b.0=sval4_1
+[obj].0.key4_b.1=sval4_2 
+[obj].0.key5_a.0=sval5_1
+[obj].0.key5_a.1=sval5_2
+[obj].0.key5_b.0=sval5_1
+[obj].0.key5_b.1=sval5_2
+</pre>
+    </td>
+  </tr>
+
+  <tr>
+    <td colspan=2>
+<script>
+document.write("Elapsed time: "+((t2.getTime() - t1.getTime())/1000)+" seconds");
+</script>
+    </td>
+  </tr>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/t/samples/yaml_js_4.html b/t/samples/yaml_js_4.html
new file mode 100644 (file)
index 0000000..f05d3db
--- /dev/null
@@ -0,0 +1,70 @@
+<html>
+<title>Yaml Test</title>
+<body>
+
+<table border=1 cellspacing=0>
+  <tr>
+    <td colspan=2>
+<script src="../../lib/CGI/Ex/yaml_load.js"></script>
+<script>
+
+if (! document.yaml_load) {
+  document.writeln('<span style="color:red"><h1>Missing document.yaml_load</h1>Path to ../../lib/CGI/Ex/yaml_load.js may be invalid.</span>');
+} else {
+  document.writeln('<span style="color:green"><h1>Found document.yaml_load</h1></span>');
+}
+
+</script>
+    </td>
+  </tr>
+
+  <tr><th colspan=2>YAML text</th></tr>
+  <tr>
+    <td colspan=2>
+<pre><script>
+var yaml = "---\n\
+foo: bar\n\
+---\n\
+- baz\n\
+- bee\n\
+---\n\
+hem: haw\n\
+";
+document.write(yaml)
+</script></pre>
+    </td>
+  </tr>
+
+
+  <tr>
+    <th>Produces</th><th>Should look like</th>
+  </tr>
+  <tr>
+    <td>
+<pre><script>
+var t1 = new Date();
+var y  = document.yaml_load(yaml)
+var t2 = new Date();
+document.write(document.js_dump(y));
+</script></pre>
+    </td>
+    <td>
+<pre>Dump:
+[obj].0.foo=bar
+[obj].1.0=baz
+[obj].1.1=bee
+[obj].2.hem=haw
+</pre>
+    </td>
+  </tr>
+
+  <tr>
+    <td colspan=2>
+<script>
+document.write("Elapsed time: "+((t2.getTime() - t1.getTime())/1000)+" seconds");
+</script>
+    </td>
+  </tr>
+
+</body>
+</html>
\ No newline at end of file
This page took 0.309494 seconds and 4 git commands to generate.