]> Dogcows Code - chaz/p5-CGI-Ex/blobdiff - lib/CGI/Ex/App.pod
CGI::Ex 2.16
[chaz/p5-CGI-Ex] / lib / CGI / Ex / App.pod
index 0188cdc40e6c0d38d508384a491d956f05fd4094..3b93a78383b9d490976b7f1e6456d9442c5c2cfe 100644 (file)
@@ -132,8 +132,10 @@ How about a form with validation (inluding javascript validation)...
 There are infinite possibilities.  There is a longer "SYNOPSIS" after
 the process flow discussion and more examples near the end of this
 document.  It is interesting to note that there have been no databases
 There are infinite possibilities.  There is a longer "SYNOPSIS" after
 the process flow discussion and more examples near the end of this
 document.  It is interesting to note that there have been no databases
-so far.  CGI::Ex::App is Controller/Viewer that is somewhat Model
-agnostic.
+so far.  It is very, very difficult to find a single database
+abstraction that fits every model.  CGI::Ex::App is Controller/Viewer
+that is somewhat Model agnostic and doesn't come with any default
+database abstraction.
 
 =head1 DESCRIPTION
 
 
 =head1 DESCRIPTION
 
@@ -202,6 +204,9 @@ The nav_loop method will run as follows:
 
         foreach step of path {
 
 
         foreach step of path {
 
+            ->require_auth (hook)
+                # exits nav_loop if true
+
             ->morph
                 # check ->allow_morph
                 # check ->allow_nested_morph
             ->morph
                 # check ->allow_morph
                 # check ->allow_nested_morph
@@ -242,16 +247,16 @@ during the run_step hook.
 
     run_step {
         ->pre_step (hook)
 
     run_step {
         ->pre_step (hook)
-            # exits nav_loop if true
+            # skips this step if true and exit nav_loop
 
         ->skip (hook)
 
         ->skip (hook)
-            # skips this step if true (stays in nav_loop)
+            # skips this step if true and stays in nav_loop
 
         ->prepare (hook - defaults to true)
 
         ->info_complete (hook - ran if prepare was true)
             ->ready_validate (hook)
 
         ->prepare (hook - defaults to true)
 
         ->info_complete (hook - ran if prepare was true)
             ->ready_validate (hook)
-            return false if ! ready_validate
+                # returns false from info_complete if ! ready_validate
             ->validate (hook - uses CGI::Ex::Validate to validate form info)
                 ->hash_validation (hook)
                    ->file_val (hook)
             ->validate (hook - uses CGI::Ex::Validate to validate form info)
                 ->hash_validation (hook)
                    ->file_val (hook)
@@ -260,7 +265,7 @@ during the run_step hook.
                        ->name_module
                        ->name_step
                        ->ext_val
                        ->name_module
                        ->name_step
                        ->ext_val
-            returns true if validate is true or if nothing to validate
+            returns true if validate is true or if nothing to validate
 
         ->finalize (hook - defaults to true - ran if prepare and info_complete were true)
 
 
         ->finalize (hook - defaults to true - ran if prepare and info_complete were true)
 
@@ -314,31 +319,31 @@ The default out of the box configuration will map URIs to steps as follows:
 
     # Assuming /cgi-bin/my_app is the program being run
 
 
     # Assuming /cgi-bin/my_app is the program being run
 
-    URI:  /cgi-bin/my_app
+   URI:  /cgi-bin/my_app
     STEP: main
     FORM: {}
     WHY:  No other information is passed.  The path method is
           called which eventually calls ->default_step which
           defaults to "main"
 
     STEP: main
     FORM: {}
     WHY:  No other information is passed.  The path method is
           called which eventually calls ->default_step which
           defaults to "main"
 
-    URI:  /cgi-bin/my_app?foo=bar
+   URI:  /cgi-bin/my_app?foo=bar
     STEP: main
     FORM: {foo => "bar"}
     WHY:  Same as previous example except that QUERY_STRING
           information was passed and placed in form.
 
     STEP: main
     FORM: {foo => "bar"}
     WHY:  Same as previous example except that QUERY_STRING
           information was passed and placed in form.
 
-    URI:  /cgi-bin/my_app?step=my_step
+   URI:  /cgi-bin/my_app?step=my_step
     STEP: my_step
     FORM: {step => "my_step"}
     WHY:  The path method is called which looks in $self->form
           for the key ->step_key (which defaults to "step").
 
     STEP: my_step
     FORM: {step => "my_step"}
     WHY:  The path method is called which looks in $self->form
           for the key ->step_key (which defaults to "step").
 
-    URI:  /cgi-bin/my_app?step=my_step&foo=bar
+   URI:  /cgi-bin/my_app?step=my_step&foo=bar
     STEP: my_step
     FORM: {foo => "bar", step => "my_step"}
     STEP: my_step
     FORM: {foo => "bar", step => "my_step"}
-    WHY:  Same as before but has other parameters were passed.
+    WHY:  Same as before but another parameter was passed.
 
 
-    URI:  /cgi-bin/my_app/my_step
+   URI:  /cgi-bin/my_app/my_step
     STEP: my_step
     FORM: {step => "my_step"}
     WHY:  The path method is called which called path_info_map_base
     STEP: my_step
     FORM: {step => "my_step"}
     WHY:  The path method is called which called path_info_map_base
@@ -348,12 +353,12 @@ The default out of the box configuration will map URIs to steps as follows:
           $self->form->{$self->step_key} for the initial step. See
           the path_info_map_base method for more information.
 
           $self->form->{$self->step_key} for the initial step. See
           the path_info_map_base method for more information.
 
-    URI:  /cgi-bin/my_app/my_step?foo=bar
+   URI:  /cgi-bin/my_app/my_step?foo=bar
     STEP: my_step
     FORM: {foo => "bar", step => "my_step"}
     WHY:  Same as before but other parameters were passed.
 
     STEP: my_step
     FORM: {foo => "bar", step => "my_step"}
     WHY:  Same as before but other parameters were passed.
 
-    URI:  /cgi-bin/my_app/my_step?step=other_step
+   URI:  /cgi-bin/my_app/my_step?step=other_step
     STEP: other_step
     FORM: {step => "other_step"}
     WHY:  The same procedure took place, but when the PATH_INFO
     STEP: other_step
     FORM: {step => "other_step"}
     WHY:  The same procedure took place, but when the PATH_INFO
@@ -371,7 +376,7 @@ that the following method is installed in your script.
         ];
     }
 
         ];
     }
 
