The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

CGI::Path - module to aid in traversing one or more paths

SYNOPSIS

CGI::Path allows for easy navigation through a set of steps, a path. It uses a session extensively (managed by default via Apache::Session) to hopefully simplify path based cgis.

A PATH

A path is a package, like CGI::Path::Skel. The path needs to be @ISA CGI::Path. The package can contain the step methods as described below. You can also make a directory for the path, like CGI/Path/Skel, where the direectory will contain a package for each step. This could be done from your $ENV{PERL5LIB}.

path_hash

The path_hash is what helps generate the path_array, which is just an array of steps. It is a hash to allow for easy overrides, since it is sort of hard to override just the third element of an array through a simple new.

The path_hash needs a key named 'initial_step', and then steps that point down the line, like so

  path_hash => {
    initial_step => 'page_one',
    page_one     => 'page_two',
    page_two     => 'page_three',
  },

since page_three doesn't point anywhere, the path_array ends. You can just override $self->path_hash, and have it return a hash ref as above.

path_array

The path_array is formed from path_hash. It is an array ref of the steps in the path.

my_module

my_module by default is something like CGI::Path::Skel. You can override $self->my_module and have it return a scalar containing your my_module. Module overrides are done based on my_module.

my_content

my_module by default is something like path/skel. It defaults to a variant of my_module. You can override $self->my_content and have it return a scalar your my_content. html content gets printed based on my_content.

include_path

include_path is a method that returns the include_path to look through for files. I suggest returning an array ref, even if it only contains one element.

navigate

$self->navigate walks through a path of steps, where each step corresponds to a .htm content file and a .val validation hash.

A step corresponds to a .htm content file. The .htm and .val need to share the base same name.

$self->{this_step} is hash ref containing the following

  previous_step => the last step
  this_step     => the current step
  validate_ref  => the validation ref for the current step

Generally, navigate generates the form (see below), and for each step does the following

  --  Get the validate ref (val_ref) for the given page
  --  Comparing the val_ref to the form see if info exists for the step
  --  Validate according to the val_ref
  --  If validation fails, or if info doesn't exist, process the page and stop

More specifically, the following methods can be called for a step, in the given order.

step details/possible uses --------------------------------------------- ${step}_hook_pre initializations, must return 1 or step gets skipped info_exists checks to see if you have info for this step ${step}_info_complete can be used to make sure you have all the info you need

  validate                contains the following
  ${step}_pre_validate    stuff to check before validate proper
  validate_proper         runs the .val file validation
  ${step}_post_validate   stuff to run after validate proper

  ${step}_hash_fill       return a hash ref of things to add to $self->fill
                          fill is a hash ref of what fills the forms
  ${step}_hash_form       perhaps set stuff for $self->{my_form}
                          my_form is a hash ref that gets passed to the process method
  ${step}_hash_errors     set errors
  ${step}_step            do actual stuff for the step
  ${step}_hook_post       last chance

VALIDATION

The three validation methods mentioned above in navigate (pre, proper, post) handle validation. The pre and post methods are hooks so you could do whatever custom validation you like. By default, validate_proper is handled by CGI::Ex::Validate. See the CGI::Ex::Validate for details. If any of the three methods find validation errors for a given page, that page is displayed. If the page was immediately shown before, errors are passed to the page.

.val files By default, .val files are turned into refs by YAML. See the YAML for details. This could easily be changed by writing your own conf_read method, which takes the full path to a file and returns a ref. The ref that gets returned needs to nicely match the structure required by validate_proper.

JAVASCRIPT VALIDATION

Javascript validation is generated by generate_js_validation based on the .val ref. generate_js_validation uses CGI::Ex::Validate by default. See the CGI::Ex::Validate for details. You need to put a js_validation tag on your page to get the validation. The form name is MYFORM by default, but can be changed by setting $self->{form_name}.

generate_form

