Essays

State.cgi Manual


[ Comments ] [ Copyright ] [ Main contents ]


The script framework

The state.cgi script is not a normal CGI script; it is a CGI script interpreter and macro processor capable (with a little work) of being deployed in any context that requires users to enter data in successive forms.

The flow of control within state.cgi is fairly simple:

  IF (I haven't run before)
     read my initialization file;
  ELSE
     parse my stored state information;
     work out what state I am in;
     execute the stub of perl business logic for that state;
     work out what state I am in now;
     load the HTML file associated with the current state;
     perform macro-interpolation on the HTML file;
     display it;

When state.cgi is executed from within mod_perl this becomes the core of a perpetual loop.

Some explanation of the term 'state' is useful at this point. A 'state' corresponds to a stage the user is at in their interaction with the state.cgi script. There can be as few as one state in a session. Each state has three files associated with it; an HTML file, a perl stub file, and an optional Help file. (Help facilities have been removed from this version of the system.)

HTML files contain HTML and macros for such things as 'submit' buttons, that can be distinguished by perl stub files.

Perl stub files take data from the HTML form and do things with it (like stash it in a named store using CGI::ApacheState). They're executed by state.cgi, which eval()'s them, and they have access to many of the internal data structures maintained by state.cgi.

One of the internal data structures is a persistent store (stashed for safe keeping in a hidden field in the HTML form, using CGI::ApacheStore) which contains some special variables:

By checking the 'submit' button that was pressed, a perl stub file can decide to advance to the next state, rewind to the previous state, or do something weird (like edit the array of states that constitutes the script's idea of where the user is going).

A simple array of states is used for simplicity's sake (ideally it would be a non-directed cyclic graph, but writing and debugging a graph-traversing nondeterministic finite state CGI script in four weeks was beyond my ability). For most purposes this is sufficient, but in some states it is necessary to loop in that state until a desired action succeeds (such as picking a valid hostname), and in others a variety of paths may be needed (such as, to different pages to fix errors, depending on whatever the user entered into the script).


The DEFAULT.CFG file

State.cgi, and all its support files, live in the same directory. Chief among the support files is DEFAULT.CGI, which is the master initialization file for an application based on state.cgi.

DEFAULT.CGI is read at startup by state.cgi, and defines one or more CGI::State (or CGI::ApacheState) stores that state.cgi must initialize. The syntax is fairly simple:

  store (storename) {
     :
  [ storage items ]
     :
  }

The keyword 'store' identifies a collection of CGI::State 'store' entities which are contained in the same object. Each storage item defines a data type, a name, and its initial values. For example:

  store (control) {
     scalar     STATE   START
     scalar     NEXT    1
     scalar     TERM    FINISH
     array      SEQ     START   1       2       3       FINISH
  }

This creates a named store called 'control'. 'control' contains a scalar data item called STATE, the value of which is ``START''. It also contains an array called SEQ, which contains the values [START],[1],[2],[3], and [FINISH]. (Not to mention a couple of other scalar values.)

Note that all fields are tab-separated. Comments may be introduced by a hash '#' character.

Note that the usage of the term 'store' here conflicts with the usage of the term 'store' in CGI::State itself. Don't worry about it; state.cgi takes care of ensuring that any persistent stored items named in a store() declaration in DEFAULT.CFG persist between invocations.

The control store is used by state.cgi to control its flow of control from one state to the next. Other stores may be used internally to store application-specific stuff.

The control store uses the following variables:

STATE
The starting state that state.cgi is in when it is first invoked.

NEXT
The state immediately following STATE. (Used when scanning SEQ, in case there are loops.)

TERM
The finishing state. You can't go beyond this.

SEQ
An array of states, in order, identified by name.

ST_PERL
A hash (associative array); each key is the name of a state, and the value is the base name (filename without '.pl' suffix) of a perl stub associated with that state.

ST_HTML
A hash (associative array); each key is the name of a state, and the value is the base name (filename without '.html' suffix) of an HTML file associated with that state.

STORE
Not preinitialized in DEFAULT.CFG but nevertheless present, this is a hash which you, the programmer, can put anything you want into. It's used in this application to store all the persistent stuff that the user enters.


What happens at start-up

If you invoke state.cgi without any parameters (via method POST), it reads in the DEFAULT.CFI file and enters the state specified in STATE (i.e., START).

It then loads and eval()'s the perl stub file START.pl.

START.pl -- like all the stub files -- can access the information stored in the control store, and can therefore change the STATE that the program is in.

Within a stub file, the object $s contains the current control store information, while the object $c contains the current CGI::ApacheState CGI session.

To see what the control store thinks the current state is:

  print "Current state is ", $s->{STATE};

To see what the control store thinks the current SEQ is:

  print "Current SEQ is [", join("][", @{ $s->{SEQ} }), "]";

($s->{SEQ} returns a reference to the array containing the SEQ parameters.)

And so on.

Some shortcut subroutines are provided by state.cgi to tweak the state information:

  $s = forward($s);    # advance to next state

  $s = rewind($s);     # retreat to previous state

In addition, you can stick data into the STORE section of the control store:

  $s->{STORE}->{my_variable} = "foo";

And read fields from the CGI environment:

  $my_field = $c->parm(my_field); # retrieves field named my_field and

Perl stub files must end with a single line, containing:

  1;

Or do something else that returns TRUE (in perl terms).

(I suppose I should have defined the stubs of perl logic as closures sourced in from some other configuration file at startup, but that's another project. This eval() process seems to work, anyway.)

Once the stub has finished executing, anything you've prodded into the $s->{STORE} data structure will be mde permanent -- assuming, that is, you have left STATE with a value for which there is a corresponding HTML file containing a %end_form% macro. (More on macros later.)


HTML and Macro processing

In addition to being a state machine, state.cgi is also a macro processor.

After executing the perl logic code for the state it woke up in, state.cgi may be in a new state. It checks to find out what $s->{STATE} is, then uses that to look up the value of $s->{ST_HTML}->{$s->{STATE}}, i.e. the HTML file corresponding to the current state.

The HTML file is loaded, macro-processed, and displayed to the user. Then state.cgi exits (or loops, if running under mod_perl).

Macro processing is not carried out recursively (this isn't a programming language :); macros are only processed the first time they are encountered.

Macros are delimited by a ``%'' sign at start and finish; e.g. %macro%. Everything from the first to the last ``%'' sign is replaced by whatever value the macro returns.

You can pass parameters to a macro: %foo(123)% invokes macro foo() with the parameter ``123''.

(But there are no flow of control constructs and no variables, so I'm safe from accusations of gratuitous interpreter writing. For now.)

Here are the macros that you can embed in HTML being processed by state.cgi:

%date%
Insert the current date, formatted neatly.

%bgcolor%
Insert the value of $MAIN::bgcolor. (Not used.)

%debugger%
Insert the value of $MAIN::debugstring. (Not used.)

%startform%
Equivalent to calling $c->startform(), or typing <FORM METHOD=``POST'' ACTION=``state.cgi''>. You must use this macro to begin your HTML forms.

%endform%
Equivalent to typing </FORM> or calling $c->endform(), except that it also takes all the persistent data that state.cgi knows about and injects it into the form as hidden fields (by calling CGI::State::commit()). You must use this macro to end your HTML forms, or data persistence will not work.

%next(caption)%
Inserts a 'submit' button in the form, showing the text 'caption'. The button is named 'Next'; perl stubs based on the standard skeleton stubfile will interpret this as a command to advance the state. (i.e., it's a ``go forward one state'' button.)

%prev(caption)%
Inserts a 'submit' button in the form, showing the text 'caption'. The button is named 'Back'; perl stubs based on the standard skeleton stubfile will interpret this as a command to go back one state. (i.e., it's a ``go back one state'' button.)

%sub(state,caption)%
Inserts a 'submit' button in the form, showing the text 'caption'. The button is named 'state'; perl stubs based on the standard skeleton stubfile will interpret this as a command to enter the the named state. (i.e., it's a ``change to arbitrary state'' button.) For example, %sub(4,Go to state 4)% will be replaced by a submit button bearing the caption ``Go to state 4'' which will be interpreted by a standard perl stub as a command to switch to state '4'.

%input(x,y,z)%
Insert an HTML text entry field, where (X) is the name of the field, (Y) is the width of the field, and (Z) is the maximum length of the field.

%area(x,y,z)%
Insert an HTML text entry area, where (X) is the name of the field, (Y) is the number of rows, and (Z) is the number of columns.

%popup(x,y,z)%
Insert an HTML popup menu, where (X) is the name of the field, (Y) is a series of pipe-separated values, and the optional (Z) argument is a hash of values to labels, pipe-separated. Default value, if any, is obtained from $s->{STORE}->{x}. For example: %popup(color,1|2|3,1|red|2|blue|3|green)% produces a popup menu containing the colours red, blue, and green. Whatever is selected is returned in the field named 'color', and each color corresponds to a number; red is 1, blue is 2, and green is 3.

%fupld(x,y,z)%
Inserts an HTML file upload field, where (X) is the name of the field, (Y) is the width of the field, and (Z) is the maximum length of the field. (NB: this is UNTESTED IN OPERATION.)

%ckbox(x,y)%
Insert an HTML checkbox, where (X) is the name of the field and (Y) is the value of the box, if checked.

%pass(x,y,z)%
Insert an HTML password entry field, where (X) is the name of the field, (Y) is the width of the field, and (Z) is the maximum length of the field.

%scroll(v,w,x,y[,z])%
Insert a scrolling multiple-selection list widget, where:

   (v) is the name of the field
   (w) is the size of the field
   (x) 1 if multiselect is true, 0 otherwise
   (y) is a series of values, pipe-separated ( a|b|c )
   (z) is a series of labels, pipe-separated pairs ( a|b|c|d )
       
The default selected value comes from $s->{STORE}->{x}
     
e.g: %scroll(cities,2,1|2|3,1|London|2|Paris|3|New York)%;
puts up a two-line selection box called 'cities', with 'London',
'Paris' and 'New York' visible, returning values 1, 2, and 3
respectively.
 
=item %radio(v,w[,x[,y[,z]]])%

Creates a gridded array of radio buttons, where: (v) is the name of the group (w) is a set of values, pipe-separated ( a|b|c ) (x) is the number of rows to display (y) is the number of columns to display (z) is a flattened hash of labels to print by the buttons The default selected button comes from $s->{STORE}->{x}

e.g: %radio(numbers,1|2|3|4|5|6,2,3,1|one|2|two|3|three|4|four|5|five|6|six)% puts up a two row by three column grid containing buttons with the values 1..6, and the field name 'numbers'. Next to each button is printed the text of the value associated with it. A default value is pulled from $s->{STORE]->{numbers}, if set.

Note: You CANNOT print labels next to buttons without defining a 2D grid!

%ckgrp(v,w[,x[,y[,z]]])%
Creates a grid of check buttons, where: (v) is the name of the group (w) is a set of values, pipe-separated ( a|b|c ) (x) is the number of rows to display (y) is the number of columns to display (z) is a flattened hash of labels to print by the buttons Default contents, if any, come from $s->{STORE}->{x}

e.g: %ckgrp(numbers,1|2|3|4|5|6,2,3,1|one|2|two|3|three|4|four|5|five|6|six)% puts up a two row by three column grid containing buttons with the values 1..6, and the field name 'numbers'. Next to each button is printed the text of the value associated with it. A default value is pulled from $s->{STORE]->{numbers}, if set.

Note: You CANNOT print labels next to buttons without defining a 2D grid!

%var(x)%
For a given variable named (X), insert whatever is held in $s->{STORE}->{X}. (That is: %var(foo)% is replaced by the value of the persistently stored variable foo.)

It is not sensible to use this macro on a hash or a reference.

%val(x,y)%
Variable interpolation: for a variable named (X), identify the value stored against it in $s->{STORE}. Then use the hash (Y) to replace it with some other value. For example: %val(FOO,1|red|2|blue|3|green)% retrieves the value of FOO, and replaces it with ``red'' (if FOO is 1), ``blue'' (if FOO is 2), or ``green'' (if FOO is 3).

%ifdef_var(x)%
For a variable named (X), insert whatever is stored against that name if and only if it is defined. Otherwise, insert an empty string.

%if_var_eq_val(X,Y,Z)%
For a variable named (X), if its stored value is equal to (Y), insert the (Z). Otherwise insert an empty string.

For example, %if_var_eq_val(CISS,1,``CISS defined'')% returns the string 'CISS not defined', if CISS == 1.

%if_var_ne_val(X,Y,Z)%
For a variable named (X), if its stored value is not equal to (Y), insert the string (Z). Otherwise insert an empty string.

For example, %if_var_ne_val(CISS,1,``CISS not defined'')% returns the string 'CISS not defined', if CISS != 1.

%if_other_var(X,Y,Z)%
For a variable named (X), if its stored value is equal to (Y), insert the value of variable (Z). Otherwise insert an empty string.

For example, %if_other_var(foo,TRUE,bar)% is replaced by the value of variable bar if and only if the stored variable ``foo'' is equal to ``TRUE''.


Example simple form:

  <HTML>
    <HEAD>
      <TITLE>This is an example form</TITLE>
      <!-- Other document metainformation goes here -->
    </HEAD>
    <BODY>
      <H1>This is an example form</H1>
      <P>
        This is some text
      </P>
      <HR>
      <P>
        Some more text.
      </P>
      %startform%
      <TABLE BORDER="0">
        <TR>
          <TD>Enter your name:</TD>
          <TD>%input(name,10,20)%</TD>
        </TR>
        <TR>
          <TD>Enter your address:</TD>
          <TD>%area(address,5,40)%</TD>
        </TR>
      </TABLE>
      %endform%
    </BODY>
  </HTML>


Example stub perl file:

  # This file plugs into state.cgi as in-line code, and is executed
  # at line 7 of exec_script(). Thus, it inherits full access to everything
  # visible from exec_script().
  #
  # 1. Determine what state we're in by checking which 'submit' button
  #    was pressed.
  #    
  # 2. From (1), change $s->STATE and $s->NEXT accordingly.
  # 
  # 3. If a radical flow-of-control branch is indicated, re-write @{ $s->SEQ }
  # 
  # 4. Any Other Business [logic]
  #
  # 5. End with a true statement (e.g. 1;), like an olde-worlde perl4 package
  #
  #############################################################################
  #
  # 1. Do forward/back button state adjustments
  
  if (grep(/next/i, $c->param()) != 0) {
     # we have a NEXT button
     warn "FORWARD button pressed\n";
     $s = forward($s);
  } elsif (grep(/back/i, $c->param()) != 0) {
     # we have a BACK button
     warn "BACK button pressed\n";
     $s = rewind($s);
  } else {
     # something else
     warn "EEk! I couldn't tell which submit button was pressed!\n";
     warn '[', join('][', $c->param()), ']';
  } 
  
  # 3. Re-write @{$s->{'SEQ'}} if necessary
  
  # 4. Do any business logic here, like messing with CGI variables
  # in this case, we're making the values of foo, bar, and quux persistent

  my ($poot) = "";
  foreach $poot (qw(foo bar quux)) {
      $s->{'STORE'}->{$poot} = $c->param($poot);
      warn "Saved $poot: [", $s->{'STORE'}->{$poot}, "]\n";
  }
  
  # 5. And return TRUE back to exec_script() ...
  
  1;

Special Hints

mod_perl builds an Apache server with an embedded Perl interpreter. The interpreter runs at startup, and can be told to load some perl modules which set up a namespace and a registry of known CGI applications. When you run a CGI application modified for use with mod_perl for the first time, it is parsed and executed; but instead of exiting, it is kept on hand, embedded in a perl subroutine called from Apache::Registry. The CGI::Apache module provides a runtime environment that exactly mimics the CGI.pm environment. However, there are a few pitfalls:

For a simple shortcut to making a script run under mod_perl, add the following to the end of your httpd.conf file:

  #mod_perl stuff

  PerlSetupEnv On
  PerlSetEnv KeyForPerlSetEnv OK
  PerlSetVar KeyForPerlSetVar OK
  PerlSendHeader On

  Alias /myscript /absolute/filesystem/path/to/myscript.cgi
  <Location /myscript>
  SetHandler perl-script
  PerlHandler Apache::Registry
  Options ExecCGI
  </Location>


[ Comments ] [ Copyright ] [ Main contents ]