-    URI:  /cgi-bin/my_app/my_step/bar
+   URI:  /cgi-bin/my_app/my_step/bar
     STEP: my_step
     FORM: {foo => "bar"}
     WHY:  The step was matched as in previous examples using
     STEP: my_step
     FORM: {foo => "bar"}
     WHY:  The step was matched as in previous examples using
@@ -381,7 +386,7 @@ that the following method is installed in your script.
           and the corresponding matched value was placed into
           the form using the keys specified following the regex.
 
           and the corresponding matched value was placed into
           the form using the keys specified following the regex.
 
-    URI:  /cgi-bin/my_app/my_step/bar/1234
+   URI:  /cgi-bin/my_app/my_step/bar/1234
     STEP: my_step
     FORM: {foo => "bar", id => "1234"}
     WHY:  Same as the previous example, except that the first
     STEP: my_step
     FORM: {foo => "bar", id => "1234"}
     WHY:  Same as the previous example, except that the first
@@ -391,19 +396,19 @@ that the following method is installed in your script.
           order that will match the most data.  The third regex
           would also match this PATH_INFO.
 
           order that will match the most data.  The third regex
           would also match this PATH_INFO.
 
-    URI:  /cgi-bin/my_app/my_step/some/other/type/of/data
+   URI:  /cgi-bin/my_app/my_step/some/other/type/of/data
     STEP: my_step
     FORM: {anything_else => 'some/other/type/of/data'}
     WHY:  Same as the previous example, except that the third
           regex matched.
 
     STEP: my_step
     FORM: {anything_else => 'some/other/type/of/data'}
     WHY:  Same as the previous example, except that the third
           regex matched.
 
-    URI:  /cgi-bin/my_app/my_step/bar?bling=blang
+   URI:  /cgi-bin/my_app/my_step/bar?bling=blang
     STEP: my_step
     FORM: {foo => "bar", bling => "blang"}
     STEP: my_step
     FORM: {foo => "bar", bling => "blang"}