The goal is that the programmer just look at $self->form for form or session information. To help facilitate this goal, I use the following

  $self->this_form           - form from the current hit
  $self->{session_only} = [] - things that get deleted from this_form and get inserted from the session
  $self->{session_wins} = [] - this_form wins by default, set this if you want something just from the session

The code then sets the form with the following line

  $self->{form} = {%{$self->session}, %{$this_form}, %{$form}};

Session management

CGI::Path uses Apache::Session::File by default for session management. If you use this default you will need to write the following methods

  session_dir      - returns the directory where the session files will go
  session_lock_dir - returns the directory where the session lock files will go

One failing of Apache::Session::File is it not working so hot over NFS. Sorry, patches were not readily accepted.

magic_fill

magic_fill is written to help aid in rapid development. It is a simple, space-delimited file of key/value pairs, like so

  address                       123 Fake Street
  email,email_address,from      cpan@spack.net

I split on the first white space, then split on commas for the key names. In the above example, I would end up with a ref like this

  {
    address       => '123 Fake Street',
    email         => 'cpan@spack.net',
    email_address => 'cpan@spack.net',
    from          => 'cpan@spack.net',
  }

Once I have a ref, those values will get filled into forms as pages are displayed. Makes it nice to fill forms with dummy data and test the flow of your script.

magic_fill is turned off by default. The method allow_magic_fill determines if magic_fill is on. By default, allow_magic_fill just looks at $self->{allow_magic_fill} and returns true or false accordingly. magic_fill_filename points to the location of your file.

When you new up your CGI::Path object you just need to do something like the following

my $self = CGI::Path->new({ allow_magic_fill => 1, magic_fill_filename => "/path/to/magic_fill_file", });

You can use variable values using the magic_fill_interpolation_hash. By default, you can use Template::Toolkit tags, like so

currenttime [% localtime %]

Currently, the following are included by default in the magic_fill_interpolation_hash

  script    - a good guess at the name of your script
  _script   - the stuff after the last _ in the above script
  localtime - scalar (localtime),
  time      - time,

I also include %ENV

Two other keys are not available by default, based on micro seconds namely

  micro      - join(".", &Time::HiRes::gettimeofday()), which really tries to get you a unique value
  micro_part - (&Time::HiRes::gettimeofday())[1];, which is just the micro seconds

To make these swaps available you need to set $self->{allow_magic_micro} to a true value.

allow_history

At least partly due to the sometimes complicated nature of paths, I have found it difficult to debug what is actually going on during navigation. The history features of CGI::Path aim to try and make things a little easier to follow. The goal is for each step to get added to the history and get spit out to a popup window, showing in hopefully easy to read fashion, what happened that led to the step being printed.

To turn on the history features, the allow_history method needs to return true. The default method checks to see that both $self->{allow_history} and $self->form->{$self->{history_key}} are true. The default history_key is named history. When CGI::Path finds that history is allowed $self->{history_key} gets saved to the session, which means the history window will pop up for each step so long as you stay in the session, and don't stop allowing history.

This is a rather new feature and I have now likely missed adding history to a few steps. Please let me know where other history might be useful.

OBJECT KEYS

Here is a listing of some keys in the object, listed with default values.

  ### turn on keeping history, $self->form->{$self->{history_key}} also needs to be true
  allow_history         => 0,
  ### history_key is the key from the form to turn on history
  history_key           => 'history',

  ### turn on magic fill
  allow_magic_fill      => 0,
  ### turn on micro seconds, which requires Time::HiRes
  allow_magic_micro     => 0,
  ### full path to the magic_fill file
  magic_fill_filename   => '',

  ### if a given page doesn't exist, create it using create_page method
  create_page           => 0,

  ### form_name is used for javascript
  form_name             => 'MYFORM',

  ### extension for htm files
  htm_extension         => 'htm',
  ### extension for validation files 
  val_extension         => 'val',

  ### if the user submits an empty form, keep the session
  keep_no_form_session  => 0,

  ### 'fake keys', stuff that gets skipped from the session
  not_a_real_key        => [qw(_begin_time _http_referer _printed_pages _session_id _submit _validated)],

  ### sort of a linked list of the path
  path_hash             => {
#      simple example
#      initial_step       => 'page1',
#      page1              => 'page2',
#      page2              => 'page3',
#      page3              => '',
  },

  ### used for requiring in files
  perl5lib              => $ENV{PERL5LIB} || '',

  ### only get these values from the session
  session_only          => ['_validated'],
  ### if these values are in the session and form, the session wins
  session_wins          => [],
  ### sometimes you might not want to use a session
  use_session           => 1,

  ### what got validated on this request
  validated_fresh       => {},

  ### a history of bless'ings
  WASA                  => [],

handle_jump_around

handle_jump_around tries to handle when users jump around. Like when they get to the third page, then jump back to the first page and resubmit something there. handle_jump_around would hopefully figure out that the user just submitted the first page, so the path gets set back there. handle_jump_around then looks through the validation refs for subsequent pages and wipes all the keys that are marked with WipeOnBack. This allows you to wipe enough keys so the pages will be redisplayed, but the user's information doesn't need to all get re-entered.

I have used keys such as enter_info_submitted, a hidden key on the enter_info page as a WipeOnBack key. Then, if the user submits a page before enter_info, the enter_info_submitted key gets wiped and consequently the enter_info page gets redisplayed, but with all the user's info still entered.

handle_unvalidated_keys

handle_unvalidated_keys looks through the generated form for keys that haven't yet been validated. If handle_unvalidated_keys finds a key that is in a subsequent validation ref, it will save it. This allows for bouncing into a cgi with all the information to get to say the receipt page, without show the all the pages.

MULTIPLE PATHS

It is quite easy to look at $ENV{PATH_INFO} and control multiple paths through a single cgi. I offer the following as a simple example

sub path_hash { my $self = shift; my $sub_path = ''; if($ENV{PATH_INFO} && $ENV{PATH_INFO} =~ m@/(\w+)@) { $sub_path = $1; } my $sub_path_hash = { '' => { initial_step => 'main', main => '', }, };

  ### this is the generic path for adding something
  if($sub_path =~ /^add_(\w+)$/ && !exists $sub_path_hash->{$sub_path}) {
    $sub_path_hash->{$sub_path} = {
      initial_step          => $sub_path,
      $sub_path             => "${sub_path}_confirm",
      "${sub_path}_confirm" => "${sub_path}_receipt",
    };
  }
  $sub_path = '' unless(exists $sub_path_hash->{$sub_path});
  return $sub_path_hash->{$sub_path};
}

The above path_hash method was used to manage a series of distinct add paths. Distinct paths added users, categories, blogs and entries. Each path was to handled differently, but they each had a path similar to the add_user path, which looked like this

add_user => add_user_confirm => add_user_receipt

APOLOGIES

This system is based on one that has been used to write some rather intense cgis, responsible for lots of pressure, traffic and dollars. That said, CGI::Path is still in sort of alpha stages, where I am still trying to get everything in its right place. That has been harder than I had hoped. It is also based heavily on CGI::Ex, also newly open sourced, but based on I feel, steady technology. I am hoping to one day soon get a nice, easy to follow example of a three step path that allows a user to enter information, confirm the information and then send an email. But for now, I am just trying to get everything working. The docs should improve in time.

VISION

Keeping the above APOLOGIES in mind, I think CGI::Path could change how the Perl world writes cgis. I have begun to use it for even doing two page paths. Not having to worry about navigation, validation and the like has been oh so nice. Once the navigation method is understood, a programmer just ties in where he needs to, and writes little else. The cgi mailer described above would require writing the htm, .val files, and the methods to actually send the email.

AUTHOR

Copyright 2003-2004, Earl J. Cahill. All rights reserved.

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

Address bug reports and comments to: cpan@spack.net.

When sending bug reports, please provide the version of CGI::Path, the version of Perl, and the name and version of the operating system you are using.