-    WHY:  Same as the first step, but additional QUERY_STRING
+    WHY:  Same as the first sample, but additional QUERY_STRING
           information was passed.
 
           information was passed.
 
-    URI:  /cgi-bin/my_app/my_step/one%20two?bar=three%20four
+   URI:  /cgi-bin/my_app/my_step/one%20two?bar=three%20four
     STEP: my_step
     FORM: {anything_else => "one two", bar => "three four"}
     WHY:  The third path_info_map regex matched.  Note that the
     STEP: my_step
     FORM: {anything_else => "one two", bar => "three four"}
     WHY:  The third path_info_map regex matched.  Note that the
@@ -440,7 +445,7 @@ for more information about the many ways you can validate your data.
 The default hash_validation hook returns an empty hashref.  This means that passed
 in data is all valid and the script will automatically call the step's finalize method.
 
 The default hash_validation hook returns an empty hashref.  This means that passed
 in data is all valid and the script will automatically call the step's finalize method.
 
-The following shows how to some contrived validation to a step called "my_step".
+The following shows how to add some contrived validation to a step called "my_step".
 
     sub my_step_hash_validation {
         return {
 
     sub my_step_hash_validation {
         return {
@@ -564,7 +569,9 @@ validation files).
 
 The default file_print hook will look for content on your file system,
 but it can also be completely overridden to return a reference to a
 
 The default file_print hook will look for content on your file system,
 but it can also be completely overridden to return a reference to a
-scalar containing the contents of your file.  Actually it can return
+scalar containing the contents of your file (beginning with version 2.14
+string references can be cached which makes templates passed this way
+"first class" citizens).  Actually it can return
 anything that Template::Alloy (Template::Toolkit compatible) will
 treat as input.  This templated html is displayed to the user during
 any step that enters the "print" phase.
 anything that Template::Alloy (Template::Toolkit compatible) will
 treat as input.  This templated html is displayed to the user during
 any step that enters the "print" phase.
@@ -1108,8 +1115,8 @@ The following items will be cleared:
     path
     path_i
     history
     path
     path_i
     history
-    __morph_lineage_start_index
-    __morph_lineage
+    _morph_lineage_start_index
+    _morph_lineage
     hash_errors
     hash_fill
     hash_swap
     hash_errors
     hash_fill
     hash_swap
@@ -1171,6 +1178,7 @@ called is "view".
     debug: admin/Recipe.pm line 14
     shift->dump_history = [
             "Elapsed: 0.00562",
     debug: admin/Recipe.pm line 14
     shift->dump_history = [
             "Elapsed: 0.00562",
+            "view - require_auth - require_auth - 0.00001 - 0",
             "view - run_step - run_step - 0.00488 - 1",
             "    view - pre_step - pre_step - 0.00003 - 0",
             "    view - skip - view_skip - 0.00004 - 0",
             "view - run_step - run_step - 0.00488 - 1",
             "    view - pre_step - pre_step - 0.00003 - 0",
             "    view - skip - view_skip - 0.00004 - 0",
@@ -1196,6 +1204,11 @@ called is "view".
             "    view - post_print - post_print - 0.00003 - 0"
     ];
 
             "    view - post_print - post_print - 0.00003 - 0"
     ];
 
+=item error_step (method)
+
+Defaults to "__error".  The name of a step to run should a dying error
+be caught by the default handle_error method.  See the handle_error method.
+
 =item exit_nav_loop (method)
 
 This method should not normally used but there is no problem with
 =item exit_nav_loop (method)
 
 This method should not normally used but there is no problem with
@@ -1229,40 +1242,6 @@ then the file "foo.val" will be searched for.
 
 See the section on FINDING TEMPLATES for further discussion.
 
 
 See the section on FINDING TEMPLATES for further discussion.
 
-=item first_step (method)
-
-Returns the first step of the path.  Note that first_step may not be the same
-thing as default_step if the path was overridden.
-
-=item form (method)
-
-Returns a hashref of the items passed to the CGI.  Returns
-$self->{form} which defaults to CGI::Ex::get_form.
-
-=item handle_error (method)
-
-If anything dies during execution, handle_error will be called with
-the error that had happened.  Default action is to die with that error.
-
-=item history (method)
-
-Returns an arrayref which contains trace history of which hooks of
-which steps 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.
-
-The dump_history method shows a short condensed version of this
-history which makes it easier to see what path was followed.
-
-In general, the arrayref is free for anything to push onto which will
-help in tracking other occurrences in the program as well.
-
-=item init (method)
-
-Called by the default new method.  Allows for any object
-initilizations that may need to take place.  Default action does
-nothing.
-
 =item fill_args (hook)
 
 Returns a hashref of args that will be passed to the CGI::Ex::Fill::fill.
 =item fill_args (hook)
 
 Returns a hashref of args that will be passed to the CGI::Ex::Fill::fill.
@@ -1318,7 +1297,7 @@ The file should be readable by CGI::Ex::Validate::get_validation.
 
 This hook is only necessary if the hash_validation hook has not been
 overridden.
 
 This hook is only necessary if the hash_validation hook has not been
 overridden.
-
+5B
 This method an also return a hashref containing the validation - but
 then you may have wanted to override the hash_validation hook.
 
 This method an also return a hashref containing the validation - but
 then you may have wanted to override the hash_validation hook.
 
@@ -1362,12 +1341,22 @@ override the base package (it is still OK to use the full method name
 
 See the run_hook method and the morph method for more details.
 
 
 See the run_hook method and the morph method for more details.
 
+=item first_step (method)
+
+Returns the first step of the path.  Note that first_step may not be the same
+thing as default_step if the path was overridden.
+
 =item forbidden_step (method)
 
 Defaults to "__forbidden".  The name of a step to run should the current
 step name be invalid, or if a step found by the default path method
 is invalid.  See the path method.
 
 =item forbidden_step (method)
 
 Defaults to "__forbidden".  The name of a step to run should the current
 step name be invalid, or if a step found by the default path method
 is invalid.  See the path method.
 
+=item form (method)
+
+Returns a hashref of the items passed to the CGI.  Returns
+$self->{form} which defaults to CGI::Ex::get_form.
+
 =item form_name (hook)
 
 Return the name of the form to attach the js validation to.  Used by
 =item form_name (hook)
 
 Return the name of the form to attach the js validation to.  Used by
@@ -1382,8 +1371,7 @@ to the authentication object during the get_valid_auth method.
 
 =item get_valid_auth (method)
 
 
 =item get_valid_auth (method)
 
-If require_auth is true at either the application level or at the
-step level, get_valid_auth will be called.
+If require_auth hook returns true on any given step then get_valid_auth will be called.
 
 It will call auth_args to get some default args to pass to
 CGI::Ex::Auth->new.  It augments the args with sensible defaults that
 
 It will call auth_args to get some default args to pass to
 CGI::Ex::Auth->new.  It augments the args with sensible defaults that
@@ -1428,6 +1416,12 @@ Full customization of the login process and the login template can
 be done via the auth_args hash.  See the auth_args method and
 CGI::Ex::Auth perldoc for more information.
 
 be done via the auth_args hash.  See the auth_args method and
 CGI::Ex::Auth perldoc for more information.
 
+=item handle_error (method)
+
+If anything dies during execution, handle_error will be called with
+the error that had happened.  Default action is to try running the
+step returned by the error_step method.
+
 =item hash_base (hook)
 
 A hash of base items to be merged with hash_form - such as pulldown
 =item hash_base (hook)
 
 A hash of base items to be merged with hash_form - such as pulldown
@@ -1520,6 +1514,19 @@ 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.
 
 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 history (method)
+
+Returns an arrayref which contains trace history of which hooks of
+which steps 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.
+
+The dump_history method shows a short condensed version of this
+history which makes it easier to see what path was followed.
+
+In general, the arrayref is free for anything to push onto which will
+help in tracking other occurrences in the program as well.
+
 =item info_complete (hook)
 
 Calls the ready_validate hook to see if data is ready to validate.  If
 =item info_complete (hook)
 
 Calls the ready_validate hook to see if data is ready to validate.  If
@@ -1527,6 +1534,12 @@ so it calls the validate hook to validate the data.  Should make
 sure the data is ready and valid.  Will not be run unless
 prepare returns true (default).
 
 sure the data is ready and valid.  Will not be run unless
 prepare returns true (default).
 
+=item init (method)
+
+Called by the default new method.  Allows for any object
+initilizations that may need to take place.  Default action does
+nothing.
+
 =item insert_path (method)
 
 Arguments are the steps to insert.  Can be called any time.  Inserts
 =item insert_path (method)
 
 Arguments are the steps to insert.  Can be called any time.  Inserts
@@ -1772,8 +1785,9 @@ This starts the process flow for the path and its steps.
 
 =item navigate_authenticated (method)
 
 
 =item navigate_authenticated (method)
 
-Same as the method navigate but sets require_auth(1) before
-running.  See the require_auth method.
+Same as the method navigate but calls ->require_auth(1) before
+running.  It will only work if the navigate_authenticated method
+has not been overwritten. See the require_auth method.
 
 =item new (class method)
 
 
 =item new (class method)
 
@@ -2022,45 +2036,104 @@ had been successfully validated and acted upon.
 Arguments are the steps used to replace.  Can be called any time.
 Replaces the remaining steps (if any) of the current path.
 
 Arguments are the steps used to replace.  Can be called any time.
 Replaces the remaining steps (if any) of the current path.
 
-=item require_auth (method)
+=item require_auth (hook)
 
 
-Default undef.  Can return either a true value or a hashref of step names.
+Defaults to self->{require_auth} which defaults to undef.
+If called as a method and passed a single value of 1, 0, or undef it will
+set the value of $self->{require_auth} to that value.  If set to a true
+value then any subsequent step will require authentication (unless its
+hook has been overwritten).
 
 
-If a hashref of stepnames is returned, authentication will be turned on
-at the step level.  In this mode if any step is accessed, the get_valid_auth
-method will be called.  If it fails, then the nav_loop will be stopped
-(the post_navigate method will be called - use the is_authed method to perform
-different functions).  Any step of the path not in the hash will not require
-authentication.  For example, to add authentication to add authentication
-to the add, edit and delete steps you could do:
+Any of the following ways can be used to require authentication on
+every step.
 
 
-    sub require_auth { {add => 1, edit => 1, delete => 1} }
+=over 4
 
 
-If a non-hash true value is returned from the require_auth method then
-authentication will take place before the pre_navigation or the nav_loop methods.
-If authentication fails the navigation process is exited (the post_navigate
-method will not be called).
+=item
 
     sub require_auth { 1 }
 
 
     sub require_auth { 1 }
 
-Alternatively you can also could do either of the following:
+=item
 
     __PACKAGE__->navigate_authenticated; # instead of __PACKAGE__->navigate;
 
 
     __PACKAGE__->navigate_authenticated; # instead of __PACKAGE__->navigate;
 
-    # OR
+=item
+
+    __PACKAGE__->new({require_auth => 1}->navigate;
+
+=item
 
     sub init { shift->require_auth(1) }
 
 
     sub init { shift->require_auth(1) }
 
-    # OR
+=back
 
 
-    __PACKAGE__->new({require_auth => 1}->navigate;
+Because it is called as a hook, the current step is passed as the
+first argument.  If the hook returns false, no authentication will be
+required on this step.  If the hook returns a true, non-hashref value,
+authentication will be required via the get_valid_auth method.  If the
+method returns a hashref of stepnames to require authentication on,
+the step will require authentication via the get_valid_auth method if
+the current step is in the hashref.  If authentication is required and
+succeeds, the step will proceed.  If authentication is required and
+fails at the step level the current step will be aborted,
+authentication will be asked for (the post_navigate method will still
+be called).
+
+For example you could add authentication to the add, edit, and delete
+steps in any of the following ways:
 
 
-If get_valid_auth returns true, in either case, the is_authed method will
-return true and the auth_data will contain the authenticated user's data.
-If it returns false, auth_data may possibly contain a defined but false
-data object with details as to why authentication failed.
+=over 4
 
 
-See the get_valid_auth method.
+=item
+
+    sub require_auth { {add => 1, edit => 1, delete => 1} }
+
+=item
+
+    sub add_require_auth    { 1 }
+    sub edit_require_auth   { 1 }
+    sub delete_require_auth { 1 }
+
+=item
+
+    sub require_auth {
+        my ($self, $step) = @_;
+        return 1 if $step && $step =~ /^(add|edit|delete)$/;
+        return 0;
+    }
+
+=back
+
+If however you wanted to require authentication on all but one or two methods
+(such as requiring authentication on all but a forgot_password step) you could do
+either of the following:
+
+=over 4
+
+=item
+
+    sub require_auth {
+        my ($self, $step) = @_;
+        return 0 if $step && $step eq 'forgot_password';
+        return 1; # require auth on all other steps
+    }
+
+=item
+
+    sub require_auth { 1 } # turn it on for all steps
+
+    sub forgot_password_require_auth { 0 } # turn it off
+
+=back
+
+See the get_valid_auth method for what occurs should authentication be required.
+
+There is one key difference from the 2.14 version of App.  In 2.14 and
+previous versions, the pre_navigate and post_navigate methods would
+not be called if require_auth returned a true non-hashref value.  In
+version 2.15 and later, the 2.15 pre_navigate and post_navigate
+methods are always called - even if authentication fails.  Also in 2.15
+and later, the method is called as a hook meaning the step is passed in.
 
 =item run_hook (method)
 
 
 =item run_hook (method)
 
@@ -2601,7 +2674,7 @@ the core logic of the application.
             return 0;
         }
 
             return 0;
         }
 
-        my $s = "UPDATE recipe SET title = ?, ingredients = ?, directions = ? WHERE id = ?";
+        $s = "UPDATE recipe SET title = ?, ingredients = ?, directions = ? WHERE id = ?";
         $self->dbh->do($s, {}, $form->{'title'},
                                $form->{'ingredients'},
                                $form->{'directions'},
         $self->dbh->do($s, {}, $form->{'title'},
                                $form->{'ingredients'},
                                $form->{'directions'},
@@ -2983,6 +3056,18 @@ will cause all steps to require validation):
 
     sub require_auth { {add => 1, edit => 1, delete => 1} }
 
 
     sub require_auth { {add => 1, edit => 1, delete => 1} }
 
+We could also enable authentication by using individual hooks as in:
+
+    sub add_require_auth    { 1 }
+    sub edit_require_auth   { 1 }
+    sub delete_require_auth { 1 }
+
+Or we could require authentication on everything - but let a few steps in:
+
+    sub require_auth { 1 }      # turn authentication on for all
+    sub main_require_auth { 0 } # turn it off for main and view
+    sub view_require_auth { 0 }
+
 That's it.  The add, edit, and delete steps will now require authentication.
 See the require_auth, get_valid_auth, and auth_args methods for more information.
 Also see the L<CGI::Ex::Auth> perldoc.
 That's it.  The add, edit, and delete steps will now require authentication.
 See the require_auth, get_valid_auth, and auth_args methods for more information.
 Also see the L<CGI::Ex::Auth> perldoc.
@@ -2992,20 +3077,22 @@ Also see the L<CGI::Ex::Auth> perldoc.
 The following corporation and individuals contributed in some part to
 the original versions.
 
 The following corporation and individuals contributed in some part to
 the original versions.
 
-    Bizhosting.com - giving a problem that fit basic design patterns.
+    Bizhosting.com  - giving a problem that fit basic design patterns.
 
 
-    Earl Cahill    - pushing the idea of more generic frameworks.
+    Earl Cahill     - pushing the idea of more generic frameworks.
 
 
-    Adam Erickson  - design feedback, bugfixing, feature suggestions.
+    Adam Erickson   - design feedback, bugfixing, feature suggestions.
 
 
-    James Lance    - design feedback, bugfixing, feature suggestions.
-
-=head1 AUTHOR
+    James Lance     - design feedback, bugfixing, feature suggestions.
 
 
-Paul Seamons <paul at seamons dot com>
+    Krassimir Berov - feedback and some warnings issues with POD examples.
 
 =head1 LICENSE
 
 This module may be distributed under the same terms as Perl itself.
 
 
 =head1 LICENSE
 
 This module may be distributed under the same terms as Perl itself.
 
+=head1 AUTHOR
+
+Paul Seamons <perl at seamons dot com>
+
 =cut
 =cut
This page took 0.036589 seconds and 4 git commands to generate.