/*****************************************************************************/
/*
                                  SSI.c

This module implements a full multi-threaded, AST-driven, asynchronous HTML
Server Side Includes pre-processor.  The AST-driven nature makes the code a
little more difficult to follow, but creates a powerful, event-driven,
multi-threaded server.  All of the necessary functions implementing this module
are designed to be non-blocking.

The SSI functionality in the WASD server includes the basics of NCSA SSI,
extensions similar to Apache's XSSI (eXtended SSI), as well as WASD, OSU and
VMS idiosyncratic elements.  The module has become an even bigger nightmare as
the compatibility between WASD, OSU and "vanilla" SSIs has attempted to be
implemented and maintained.  It eventually provides a reasonably capable (but
somewhat overly complex) server-side document engine including:

    o  inclusion of server information (e.g. date, name, software)
    o  inclusion of document information (e.g. path, last modification)
    o  inclusion of custom layout directory listings
    o  innocuous DCL command execution (e.g. "SHOW TIME")
    o  privileged DCL command execution (e.g. DCL procedures)
    o  expression evaluation and user variable assignment
    o  flow control in document output (e.g. if-then-else)
    o  document access counter

An SSI statement CAN BE SPLIT OVER MULTIPLE LINES, up to a maximum dictated by
the size of the statement buffer.  Occasionally this buffer is exhausted
resulting in a buffer overflow error.  It can be set by the document using the
<!--#config buffersize="" --> directive.  The default is the longest record
length of the file but can be set to anything up to 32767.  This directive
should be the first in the SSI document.

The '\' character may be used to escape characters reserved or forbidden inside
of SSI directives.  Here are some common examples '\#', '\"', '\{', '\}'.  

The flow control statements and associated evaluations were designed for
simplicity, both for the author (who didn't want to spend a huge amount of
time building a complex expression parser, etc.) and also for the server, with
a simple scheme presumably resulting in a lower execution overhead.  It is
assumed most documents will have simple internal flows so this shouldn't be an
issue.  If very complex decision making is required it is probably better
exported to a legitimate script.

To ease the clutter often present in XSSI documents (lots of SSI statements
all over the place) the <!--#ssi ... --> statement allows multiple SSI
statements to be grouped where the Ellipsis is shown.  See the example below.
Of course only SSI statements may be included in this structure!  No plain
HTML! The leading '#' of new SSI statement ends any previous statement. The
"-->" of the "<!--#ssi" ends the last statement.

Other SSI documents may be #included with full SSI processing up to a nested
limit imposed by SSI_MAX_DEPTH.

Files #included are automatically encapsulated in <PRE></PRE> tags and HTML-
escaped if not "text/html" content-type (i.e. if they are "text/plain" 
content-type).  They can be forced to be directly included by including a 
'par="text/html"' parameter.  Conversly, "text/html" files can be forced to be 
included as plain-text using the 'par="text/plain"' parameter. 

By default the #include directive reports an error if it cannot access the file
for any reason.  The optional 'fmt="?"' tag modifies this behaviour so that
processing will continue.  For example, '#include virtual="test.txt" fmt="?"'
would not report an error or stop processing if TEST.TXT did not exist.

Format ('fmt=""') specifications for time values follow those allowed by 
strftime().  If none is specified it defaults to a fairly standard looking 
VMS-style time.

Documents containing *PRIVILEGED* statements are only allowed in

  o  documents owned by SYSTEM ([1,4]) and that are NOT world writable!
  o  documents that have been mapped by the "set PRIVSSI" setting
  o  the privileged statement is found in a "set SSI=exec=<string>"


OSU-COMPATIBLILITY
------------------
The SSI engine can process OSU-specific directives.  In addition, the server
can be configured to tranparently process OSU .HTMLX SSI files.  This eases any
transition between the two.  There may be minor incompatibilities reflecting
the UNIX-style file syntax used by OSU and the DECnet environment of OSU
scripts.  Also see comments in section "Evaluations and Flow Control" below. 
The following lists the WASD configuration requirements.

  HTTPD$CONFIG:
  [AddType]
  .HTMLX  text/x-shtml  -  OSU SSI HTML

Note that the content description must contain the string "OSU" to activate
some compliancy behaviours.  

  HTTPD$MAP:
  redirect /*.*.htmlx /*.htmlx?httpd=ssi&__part=*

This provides a mechanism for the OSU part-document facility.
(Yes, the "__part" has two leading underscores!)


DIRECTIVES
----------

<!--## -->                              comment (not in resulting document!)
<!--#" "-->                             synonym for echo

<!--#accesses [ordinal] [since=""] [timefmt=""] -->  accesses of document

<!--#begin label -->                    OSU-compliant, #include delimiter

<!--#config compliance="" -->           backward-compatibility integer
<!--#config errmsg="" -->               SSI error message header
<!--#config sizefmt="" -->              set file size output format
<!--#config timefmt="" -->              set default time format
<!--#config OSU="1|0" -->               "force" to be processed as OSU (or not)
<!--#config trace="1|0" -->             set trace statements on or off
<!--#config verify="1|0" -->            OSU-compliant, include commented tags

<!--#dcl -->                            synonym for #exec
<!--#dir file="" [par=""] -->           directory (index of) file spec
<!--#dir virtual="" [par=""] -->        directory (index of) virtual spec
<!--#index file="" [par=""] -->         synonym for the above
<!--#index virtual="" [par=""] -->      synonym for the above

<!--#echo accesses -->                  OSU-compliant, accesses of document
<!--#echo accesses_ordinal -->          OSU-compliant, accesses of document
<!--#echo created[=fmt] -->             current document creation date/time
<!--#echo date_local[=fmt] -->          current local date/time
<!--#echo date_gmt[=fmt] -->            current GMT date/time
<!--#echo document_name -->             current document VMS file path
<!--#echo document_uri - ->             current document URL path
<!--#echo file_name -->                 current document VMS file path
<!--#echo getenv="" -->                 OSU-compliant, symbol or logical
<!--#echo hw_name -->                   OSU-compliant, hardware name
<!--#echo last_modified[=fmt] -->       current document modified date
<!--#echo query_string_unescaped -->    same as CGI query_string
<!--#echo server_name -->               OSU-compliant, HTTPd server name
<!--#echo server_version -->            OSU-compliant, HTTPd server software
<!--#echo vms_version -->               OSU-compliant, VMS version
<!--#echo var="" -->                    synonym for any of the above echos
<!--#echo <any-CGI-variable> -->        any CGI scripting variable name

<!--#end label -->                      OSU-compliant, #include delimiter

NOTE: the "#dcl" and "#exec" statements are identical

<!--#exec cgi="" -->                    execute CGI/NPH script absorbing header
<!--#exec dir="" [par=""] -->           DIRECTORY file-spec [qualifiers]
<!--#exec vdir="" [par=""] -->          DIRECTORY virtual-file-spec [qualifiers]
<!--#exec show="" -->                   SHOW command
<!--#exec say="" -->                    WRITE SYS$OUTPUT 'anything'
<!--#exec script="" -->                 execute CGI/NPH script absorbing header

<!--#exec exec="" -->                   *PRIVILEGED* execute any DCL command
<!--#exec file="" [par=""] -->          *PRIVILEGED* execute command procedure
<!--#exec run="" [par=""] -->           *PRIVILEGED* execute image
<!--#exec virtual="" [par=""] -->       *PRIVILEGED* execute command procedure
<!--#exec vrun="" [par=""] -->          *PRIVILEGED* execute virtual image

<!--#exit -->                           stop processing the current file

<!--#fcreated file="" [fmt=""] -->      specified file creation date/time
<!--#fcreated virtual="" [fmt=""] -->   specified URL document creation date
<!--#flastmod file="" [fmt=""] -->      specified file last modified date/time
<!--#flastmod virtual="" [fmt=""] -->   specified URL document last modified 
<!--#fsize file="" -->                  specified file size (bytes, Kb, Mb)
<!--#fsize virtual="" -->               specified URL document size

<!--#include file="" [type=""] [fmt=""] -->     include file's contents
<!--#include virtual="" [type=""] [fmt=""] -->  include URL document contents

<!--#include file="" [part=""] -->      OSU-compliant, include only part
<!--#include virtual="" [part=""] -->   OSU-compliant, include only part

<!--#if value="" [...] -->              flow control
<!--#orif value="" [...] -->            flow control
<!--#elif value="" [...] -->            flow control
<!--#else -->                           flow control
<!--#endif -->                          flow control

<!--#modified file="" [fmt=""] -->      get the RDT of this file
<!--#modified virtual="" [fmt=""] -->   get the RDT of this file
<!--#modified if-modified-since -->     304 response returned if not modified
<!--#modified last-modified -->         "Last-Modified:" response field added
<!--#modified expires="" -->            "Expires:" response field added

<!--#printenv -->                       prints all assigned variables
<!--#set var= value="" -->              sets the variable name to value
<!--#ssi [statement|...] -->            multiple SSI statements
<!--#stop -->                           stop processing the virtual document


VARIABLES
---------
There are server and user variables. User variables may be assigned using the
"#set" statement. The values of both may be substituted into any tag value
associated with any statement. User variable names may comprise alphanumeric
and underscore characters, and are not case sensitive. Variable substitution
is indicated by delimitting the variable name with '{' and '}'. The variable
name may be followed by one or two comma separated numbers which serve as an
extraction start index and count. A single number results in the variable text
from zero for the specified. Two numbers specify a start index and count.

    <!--#set var=EXAMPLE1 value="This is an example!" -->
    <!--#set var=EXAMPLE2 value={EXAMPLE1,0,10} -->
    <!--#set var=EXAMPLE3 value="{EXAMPLE2}other example ..." -->
    <!--#set var=EXAMPLE4 value="{EXAMPLE1,10}other example!" -->
    <!--#echo "<P>" var={EXAMPLE3} "<BR>" var={EXAMPLE4} -->

The output from the "#echo" would be

    <P>This is another example ...
    <BR>This is another example!

All variables available, server, CGI and user, may be displayed using the
#printenv directive.  All variables are global in scope.  This means the same
set of variables are visible to all #included SSI documents, and user variables
set by #included documents are seen by their parents, etc.  Server variables
containing document related information generally refer to the top-level, or
parent document, with these local exceptions.

    COMPLIANCE ......... the level of compliance that should be applied
    DOCUMENT_DEPTH ..... level of "#include"d documents (root file is 1)
    PARENT_FILE_NAME ... name of file "#include"ing this current file
    THIS_FILE_NAME ..... name of the current file

GENERAL GUIDELINES:  To use the extended functionality of WASD variables leave
the quotes off of 'var=' tags wherever possible.  When they are present the SSI
engine attempts to retain compatibility with other, general SSI implmentations,
and the OSU cersion, and this may cause unexpected interactions or results. 
Another option is to use the 'value=' tag.


EVALUATIONS and FLOW CONTROL
----------------------------
Flow control statements work in the same fashion to other implementations. The
possible WASD idiosyncrasy is "#orif" which, in the absence of a true
expression parser, allows multiple conditions to execute a single block of
statements.

    <!--#if value="" [...=""] -->
    <!--#orif value="" [...=""] -->
    <!--#elif value="" [...=""] -->
    <!--#else -->
    <!--#endif -->

The evalution of a flow control decision in an "#if", "#orif" or "#elif" is
based on one or more tests against one or more variables.

    <!--#if value="" -->                  if string not empty, or not numeric 0
    <!--#if value="" eqs="" -->           string equal to
    <!--#if value="" srch="" -->          string search (* and % wildcards)
    <!--#if value="" lt="" -->            numeric less than
    <!--#if value="" eq="" -->            numeric equal to
    <!--#if value="" gt="" -->            numeric greater than

If a single string/variable is supplied in the test then if it is numeric and
non-zero then the test is true, if a string and non-empty it is true.  Other
states are false.  The "eqs=" test does a case-insensitive string compare of
the supplied string and the "value=" string. The "srch=" test searches "value="
with the supplied string, which can contain the wildcards "*" matching
multiple characters and "%" matching any one character.  The "lt=", "eq=" and
"gt=" convert the parameters to integers and do an arithetic compare.
Multiple comparisons, even with multiple variables, may occur in the one
decision statement, these will act as a logical AND.  Logical ORs may be
created using "#orif" statements.  Any evaluation tag preceded by an
exclamation point will have it's result logically negated.  For example,

    <!--#if value="0" !eq="0" -->

returns a BOOL value of false.

Note that the OSU-compliant #begin and #end directives are implemented as flow
control statements.  They are only permitted in SSI files identified as
OSU-compliant (i.e. .HTMLX) and cannot be used in standard WASD SSI.


SIMPLE EXAMPLE
--------------
This example demonstrates some of the salient features of WASD's XSSI.

    <!--##config trace=1 -->
    <HTML>
    <!--#ssi
    #set var=HOUR value={DATE_LOCAL,12,2}
    #if value={HOUR} lt=12
      #set var=GREETING value="Good morning"
    #elif value={HOUR} lt=19
      #set var=GREETING value="Good afternoon"
    #else
      #set var=GREETING value="Good evening"
    #endif
    -->
    <HEAD>
    <TITLE><!--#echo value={GREETING} -->
    <!--#echo value="{REMOTE_HOST}!" --></TITLE>
    </HEAD>
    <BODY>
    <H2>Simple XSSI Demonstration</H2>
    <!--#echo value={GREETING} --> <!--#echo value={REMOTE_HOST} -->,
    the time here is <!--#echo value={DATE_LOCAL,12,5} -->.
    </BODY>
    </HTML>


FILE EXIST/NOT-EXIST
--------------------
Generally, errors encountered halt document processing and report the error. 
For some common circumstances, in particular the existance or not of a
particular file, may require an alternative action.  For file activities (e.g.
#include, #flastmod, #created, #fsize) the optional 'fmt=""' tag provides some
measure of control on error behaviour.  If the format string begins with a '?'
files not found are not reported as errors and processing continues.  Other
file systems errors, such as directory not found, syntax errors, etc., are
always reported.

Every time a file is accessed (e.g. #include, #flastmod) the server variable
THE_FILE_NAME gets set to that name if successful, or reset to empty if
unsuccessful.  This variable can be checked to determine success or otherwise.

For #included files, the 'fmt="?"' just suppresses an error report, if the file
exists then it is included.

For #modified file specifications use 'fmt="?"' to suppress error reporting on
evaluation of files that may exist but are not mandatory.

For file statistic directives (e.g. #flastmod, #fcreated, #fsize) the 'fmt="?"'
tag completely suppresses all output as well as error reporting.  This can be
used to check for the existance of a file.  For example if the file TEST.TXT
exists in the following example the variable THE_FILE_NAME would contain the
full file name, if it does not exist it would be empty, and the code example
would behave accordingly.

    <!--#fcreated virtual="TEST.TXT" fmt="?" -->
    <!--#if var={THE_FILE_NAME} eqs="" -->
    File does not exist!
    <!--#else -->
    File exists!
    <!--#endif -->


"IF-MODIFIED-SINCE:"
--------------------
SSI documents generally contain dynamic elements, that is those that may change
with each access to the document (e.g. current date/time).  This makes
evaluation of any document modification date difficult (i.e. too much for the
author to bother coding ;^) and so by default no "Last-Modified:" information
is supplied against an SSI document.  The potential efficiencies of having
document last-modified timestamps so that if-modified-since requests can
generate not-modified responses are significant in a range of cases (basically
where any dynamic document elements relate only to the file(s) of which the
document is composed).

With the significant CPU overheads of processing SSI documents this approach
has the potential to return substantial benefits for suitable documents and
sites.

WASD allows the document author to determine whether or not a last-modified
header field should be generated for a particular document and which
contributing document(s) should be used to determine it.  This is done using
the #modified directive.  Where multiple source documents (files) are employed
each can be checked using the virtual= or file= tags of the #modified
directive, the most recently modified will be used to determine if it's been
modified and also to generate the last-modified timestamp.  By default the
#modified directive gets the last-modified date of the current document (i.e.
when no tags follow the directive, as in the first line of the example below).

The "if-modified-since" tag compares the (most recent) revision date/time of
the file(s) checked using the virtual= or file= tags, with any
"If-Modified-Since:" date/time supplied with the request.  If the file(s)
revision date/time is the same or older than the request's then a not-modified
(304 status) header is generated and sent to the client and document processing
stops.  If more recent an appropriate "Last-Modified:" header field is added to
the document and it continues to be processed.

If a request has a "Pragma: no-cache" field (as with Navigator's "reload"
function) the document is always generated (this is consistent with general
WASD behaviour).

The following (rather exagerated) example illustrates the essential features.

    <!--#ssi
    #modified
    #modified virtual="/web/common/header.shtml"
    #modified virtual="header.html" fmt="?"
    #modified virtual="index.html"  fmt="?"
    #modified virtual="footer.html" fmt="?"
    #modified virtual="/web/common/footer.shtml"
    #modified if-modified-since
    -->

This construct should be placed at the very beginning of the SSI document, and
certainly before there is any chance of output being sent to the browser.  Once
output to the client has occured there can be no change to the response header
information (not unreasonably).

The "last-modified" tag generates a "Last-Modified:" field using the (most
recent) revision date/time of the file(s) and adds it to the response header. 
It is not necessary to do this if the "if-not-modified" tag has been used as
this implicitly generates one, but there may be circumstances where a
last-modified field might be desired even though the document is always
generated.

The 'expires=""' tag (which is bundled in with the #modified directive) takes a
string literal and generates then adds an "Expires:" response header field. 
The string literal should be a legitimate RFC-1123 date string.  This can be
used for pre-expiring documents (so they are always reloaded), set it to a date
in the not-too-distant past (e.g. expires="Fri, 13 Jan 1978 14:00:00 GMT").
Of course it could also be used for setting the legitimate future expiry of
documents.


VERSION HISTORY
---------------
10-JAN-2004  MGD  support <!--#exec script="/cgi-bin/blah" -->,
                  SSI #exec (#dcl) directives can be allowed on per-path basis
                    using SET ssi=exec=<string> (e.g. 'ssi=exec=say,show'),
                  SSI can now be enabled on a per-path basis using 'ssi=exec=#'
07-JUL-2003  MGD  cache loading from SSI using network output
05-OCT-2002  MGD  refine VMS security profile usage
10-JUL-2002  MGD  provide real path to directory listing
27-APR-2002  MGD  make SYSPRV enabled ASTs asynchronous,
                  bugfix; SsiAccessesClose() now synchronous using SYSPRV
                  (J.Begg demonstrated it was required for alarm-free closure)
04-AUG-2001  MGD  support module WATCHing,
                  bugfix; ++AccountingPtr->DoSsiCount;
24-JUN-2001  MGD  add setlocale() as suggested by jfp@altavista.com
30-DEC-2000  MGD  rework for FILE.C getting file contents in-memory
                  (allows CACHE.C to cache the file, the test-bench showing
                  a 100% improvement in throughput for simple SSIs!)
01-SEP-2000  MGD  add optional, local path authorization
                  (for calls from the likes of SSI.C)
18-MAY-2000  MGD  bugfix; prevent '#exec blah="@file.com"'
09-APR-2000  MGD  modify ERROR_... special variables in line with ERROR.C
04-MAR-2000  MGD  use NetWriteFaol(), et.al.,
                  add the [NO]PRIVSSI setting
31-DEC-1999  MGD  support ODS-2 and ODS-5 using ODS module,
                  allow for carriage-control in FIX/UDF format files
12-NOV-1999  MGD  bugfix; brain-dead code in SsiOsuFileName()
10-OCT-1999  MGD  support "scrunched" :^) SSI files,
                  support OSU-compatible directives,
                  bugfix; sys$parse() NAM$M_NOCONCEAL for search lists,
                  bugfix; no 'nxt' after 'FileXabPro'!
05-AUG-1999  MGD  add #include content="text/..." whatever
11-APR-1999  MGD  variables to ease the creation of virtual documents
08-APR-1999  MGD  last-modified/not-modified functionality,
                  'fmt="?"' tag on #include and other file related directives
28-MAR-1999  MGD  'FileRab.rab$w_usz' now dynamic ('FileXabFhc.xab$w_lrl'),
                  'StatementBuffer' dynamic (<!--#config buffersize="" -->)
11-MAR-1999  MGD  user variables have global scope (nested HTML documents),
                  bugfix; GetFileSpec() wasn't using generic GetTagValue()
07-NOV-1998  MGD  WATCH facility
18-OCT-1998  MGD  error report support
19-SEP-1998  MGD  improve granularity of file open, connect, close, ACP
19-JUL-1998  MGD  bugfix; GetTagValue() must return number of characters!
                  bugfix; MapUrl_Map() pass 'rqptr' for conditionals to work
13-APR-1998  MGD  add <!--#ssi ... --> block statement,
                  variable assignment and flow-control eXtensions
23-JAN-1998  MGD  <!--#echo server_gmt --> and ordinal accesses
25-OCT-1997  MGD  <!--#echo http_accept_charset, http_host, http_forward -->
17-AUG-1997  MGD  message database,
                  SYSUAF-authenticated users security-profile,
                  additional <!--#echo ... --> (just for the hang of it :^)
27-FEB-1997  MGD  delete on close for "temporary" files
01-FEB-1997  MGD  HTTPd version 4 (also changed module name to SSI.c);
                  extensive rework of some functions;
                  statements now allowed to span multiple lines
04-JUN-1996  MGD  bugfix; SsiFileDetails() error reporting
01-DEC-1995  MGD  new for HTTPd version 3
*/
/*****************************************************************************/

#ifdef WASD_VMS_V6
#undef _VMS_V6_SOURCE
#define _VMS_V6_SOURCE
#undef __VMS_VER
#undef __VMS_VER
#undef __CRTL_VER
#define __CRTL_VER 60000000
#endif

/* standard C header files */
#include <ctype.h>
#include <locale.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>

/* VMS related header files */
#include <descrip.h>
#include <libdef.h>
#include <prvdef.h>
#include <rms.h>
#include <ssdef.h>
#include <stsdef.h>

/* application-related header files */
#include "wasd.h"

#define WASD_MODULE "SSI"

#define SSI_DEFAULT_TIME_FORMAT "%Od-%b-%Y %T"  /* a little VMSish! */
#define SSI_DEFAULT_SIZE_FORMAT "abbrev"

#define SSI_DEFAULT_COMPLIANCE_LEVEL 603  /* current SSI implemented v6.0.3 */
#define SSI_OSU_COMPLIANCE_LEVEL     603  /* OSU HTMLX supported at v6.0.3 */
#define SSI_VAR_FMT_COMPLIANCE_LEVEL 603  /* #echo var="NAME=fmt" at v6.0.3 */

#define FILE_LAST_MODIFIED  1
#define FILE_FCREATED       2
#define FILE_FLASTMOD       3
#define FILE_FSIZE          4

#define SSI_STATE_DEFAULT  1
#define SSI_STATE_IF       2
#define SSI_STATE_ELIF     3
#define SSI_STATE_ELSE     4
/* for OSU #include part compliance */
#define SSI_STATE_BEGIN    5

#define SSI_MAX_DEPTH  5

/******************/
/* global storage */
/******************/

int  SsiComplianceLevel,
     SsiSizeMax;

/********************/
/* external storage */
/********************/

#ifdef DBUG
extern BOOL Debug;
#else
#define Debug 0 
#endif

extern int  DclCgiVariablePrefixLength;

extern unsigned long  SysPrvMask[];

extern char  ConfigContentTypeSsi[],
             ErrorSanityCheck[],
             HttpProtocol[],
             SoftwareID[],
             TimeGmtString[];

extern ACCOUNTING_STRUCT  *AccountingPtr;
extern CONFIG_STRUCT  Config;
extern MSG_STRUCT  Msgs;
extern SYS_INFO  SysInfo;
extern WATCH_STRUCT  Watch;

/*****************************************************************************/
/*
Open the specified file.  If there is a problem opening it then just return 
the status, do not report an error or anything.  This is used to determine 
whether the file exists, as when attempting to open one of multiple, possible 
home pages.

As pre-processed HTML files are by definition dynamic, no check of any
"If-Modified-Since:" request field date/time is made, and no "Last-Modified:" 
field is included in any response header. 

When successfully opened and connected generate an HTTP header, if required.
Once open and connected the pre-processing becomes AST-driven.
*/

SsiBegin (REQUEST_STRUCT *rqptr)

{
   int  status;
   char  *cptr, *sptr, *zptr;
   FILE_CONTENT  *fcptr;
   SSI_TASK  *tkptr,
             *ParentDocumentTaskPtr,
             *RootDocumentTaskPtr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                 "SsiBegin() !&F !&X !UL !UL !UL", &SsiBegin,
                 rqptr->FileContentPtr->ContentPtr,
                 rqptr->FileContentPtr->ContentSize,
                 rqptr->FileContentPtr->ContentLength,
                 rqptr->FileContentPtr->ContentRemaining);

   /* take control of the file contents structure */
   fcptr = rqptr->FileContentPtr;
   rqptr->FileContentPtr = NULL;

   if (ERROR_REPORTED (rqptr))
   {
      /* previous error, cause threaded processing to unravel */
      SysDclAst (fcptr->NextTaskFunction, rqptr);
      return;
   }

   if (!(Config.cfSsi.Enabled ||
         (rqptr->rqPathSet.SsiExecPtr &&
          rqptr->rqPathSet.SsiExecPtr[0] == '#')))
   {
      rqptr->rqResponse.HttpStatus = 403;
      ErrorGeneral (rqptr, MsgFor(rqptr,MSG_GENERAL_DISABLED), FI_LI);
      SysDclAst (fcptr->NextTaskFunction, rqptr);
      return;
   }

   /* SSI documents can call other SSI documents ... infinite recursion! */
   if (LIST_GET_COUNT (&rqptr->SsiTaskList) > SSI_MAX_DEPTH) 
   {
      rqptr->rqResponse.HttpStatus = 403;
      ErrorGeneral (rqptr, MsgFor(rqptr,MSG_SSI_RECURSION), FI_LI);
      SysDclAst (fcptr->NextTaskFunction, rqptr);
      return;
   }

   if (WATCHING(rqptr) && WATCH_CATEGORY(WATCH_RESPONSE))
      WatchThis (rqptr, FI_LI, WATCH_RESPONSE, "SSI !AZ !UL bytes",
                 fcptr->FileName, fcptr->ContentLength);

   if (rqptr->SsiTaskPtr)
   {
      /* this is an included SSI document */
      ParentDocumentTaskPtr = rqptr->SsiTaskPtr;
      RootDocumentTaskPtr = rqptr->SsiTaskPtr->RootDocumentTaskPtr;
   }
   else
   {
      /* no parent SSI document, will generate new CGI/user variables */
      ParentDocumentTaskPtr = RootDocumentTaskPtr = NULL;
   }

   /* set up the task structure (possibly multiple concurrent) */
   if (rqptr->SsiTaskPtr && LIST_HAS_NEXT (rqptr->SsiTaskPtr))
   {
      rqptr->SsiTaskPtr = tkptr = LIST_GET_NEXT (rqptr->SsiTaskPtr);
      memset (LIST_GET_DATA(tkptr), 0, sizeof(SSI_TASK)-sizeof(LIST_ENTRY));
   }
   else
   {
      rqptr->SsiTaskPtr = tkptr = VmGetHeap (rqptr, sizeof(SSI_TASK));
      ListAddTail (&rqptr->SsiTaskList, tkptr);
   }

   /* set up read to SSI parse the file's body */
   tkptr->FileContentPtr = fcptr;
   tkptr->ParsePtr = fcptr->ContentPtr;

   /* buffer the request's escape-HTML flag */
   tkptr->OutputBufferEscapeHtml = rqptr->rqOutput.BufferEscapeHtml;

   tkptr->SizeFmtPtr = SSI_DEFAULT_SIZE_FORMAT;
   tkptr->TimeFmtPtr = SSI_DEFAULT_TIME_FORMAT;

   if (RootDocumentTaskPtr)
      tkptr->RootDocumentTaskPtr = RootDocumentTaskPtr;
   else
      tkptr->RootDocumentTaskPtr = tkptr;
   tkptr->ParentDocumentTaskPtr = ParentDocumentTaskPtr;

   if (tkptr->ParentDocumentTaskPtr)
   {
      /* inherit these from the parent document */
      tkptr->ComplianceLevel = ParentDocumentTaskPtr->ComplianceLevel;
      tkptr->OsuCompliant = ParentDocumentTaskPtr->OsuCompliant;
      tkptr->TagVerify = ParentDocumentTaskPtr->TagVerify;
      tkptr->TraceState = ParentDocumentTaskPtr->TraceState;

      tkptr->CgiBufferPtr = tkptr->ParentDocumentTaskPtr->CgiBufferPtr;
      memcpy (&tkptr->SsiVarList,
              &tkptr->ParentDocumentTaskPtr->SsiVarList,
              sizeof(tkptr->SsiVarList));
   }
   else
   {
      /* top-level document */
      if (!rqptr->AccountingDone++)
         InstanceGblSecIncrLong (&AccountingPtr->DoSsiCount);

      /* set the default compliance level */
      if (!SsiComplianceLevel)
      {
         /* first time though check for this kludge */
         if (!(cptr = getenv ("HTTPD$SSI_COMPLIANCE")))
            SsiComplianceLevel = SSI_DEFAULT_COMPLIANCE_LEVEL;
         else
            SsiComplianceLevel = atoi(cptr);
      }
      tkptr->ComplianceLevel = SsiComplianceLevel;

      if (tkptr->ComplianceLevel >= SSI_OSU_COMPLIANCE_LEVEL)
      {
         /* OSU SSI? (it is if the content description contains "OSU"!) */
         if (rqptr->rqContentInfo.DescriptionPtr &&
             strstr (rqptr->rqContentInfo.DescriptionPtr, "OSU"))
            tkptr->OsuCompliant = true;
      }

      /* generate top-level document CGI variables to be used thoughout */
      if (VMSnok (CgiGenerateVariables (rqptr, CGI_VARIABLE_STREAM)))
      {
         SsiEnd (rqptr);
         return;
      }
      tkptr->CgiBufferPtr = rqptr->rqCgi.BufferPtr;
      rqptr->rqCgi.BufferPtr = NULL;
      memset (&tkptr->SsiVarList, 0, sizeof(tkptr->SsiVarList));
   }

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z !UL !&B",
                 fcptr->FileName, tkptr->ComplianceLevel, tkptr->OsuCompliant);

   if (tkptr->OsuCompliant &&
       !tkptr->ParentDocumentTaskPtr &&
       SsiGetVar (rqptr, "FORM___PART", NULL, true))
   {
      /*****************/
      /* OSU path part */
      /*****************/

      /* part was supplied with an OSU "/path/file.part.type" */
      cptr = SsiGetVar (rqptr, "FORM___PART", NULL, false);
      zptr = (sptr = tkptr->CurrentPart) + sizeof(tkptr->CurrentPart);
      while (*cptr && sptr < zptr) *sptr++ = *cptr++;
      if (sptr >= zptr)
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_GENERAL_OVERFLOW), FI_LI);
         SsiEnd (rqptr);
         return;
      }
      *sptr = '\0';
      tkptr->FlowControlState[0] = SSI_STATE_DEFAULT;
      tkptr->FlowControlHasExecuted[0] =
         tkptr->FlowControlIsExecuting[0] = false;
   }
   else
   if (tkptr->OsuCompliant &&
       tkptr->ParentDocumentTaskPtr &&
       tkptr->ParentDocumentTaskPtr->IncludePart[0])
   {
      /********************/
      /* OSU include part */
      /********************/

      /* if the parent had an include part then can't begin straight off */
      strcpy (tkptr->CurrentPart, tkptr->ParentDocumentTaskPtr->IncludePart);
      tkptr->FlowControlState[0] = SSI_STATE_DEFAULT;
      tkptr->FlowControlHasExecuted[0] =
         tkptr->FlowControlIsExecuting[0] = false;
   }
   else
   {
      /************/
      /* WASD SSI */
      /************/

      /* no flow control structure always outputs */
      tkptr->FlowControlState[0] = SSI_STATE_DEFAULT;
      tkptr->FlowControlHasExecuted[0] = false;
      tkptr->FlowControlIsExecuting[0] = true;
   }
   tkptr->FlowControlIndex = 0;

   if (!rqptr->rqResponse.HeaderPtr)
   {
      rqptr->rqResponse.PreExpired = PRE_EXPIRE_SSI;
      RESPONSE_HEADER_200_HTML (rqptr);

      if (rqptr->rqHeader.Method == HTTP_METHOD_HEAD)
      {
         SsiEnd (rqptr);
         return;
      }
   }

   /* network writes are checked for success, fudge the first one! */
   rqptr->rqNet.WriteIOsb.Status = SS$_NORMAL;

   if (*tkptr->ParsePtr)
   {
      tkptr->LineNumber++;
      if (tkptr->TraceState) SsiTraceLine (rqptr, tkptr->ParsePtr);
   }

   if (rqptr->rqPathSet.CacheSSI &&
       !rqptr->rqCache.LoadCheck)
   {
      /* SSI output to be cached (using network output) */
      rqptr->rqCache.LoadFromNet =
         CacheLoadBegin (rqptr, rqptr->rqResponse.ContentLength,
                                rqptr->rqResponse.ContentTypePtr);
   } 

   SsiParse (rqptr);
} 

/*****************************************************************************/
/*
End of SSI interpretation, successful or otherwise.
*/ 

SsiEnd (REQUEST_STRUCT *rqptr)

{
   int  status;
   SSI_TASK  *tkptr;
   REQUEST_AST  NextTaskFunction;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiEnd() !&F !&A",
                 &SsiEnd, rqptr->SsiTaskPtr->FileContentPtr->NextTaskFunction);

   tkptr = rqptr->SsiTaskPtr;

   if (tkptr->DetailsOds.ParseInUse) OdsParseRelease (&tkptr->DetailsOds);

   NextTaskFunction = tkptr->FileContentPtr->NextTaskFunction;

   /* free the memory allocated to contain the file contents */
   if (tkptr->FileContentPtr)
   {
      VmFreeFromHeap (rqptr, tkptr->FileContentPtr, FI_LI);
      tkptr->FileContentPtr = NULL;
   }

   /* propagate any requirement to stop processing to any parent */
   if (tkptr->StopProcessing && tkptr->ParentDocumentTaskPtr)
      tkptr->ParentDocumentTaskPtr->StopProcessing = true;

   /* restore the escape-HTML flag */
   rqptr->rqOutput.BufferEscapeHtml = tkptr->OutputBufferEscapeHtml;

   /* could not possibly be a keep-alive response */
   rqptr->KeepAliveResponse = false;

   /* restore previous SSI task (if any) */
   rqptr->SsiTaskPtr = LIST_GET_PREV (tkptr);

   /* pass control to the next task function */
   SysDclAst (NextTaskFunction, rqptr);
}

/*****************************************************************************/
/*
'tkptr->ParsePtr' points to the currently parsed-up-to position in the buffer. 
Continue parsing the buffer looking for preprocessor statements.
*/ 

SsiParse (REQUEST_STRUCT *rqptr)

{
   BOOL  BeginSsi;
   int  status;
   char  ch;
   char  *cptr;
   SSI_TASK  *tkptr;
   REQUEST_AST  AstFunction;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiParse() !&F", &SsiParse);

   tkptr = rqptr->SsiTaskPtr;

   /* first check if any error occured during any previous processing */
   if (ERROR_REPORTED (rqptr) || tkptr->StopProcessing)
   {
      tkptr->StopProcessing = true;
      SsiEnd (rqptr);
      return;
   }

   /* ensure the escape-HTML flag is "off" from any previous processing */
   rqptr->rqOutput.BufferEscapeHtml = tkptr->TraceState;

   if (tkptr->TerminatedPtr)
   {
      /*
         For convenient (read lazy) and possibly more efficient programming
         each SSI directive has it's first character pointed to and it's
         next-after-last character overwritten with a null.  This allows it
         to be parsed as a null-terminated string without needing to be
         copied to some other buffer, or the like.  If this has happened then
         'TerminatedPtr' points to the in-memory location and 'TerminatedChar'
         contains the character that was overwritten.  Just restore the
         in-memory file to it's original condition.  Must be restored *after*
         any asynchronous processing (e.g. #including a file) hence it being
         done here before we begin any next directive.
      */
      *tkptr->TerminatedPtr = tkptr->TerminatedChar;
      tkptr->TerminatedPtr = NULL;
   }

   if (tkptr->StatementEndPtr)
   {
      /* adjust parse pointer for any statement previously processed */
      cptr = tkptr->ParsePtr = tkptr->StatementEndPtr;
      if (cptr[0] == '-' && cptr[1] == '-' && cptr[2] == '>')
      {
         if (tkptr->InsideSsiStatement) tkptr->InsideSsiStatement = false;
         tkptr->ParsePtr += 3;
      }
   }

   BeginSsi = false;
   tkptr->StatementBeginPtr = tkptr->StatementEndPtr = NULL;

/**
   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchDataDump (tkptr->ParsePtr, strlen(tkptr->ParsePtr));
**/

   if (tkptr->InsideSsiStatement)
   {
      /**********************************/
      /* inside an "<!--#ssi" statement */
      /**********************************/

      cptr = tkptr->ParsePtr;
      while (*cptr)
      {
         while (*cptr && *cptr != '#' && *cptr != '-')
         {
            if (*cptr != '\n')
            {
               /* step over any escape character */
               if (*cptr == '\\') cptr++;
               if (*cptr) cptr++;
               continue;
            }
            cptr++;
            tkptr->LineNumber++;
            if (tkptr->TraceState && *cptr) SsiTraceLine (rqptr, cptr);
            tkptr->SuppressLine = false;
         }
         if (!*cptr) break;
         if (*cptr == '#')
         {
            tkptr->StatementBeginPtr = cptr;
            break;
         }
         if (cptr[0] == '-' && cptr[1] == '-' && cptr[2] == '>')
         {
            tkptr->StatementEndPtr = cptr;
            break;
         }
         cptr++;
      }
      if (*cptr == '#')
      {
         cptr++;
         /* allow for "##" comments */
         if (*cptr == '#') cptr++;
         while (*cptr)
         {
            while (*cptr && *cptr != '#' && *cptr != '-')
            {
               if (*cptr != '\n')
               {
                  /* step over any escape character */
                  if (*cptr == '\\') cptr++;
                  if (*cptr) cptr++;
                  continue;
               }
               cptr++;
               tkptr->LineNumber++;
               if (tkptr->TraceState && *cptr) SsiTraceLine (rqptr, cptr);
               tkptr->SuppressLine = false;
            }
            if (!*cptr) break;
            if (*cptr == '#')
            {
               tkptr->StatementEndPtr = cptr;
               break;
            }
            if (cptr[0] == '-' && cptr[1] == '-' && cptr[2] == '>')
            {
               tkptr->StatementEndPtr = cptr;
               break;
            }
            cptr++;
         }
      }
   }
   else
   {
      /*****************************/
      /* delimit the "<!--#...-->" */
      /*****************************/

      cptr = tkptr->ParsePtr;
      /* look for the start of a statement */
      while (*cptr)
      {
         while (*cptr && *cptr != '<')
         {
            if (*cptr++ != '\n') continue;
            tkptr->LineNumber++;
            if (tkptr->TraceState && *cptr) SsiTraceLine (rqptr, cptr);
            if (tkptr->SuppressLine)
            {
               tkptr->ParsePtr = cptr;
               tkptr->SuppressLine = false;
            }
         }
         if (!*cptr) break;
         if (cptr[0] == '<' && cptr[1] == '!' && cptr[2] == '-' &&
             cptr[3] == '-' && cptr[4] == '#')
         {
            tkptr->StatementBeginPtr = cptr;
            break;
         }
         cptr++;
      }
      if (tkptr->StatementBeginPtr &&
          !strsame (tkptr->StatementBeginPtr, "<!--#ssi", 8))
      {
         /* look for the end of a statement */
         while (*cptr)
         {
            while (*cptr && *cptr != '-')
            {
               if (*cptr++ != '\n') continue;
               tkptr->LineNumber++;
               if (tkptr->TraceState && *cptr) SsiTraceLine (rqptr, cptr);
               if (tkptr->SuppressLine)
               {
                  tkptr->ParsePtr = cptr;
                  tkptr->SuppressLine = false;
               }
            }
            if (!*cptr) break;
            if (cptr[0] == '-' && cptr[1] == '-' && cptr[2] == '>')
            {
               tkptr->StatementEndPtr = cptr;
               break;
            }
            cptr++;
         }
      }
   }

   if (tkptr->StatementBeginPtr)
   {
      /*****************/
      /* SSI statement */
      /*****************/

      if (!tkptr->StatementEndPtr) tkptr->StatementEndPtr = cptr;
      cptr = tkptr->StatementBeginPtr + 4;

      tkptr->StatementLineNumber = tkptr->LineNumber;

/**
      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchDataDump (cptr, strlen(cptr));
**/

      ch = tolower(cptr[1]);
      if (ch == 'b' && strsame (cptr, "#begin", 6) ||
          ch == 'i' && strsame (cptr, "#if", 3) ||
          ch == 'e' && strsame (cptr, "#elif", 5) ||
          ch == 'e' && strsame (cptr, "#else", 5) ||
          /* MUST precede #end for the obvious reason */
          ch == 'e' && strsame (cptr, "#endif", 6) ||
          ch == 'e' && strsame (cptr, "#end", 4) ||
          ch == 'o' && strsame (cptr, "#orif", 5) ||
          ch == 's' && strsame (cptr, "#set", 4) ||
          /* #trace is kept for backward compatibility */
          ch == 't' && strsame (cptr, "#trace", 6) ||
          *((unsigned short*)cptr) == '##')
      {
         /* flow control line, etc., suppress resultant "blank" lines */
         tkptr->SuppressLine = true;
      }
      else
      if (ch == 's' && strsame (cptr, "#ssi", 4))
      {
         /* found the start of an "<!--#ssi" statement */
         if (tkptr->InsideSsiStatement)
         {
             /* already inside an "<!--#ssi" statement! */
             SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_STATEMENT), FI_LI);
             SsiEnd (rqptr);
             return;
         }
         BeginSsi = tkptr->InsideSsiStatement = tkptr->SuppressLine = true;
      }
   }   

   if (!tkptr->StatementBeginPtr)
   {
      if (tkptr->FlowControlIndex)
      {
         /* hmmm, got to end of file and there's still nested control */
         tkptr->StatementLineNumber = tkptr->LineNumber - 1;
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_FLOW_CONTROL), FI_LI);
         SsiEnd (rqptr);
         return;
      }
   }
   else
      cptr = tkptr->StatementBeginPtr;

   if (cptr > tkptr->ParsePtr)
   {
      /* output chars preceding the statement, or at the end-of-document */
      if ((BeginSsi || !tkptr->InsideSsiStatement) &&
          tkptr->FlowControlIsExecuting[tkptr->FlowControlIndex])
      {
         if (!tkptr->StatementBeginPtr)
            AstFunction = &SsiEnd;
         else
         if (BeginSsi)
            AstFunction = &SsiParse;
         else
            AstFunction = &SsiStatement;
         NetWriteBuffered (rqptr, AstFunction,
                           tkptr->ParsePtr, cptr - tkptr->ParsePtr);
         tkptr->TraceOutput = true;
         if (BeginSsi) tkptr->StatementEndPtr = tkptr->StatementBeginPtr + 8;
         return;
      }
   }

   if (!tkptr->StatementBeginPtr)
      SsiEnd (rqptr);
   else
   if (BeginSsi)
   {
      tkptr->StatementEndPtr = tkptr->StatementBeginPtr + 8;
      SsiParse (rqptr);
   }
   else
      SsiStatement (rqptr);
}

/*****************************************************************************/
/*
'tkptr->ParsePtr' points to the start of a pre-processor statement.  
Parse that statement and execute it, or provide an error message.
*/ 

SsiStatement (REQUEST_STRUCT *rqptr)

{
   int  status;
   char  ch;
   char  *cptr;
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                 "SsiStatement() !&F", &SsiStatement);

   tkptr = rqptr->SsiTaskPtr;

   /* terminate the statement */
   tkptr->TerminatedChar = *tkptr->StatementEndPtr;
   *(tkptr->TerminatedPtr = tkptr->StatementEndPtr) = '\0';

   /* step over any leading "<!--" */
   if (*tkptr->StatementBeginPtr == '<') tkptr->StatementBeginPtr += 4;

   /* trim trailing spaces */
   cptr = tkptr->StatementEndPtr;
   if (cptr > tkptr->StatementBeginPtr)
   {
      cptr--;
      while (cptr > tkptr->StatementBeginPtr && isspace(*cptr)) cptr--;
      if (!isspace(*cptr)) cptr++;
      *tkptr->TerminatedPtr = tkptr->TerminatedChar;
      tkptr->TerminatedChar = *cptr;
      *(tkptr->TerminatedPtr = cptr) = '\0';
   }

   tkptr->StatementLength = cptr - tkptr->StatementBeginPtr;

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
   {
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "flow:!UL line:!UL length:!UL",
                 tkptr->FlowControlIsExecuting[tkptr->FlowControlIndex],
                 tkptr->LineNumber, 
                 tkptr->TerminatedPtr - tkptr->StatementBeginPtr);
      WatchDataDump (tkptr->StatementBeginPtr,
                     tkptr->TerminatedPtr - tkptr->StatementBeginPtr);
   }

   if (tkptr->TagVerify)
   {
      /* OSU-compliant, trace each statement inside HTML comments */
      status = NetWriteFao (rqptr, "<!!-- !#AZ -->",
                            tkptr->StatementLength, tkptr->StatementBeginPtr);
      if (VMSnok (status)) ErrorNoticed (status, "NetWriteFao()", FI_LI);
      tkptr->TraceOutput = true;
   }

   cptr = tkptr->StatementBeginPtr;
   ch = tolower(cptr[1]);

   if (tkptr->FlowControlIsExecuting[tkptr->FlowControlIndex])
   {
      /*************************************************/
      /* flow-control is currently allowing processing */
      /*************************************************/

      if (*((unsigned short*)cptr) == '##')
         SsiDoComment (rqptr);
      else
      if (*((unsigned short*)cptr) == '#"')
         SsiDoEcho (rqptr);
      else
      if (ch == 'b' && strsame (cptr, "#begin", 6))
         SsiDoBegin (rqptr);
      else
      if (ch == 'a' && strsame (cptr, "#accesses", 9))
         SsiDoAccesses (rqptr);
      else
      if (ch == 'c' && strsame (cptr, "#config", 7))
         SsiDoConfig (rqptr);
      else
      if (ch == 'd' && strsame (cptr, "#dcl", 4))
         SsiDoDcl (rqptr);
      else
      if (ch == 'd' && strsame (cptr, "#dir", 4))
         SsiDoDir (rqptr);
      else
      if (ch == 'e' && strsame (cptr, "#echo", 5))
         SsiDoEcho (rqptr);
      else
      if (ch == 'e' && strsame (cptr, "#elif", 5))
         SsiDoElif (rqptr);
      else
      if (ch == 'e' && strsame (cptr, "#else", 5))
         SsiDoElse (rqptr);
      else
      /* MUST precede #end for the obvious reason */
      if (ch == 'e' && strsame (cptr, "#endif", 6))
         SsiDoEndif (rqptr);
      else
      if (ch == 'e' && strsame (cptr, "#end", 4))
         SsiDoEnd (rqptr);
      else
      if (ch == 'e' && strsame (cptr, "#exec", 5))
         SsiDoDcl (rqptr);
      else
      if (ch == 'e' && strsame (cptr, "#exit", 5))
         SsiDoExit (rqptr);
      else
      if (ch == 'f' && strsame (cptr, "#fcreated", 9))
         SsiDoFCreated (rqptr);
      else
      if (ch == 'f' && strsame (cptr, "#flastmod", 9))
         SsiDoFLastMod (rqptr);
      else
      if (ch == 'f' && strsame (cptr, "#fsize", 6))
         SsiDoFSize (rqptr);
      else
      if (ch == 'i' && strsame (cptr, "#if", 3))
         SsiDoIf (rqptr);
      else
      if (ch == 'i' && strsame (cptr, "#include", 8))
         SsiDoInclude (rqptr);
      else
      if (ch == 'i' && strsame (cptr, "#index", 6))
         SsiDoDir (rqptr);
      else
      if (ch == 'm' && strsame (cptr, "#modified", 9))
         SsiDoModified (rqptr);
      else
      if (ch == 'o' && strsame (cptr, "#orif", 5))
         SsiDoOrif (rqptr);
      else
      if (ch == 'p' && strsame (cptr, "#printenv", 9))
         SsiDoPrintEnv (rqptr);
      else
      /* not needed with WASD v7.2ff, backward compatibility, do nothing! */
      if (ch == 's' && strsame (cptr, "#scrunch", 8))
         status = 0;
      else
      if (ch == 's' && strsame (cptr, "#set", 4))
         SsiDoSet (rqptr);
      else
      if (ch == 's' && strsame (cptr, "#stop", 5))
         SsiDoStop (rqptr);
      else
      /* #trace is kept for backward compatibility */
      if (ch == 't' && strsame (cptr, "#trace", 6))
         SsiDoTrace (rqptr);
      else
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_STATEMENT_UNKNOWN), FI_LI);
         SsiEnd (rqptr);
         return;
      }
   }
   else
   {
      /********************************************/
      /* flow control is currently not processing */
      /********************************************/

      /* looking only for the following */
      if (ch == 'b' && strsame (cptr, "#begin", 6))
         SsiDoBegin (rqptr);
      else
      if (ch == 'i' && strsame (cptr, "#if", 3))
         SsiDoIf (rqptr);
      else
      if (ch == 'o' && strsame (cptr, "#orif", 5))
         SsiDoOrif (rqptr);
      else
      if (ch == 'e' && strsame (cptr, "#elif", 5))
         SsiDoElif (rqptr);
      else
      if (ch == 'e' && strsame (cptr, "#else", 5))
         SsiDoElse (rqptr);
      else
      if (ch == 'e' && strsame (cptr, "#endif", 6))
         SsiDoEndif (rqptr);
      else
         SysDclAst (&SsiParse, rqptr);
   }
}

/*****************************************************************************/
/*
Number of times this document has been accessed.  Working with AST-driven code
is sometimes like participating in a lonnngg, sloowww root-canal.
*/ 

SsiDoAccesses (REQUEST_STRUCT *rqptr)

{
   int  status;
   char  *dptr;
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiDoAccesses()");

   tkptr = rqptr->SsiTaskPtr;

   if (tkptr->TraceState) SsiTraceStatement (rqptr);

   if (!Config.cfSsi.AccessesEnabled)
   {
      SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_ACCESS_DISABLED), FI_LI);
      SsiEnd (rqptr);
      return;
   }

   tkptr->AccessSinceText[0] = tkptr->FormatString[0] = '\0';
   tkptr->AccessOrdinal = false;

   dptr = rqptr->SsiTaskPtr->StatementBeginPtr;
   while (*dptr && !isspace(*dptr)) dptr++;

   while (*dptr)
   {
      if (rqptr->SsiTaskPtr->StopProcessing) break;

      while (*dptr && isspace(*dptr)) dptr++;
      if (!*dptr) break;

      if (strsame (dptr, "ORDINAL", 7))
      {
         dptr += 7;
         tkptr->AccessOrdinal = true;
      }
      else
      if (strsame (dptr, "SINCE=", 6))
         dptr += SsiGetTagValue (rqptr, dptr, tkptr->AccessSinceText,
                                 sizeof(tkptr->AccessSinceText));
      else
      if (strsame (dptr, "TIMEFMT=", 8))
         dptr += SsiGetTagValue (rqptr, dptr, tkptr->FormatString,
                                 sizeof(tkptr->FormatString));
      else
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_TAG_UNKNOWN), FI_LI);
         SsiEnd (rqptr);
         return;
      }
   }

   if (rqptr->SsiTaskPtr->StopProcessing)
   {
      SsiEnd (rqptr);
      return;
   }

   /* if the file's access count has been previously determined */
   if (tkptr->AccessCount)
      SsiAccessesOutput (rqptr);
   else
      SsiAccessesOpen (rqptr);
}

/*****************************************************************************/
/*
AST-driven file create/open.
These functions do not need to be ODS-5 aware as they do not use a NAM block.
*/ 

SsiAccessesOpen (REQUEST_STRUCT *rqptr)

{
   int  status;
   char  *dptr, *cptr, *sptr, *zptr;
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   tkptr = rqptr->SsiTaskPtr;

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                 "SsiAccessesOpen() !&F", &SsiAccessesOpen);

   zptr = (sptr = tkptr->ScratchFileName) + sizeof(tkptr->ScratchFileName);
   for (cptr = tkptr->FileContentPtr->FileName;
        *cptr && *cptr != ';' && sptr < zptr;
        *sptr++ = *cptr++);
   if (sptr < zptr) *sptr++ = '$';
   if (sptr >= zptr)
   {
      SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_GENERAL_OVERFLOW), FI_LI);
      SsiEnd (rqptr);
      return;
   }
   *sptr = '\0';
   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", tkptr->ScratchFileName);

   tkptr->AccessFab = cc$rms_fab;
   tkptr->AccessFab.fab$l_ctx = rqptr;
   tkptr->AccessFab.fab$l_fna = tkptr->ScratchFileName;  
   tkptr->AccessFab.fab$b_fns = sptr-tkptr->ScratchFileName;
   tkptr->AccessFab.fab$l_fop = FAB$M_CIF | FAB$M_SQO;
   tkptr->AccessFab.fab$b_fac = FAB$M_GET | FAB$M_PUT | FAB$M_UPD;
   tkptr->AccessFab.fab$w_mrs = sizeof(unsigned long);
   tkptr->AccessFab.fab$b_shr = FAB$M_SHRGET | FAB$M_SHRPUT | FAB$M_SHRUPD;
   tkptr->AccessFab.fab$b_org = FAB$C_SEQ;
   tkptr->AccessFab.fab$b_rfm = FAB$C_FIX;

   /* initialize the date extended attribute block */
   tkptr->AccessFab.fab$l_xab = &tkptr->AccessXabDat;
   tkptr->AccessXabDat = cc$rms_xabdat;
   tkptr->AccessXabDat.xab$l_nxt = &tkptr->AccessXabPro;

   /* initialize the protection extended attribute block */
   tkptr->AccessXabPro = cc$rms_xabpro;
   tkptr->AccessXabPro.xab$w_pro = 0xaa00;  /* W:RE,G:RE,O:RWED,S:RWED */

   /* "temporary" file, automatic delete on closing it */
   if (rqptr->DeleteOnClose) tkptr->AccessFab.fab$l_fop |= FAB$M_DLT;

   /* turn on SYSPRV to allow access to the counter file */
   sys$setprv (1, &SysPrvMask, 0, 0);
   tkptr->AccessFab.fab$l_fop |= FAB$M_ASY;
   status = sys$create (&tkptr->AccessFab, &SsiAccessesOpenAst,
                                           &SsiAccessesOpenAst);
   sys$setprv (0, &SysPrvMask, 0, 0);
}

/*****************************************************************************/
/*
AST called from SsiDoAccesses() when asynchronous RAB connect completes.
*/ 

SsiAccessesOpenAst (struct FAB *FabPtr)

{
   int  status;
   REQUEST_STRUCT  *rqptr;
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   rqptr = FabPtr->fab$l_ctx;

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                 "SsiAccessesOpenAst() !&F sts:!&S stv:!&S",
                 &SsiAccessesOpenAst, FabPtr->fab$l_sts, FabPtr->fab$l_stv);

#if WATCH_MOD
   HttpdCheckPriv (FI_LI);
#endif /* WATCH_MOD */

   tkptr = rqptr->SsiTaskPtr;

   if (VMSnok (FabPtr->fab$l_sts) && FabPtr->fab$l_stv)
      status = FabPtr->fab$l_stv;
   else
      status = FabPtr->fab$l_sts;

   if (VMSnok (status))
   {
      SsiProblem (rqptr, "!&m", status, FI_LI);
      tkptr->AccessAstFunction = &SsiEnd;
      SsiAccessesClose (&tkptr->AccessFab);
      return;
   }

   tkptr->AccessRab = cc$rms_rab;
   tkptr->AccessRab.rab$l_ctx = rqptr;
   tkptr->AccessRab.rab$l_fab = &tkptr->AccessFab;
   tkptr->AccessRab.rab$l_rbf = tkptr->AccessCount;
   tkptr->AccessRab.rab$w_rsz = sizeof(tkptr->AccessCount);

   status = sys$connect (&tkptr->AccessRab, &SsiAccessesConnectAst,
                                            &SsiAccessesConnectAst);
}

/*****************************************************************************/
/*
AST called from SsiDoAccesses() when asynchronous RAB connect completes.
*/ 

SsiAccessesConnectAst (struct RAB *RabPtr)

{
   int  status;
   REQUEST_STRUCT  *rqptr;
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   rqptr = RabPtr->rab$l_ctx;

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                 "SsiAccessesConnectAst() !&F sts:!&S stv:!&S",
                 &SsiAccessesConnectAst, RabPtr->rab$l_sts, RabPtr->rab$l_stv);

   tkptr = rqptr->SsiTaskPtr;

   if (VMSnok (RabPtr->rab$l_sts) && RabPtr->rab$l_stv)
      status = RabPtr->rab$l_stv;
   else
      status = RabPtr->rab$l_sts;

   if (VMSnok (status))
   {
      SsiProblem (rqptr, "!&m", status, FI_LI);
      tkptr->AccessAstFunction = &SsiEnd;
      SsiAccessesClose (&tkptr->AccessFab);
      return;
   }

   if (RabPtr->rab$l_sts == RMS$_CREATED)
   {
      /***************************************/
      /* count file did not previously exist */
      /***************************************/

      tkptr->AccessCount = 1;
      tkptr->AccessRab.rab$l_rbf = &tkptr->AccessCount;
      tkptr->AccessRab.rab$w_rsz = sizeof(tkptr->AccessCount);
      tkptr->AccessRab.rab$l_rop |= RAB$M_ASY;
      sys$put (&tkptr->AccessRab, &SsiAccessesUpdateAst,
                                  &SsiAccessesUpdateAst);
      return;
   }
   else
   {
      /**********************/
      /* count file existed */
      /**********************/

      tkptr->AccessRab.rab$l_ubf = &tkptr->AccessCount;
      tkptr->AccessRab.rab$w_usz = sizeof(tkptr->AccessCount);
      tkptr->AccessRab.rab$l_rop |= RAB$M_ASY;
      status = sys$get (&tkptr->AccessRab, &SsiAccessesGetAst,
                                           &SsiAccessesGetAst); 
      return;
   }
}

/*****************************************************************************/
/*
Count file existed and the sys$get() of the previous count from 
SsiAccessesConnectAst() has generated an AST to this function.
*/ 

SsiAccessesGetAst (struct RAB *RabPtr)

{
   int  status;
   REQUEST_STRUCT  *rqptr;
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   rqptr = RabPtr->rab$l_ctx;
   tkptr = rqptr->SsiTaskPtr;

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                 "SsiAccessesGetAst() !&F, sts:!&S stv:!&S !UL",
                 &SsiAccessesGetAst, RabPtr->rab$l_sts,
                 RabPtr->rab$l_stv, tkptr->AccessCount);

   if (VMSnok (RabPtr->rab$l_sts) && RabPtr->rab$l_stv)
      status = RabPtr->rab$l_stv;
   else
      status = RabPtr->rab$l_sts;

   if (VMSnok (status))
   {
      if (status != RMS$_EOF)
      {
         SsiProblem (rqptr, "!&m", status, FI_LI);
         tkptr->AccessAstFunction = &SsiEnd;
         SsiAccessesClose (&tkptr->AccessFab);
         return;
      }

      /* EOF indicates the initial write failed (e.g. disk quota) */
      tkptr->AccessCount = 1;
      tkptr->AccessRab.rab$l_rbf = &tkptr->AccessCount;
      tkptr->AccessRab.rab$w_rsz = sizeof(tkptr->AccessCount);
      tkptr->AccessRab.rab$l_rop |= RAB$M_ASY;
      sys$put (&tkptr->AccessRab, &SsiAccessesUpdateAst,
                                  &SsiAccessesUpdateAst);
      return;
   }

   tkptr->AccessCount++;

   tkptr->AccessRab.rab$l_rbf = &tkptr->AccessCount;
   tkptr->AccessRab.rab$w_rsz = sizeof(tkptr->AccessCount);
   tkptr->AccessRab.rab$l_rop |= RAB$M_ASY;
   status = sys$update (&tkptr->AccessRab, &SsiAccessesUpdateAst,
                                           &SsiAccessesUpdateAst);
}

/*****************************************************************************/
/*
Called as an AST from either the sys$put() in SsiAccessesConnectAst() or the
sys$update() in SsiAccessesgetAst().  Check the status of the update to the
access count in the file and if OK generated the required SSI page output.
*/ 

SsiAccessesUpdateAst (struct RAB *RabPtr)

{
   int  status;
   REQUEST_STRUCT  *rqptr;
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   rqptr = RabPtr->rab$l_ctx;
   tkptr = rqptr->SsiTaskPtr;

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                 "SsiAccessesUpdateAst() !&F sts:!&S stv:!&S !UL",
                 &SsiAccessesUpdateAst, RabPtr->rab$l_sts,
                 RabPtr->rab$l_stv, tkptr->AccessCount);

   if (VMSnok (RabPtr->rab$l_sts) && RabPtr->rab$l_stv)
      status = RabPtr->rab$l_stv;
   else
      status = RabPtr->rab$l_sts;

   if (VMSnok (status))
   {
      SsiProblem (rqptr, "!&m", status, FI_LI);
      tkptr->AccessAstFunction = &SsiEnd;
      SsiAccessesClose (&tkptr->AccessFab);
      return;
   }

   SsiAccessesOutput (rqptr);
}

/*****************************************************************************/
/*
This function can be called from a number of places to close an open access
count file. Prior to calling this function 'tkptr->AccessAstFunction' should
be set to the function required to be executed after the close is complete.
*/ 

SsiAccessesClose (struct FAB *FabPtr)

{
   int  status;
   REQUEST_STRUCT  *rqptr;
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   rqptr = FabPtr->fab$l_ctx;

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                 "SsiAccessesClose() !&F", &SsiAccessesClose);

   tkptr = rqptr->SsiTaskPtr;

   if (tkptr->AccessFab.fab$w_ifi)
   {
      if (rqptr->DeleteOnClose) tkptr->AccessFab.fab$l_fop |= FAB$M_DLT;
      /* use SYSPRV to ensure alarm-free close of file */
      sys$setprv (1, &SysPrvMask, 0, 0);
      tkptr->AccessFab.fab$l_fop |= FAB$M_ASY;
      status = sys$close (&tkptr->AccessFab,
                          &SsiAccessesFileClosed,
                          &SsiAccessesFileClosed);
      sys$setprv (0, &SysPrvMask, 0, 0);
      return;
   }

   SysDclAst (tkptr->AccessAstFunction, rqptr);
}

/*****************************************************************************/
/*
May be called directly or as an AST from sys$close() above.
*/

SsiAccessesFileClosed (struct FAB *FabPtr)

{
   REQUEST_STRUCT  *rqptr;
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   rqptr = FabPtr->fab$l_ctx;
   tkptr = rqptr->SsiTaskPtr;

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                 "SsiAccessesFileClosed() !&F sts:!&S stv:!&S",
                 &SsiAccessesFileClosed, FabPtr->fab$l_sts, FabPtr->fab$l_stv);

#if WATCH_MOD
   HttpdCheckPriv (FI_LI);
#endif /* WATCH_MOD */

   SysDclAst (tkptr->AccessAstFunction, rqptr);
} 

/*****************************************************************************/
/*
Generate and output the formatted access count.
*/ 

SsiAccessesOutput (REQUEST_STRUCT *rqptr)

{
   static char  *Ordination [] =
      { "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th" };

   int  status,
        TensUnits;
   unsigned short  Length;
   char  *OrdinalPtr;
   char  TimeString [256];
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                 "SsiAccessesOutput() !8XL", &SsiAccessesOutput);

   tkptr = rqptr->SsiTaskPtr;

   if (tkptr->AccessOrdinal)
   {
      TensUnits = tkptr->AccessCount % 100;
      if (TensUnits >= 11 && TensUnits <= 19)
         OrdinalPtr = "th";
      else
         OrdinalPtr = Ordination[TensUnits%10];
   }
   else
      OrdinalPtr = "";

   if (tkptr->AccessSinceText[0])
   {
      if (!SsiTimeString (rqptr, &tkptr->AccessXabDat.xab$q_cdt,
                          tkptr->FormatString,
                          TimeString, sizeof(TimeString)))
      {
         SsiEnd (rqptr);
         return;
      }
   }
   else
      TimeString[0] = '\0';

   status = NetWriteFao (rqptr, "!&,UL!AZ!AZ!AZ", 
                         tkptr->AccessCount, OrdinalPtr,
                         tkptr->AccessSinceText, TimeString);
   if (VMSnok (status)) ErrorNoticed (status, "NetWriteFao()", FI_LI);
   tkptr->TraceOutput = true;

   if (tkptr->AccessFab.fab$w_ifi)
   {
      /* we've been access the on-disk file */
      tkptr->AccessAstFunction = &SsiParse;
      SsiAccessesClose (&tkptr->AccessFab);
   }
   else
   {
      /* already had the access count, just continue */
      SysDclAst (&SsiParse, rqptr);
   }
}

/*****************************************************************************/
/*
OSU-compliant, marking the beginning of an #includable "part".
*/

SsiDoBegin (REQUEST_STRUCT *rqptr)

{
   char  String [SSI_INCLUDE_PART_MAX+1];
   char  *cptr, *dptr, *sptr, *zptr;
   SSI_TASK  *tkptr,
             *ParentDocumentTaskPtr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiDoBegin()");

   tkptr = rqptr->SsiTaskPtr;

   if (!tkptr->OsuCompliant ||
       !tkptr->CurrentPart[0])
   {
      /* declare an AST to execute the next function */
      SysDclAst (&SsiParse, rqptr);
      return;
   }

   if (tkptr->TraceState) SsiTraceStatement (rqptr);

   dptr = tkptr->StatementBeginPtr;
   while (*dptr && !isspace(*dptr)) dptr++;
   while (*dptr && isspace(*dptr)) dptr++;

   while (*dptr)
   {
      while (*dptr && (*dptr == '\"' || isspace(*dptr))) dptr++;
      if (!*dptr) break;

      zptr = (sptr = String) + sizeof(String);
      while (*dptr && *dptr != '\"' && !isspace(*dptr) && sptr < zptr)
         *sptr++ = *dptr++;
      if (sptr >= zptr)
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_GENERAL_OVERFLOW), FI_LI);
         SsiEnd (rqptr);
         return;
      }
      *sptr = '\0';

      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                    "!&Z !&Z", String, tkptr->CurrentPart);
      if (strcmp (String, tkptr->CurrentPart)) continue;

      tkptr->SuppressLine = true;
                    
      tkptr->FlowControlIndex++;
      if (tkptr->FlowControlIndex > SSI_MAX_FLOW_CONTROL)
      {
         tkptr->FlowControlIndex = 0;
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_FLOW_CONTROL), FI_LI);
         SsiEnd (rqptr);
         return;
      }

      /* beginning of the #included part */
      tkptr->FlowControlState[tkptr->FlowControlIndex] = SSI_STATE_BEGIN;
      tkptr->FlowControlHasExecuted[tkptr->FlowControlIndex] =
         tkptr->FlowControlIsExecuting[tkptr->FlowControlIndex] = true;
      break;
   }

   /* declare an AST to execute the next function */
   SysDclAst (&SsiParse, rqptr);
}

/*****************************************************************************/
/*
Comment hidden from the final document because it's inside an SSI statement!
*/

SsiDoComment (REQUEST_STRUCT *rqptr)

{
   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiDoComment()");

   if (rqptr->SsiTaskPtr->TraceState) SsiTraceStatement (rqptr);

   rqptr->SsiTaskPtr->SuppressLine = true;

   SysDclAst (&SsiParse, rqptr);
}

/*****************************************************************************/
/*
Config sets the default behaviour for a specific action for the rest of the 
document.
*/ 

SsiDoConfig (REQUEST_STRUCT *rqptr)

{
   int  status,
        BufferSize;
   char  *cptr, *dptr;
   char  TagValue [256];
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiDoConfig() !&Z",
                 rqptr->SsiTaskPtr->StatementBeginPtr);

   tkptr = rqptr->SsiTaskPtr;

   if (tkptr->TraceState) SsiTraceStatement (rqptr);

   dptr = tkptr->StatementBeginPtr;
   while (*dptr && !isspace(*dptr)) dptr++;
   while (*dptr && isspace(*dptr)) dptr++;

   if (strsame (dptr, "BUFFERSIZE", 6))
   {
      /* not needed with WASD v7.2ff, backward compatibility, do nothing! */
      dptr += SsiGetTagValue (rqptr, dptr, TagValue, sizeof(TagValue));
   }
   else
   if (strsame (dptr, "COMPLIANCE", 10))
   {
      dptr += SsiGetTagValue (rqptr, dptr, TagValue, sizeof(TagValue));
      tkptr->ComplianceLevel = atoi(TagValue);
   }
   else
   if (strsame (dptr, "ERRMSG", 6))
   {
      dptr += SsiGetTagValue (rqptr, dptr, TagValue, sizeof(TagValue));
      cptr = VmGetHeap (rqptr, strlen(TagValue)+1);
      strcpy (tkptr->ErrMsgPtr = cptr, TagValue);
   }
   else
   if (strsame (dptr, "SIZEFMT", 7))
   {
      dptr += SsiGetTagValue (rqptr, dptr, TagValue, sizeof(TagValue));
      if (!(strsame (TagValue, "abbrev", -1) ||
            strsame (TagValue, "bytes", -1) ||
            strsame (TagValue, "blocks", -1)))
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_TAG_VALUE), FI_LI);
         SsiEnd (rqptr);
         return;
      }
      cptr = VmGetHeap (rqptr, strlen(TagValue)+1);
      strcpy (tkptr->SizeFmtPtr = cptr, TagValue);
   }
   else
   if (strsame (dptr, "OSU", 3))
   {
      dptr += SsiGetTagValue (rqptr, dptr, TagValue, sizeof(TagValue));
      if (toupper(TagValue[0]) == 'Y' || TagValue[0] == '1')
         tkptr->OsuCompliant = true;
      else
      if (toupper(TagValue[0]) == 'N' || TagValue[0] == '0')
         tkptr->OsuCompliant = false;
      else
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_TAG_VALUE), FI_LI);
         SsiEnd (rqptr);
         return;
      }
   }
   else
   if (strsame (dptr, "TIMEFMT", 7))
   {
      dptr += SsiGetTagValue (rqptr, dptr, TagValue, sizeof(TagValue));
      cptr = VmGetHeap (rqptr, strlen(TagValue)+1);
      strcpy (tkptr->TimeFmtPtr = cptr, TagValue);
   }
   else
   if (strsame (dptr, "TRACE", 5))
   {
      dptr += SsiGetTagValue (rqptr, dptr, TagValue, sizeof(TagValue));
      if ((toupper(TagValue[0]) == 'O' && toupper(TagValue[1]) == 'N') ||
          toupper(TagValue[0]) == 'Y' || 
          TagValue[0] == '1')
      {
         if (!rqptr->SsiTaskPtr->TraceState)
         {
            NetWriteBuffered (rqptr, NULL, "<PRE>", 5);
            rqptr->rqOutput.BufferEscapeHtml = true;
            tkptr->TraceState = true;
         }
         SsiTraceStatement (rqptr);
      }
      else
      if ((toupper(TagValue[0]) == 'O' && toupper(TagValue[1]) == 'F') ||
          toupper(TagValue[0]) == 'N' || 
          TagValue[0] == '0')
      {
         if (rqptr->SsiTaskPtr->TraceState)
         {
            SsiTraceStatement (rqptr);
            rqptr->rqOutput.BufferEscapeHtml = false;
            NetWriteBuffered (rqptr, NULL, "</PRE>", 6);
            tkptr->TraceState = false;
         }
      }
      else
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_TAG_VALUE), FI_LI);
         SsiEnd (rqptr);
         return;
      }
   }
   else
   if (strsame (dptr, "VERIFY", 6))
   {
      dptr += SsiGetTagValue (rqptr, dptr, TagValue, sizeof(TagValue));
      if (toupper(TagValue[0]) == 'Y' || TagValue[0] == '1')
         tkptr->TagVerify = true;
      else
      if (toupper(TagValue[0]) == 'N' || TagValue[0] == '0')
         tkptr->TagVerify = false;
      else
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_TAG_VALUE), FI_LI);
         SsiEnd (rqptr);
         return;
      }
   }
   else
   {
      SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_TAG_UNKNOWN), FI_LI);
      SsiEnd (rqptr);
      return;
   }

   /* declare an AST to execute the required function */
   SysDclAst (&SsiParse, rqptr);
}

/*****************************************************************************/
/*
Process a DCL/EXEC statement.  The array at the beginning of this function
provides  the allowed DCL statements and their DCL command equivalents.  Only
innocuous  DCL commands are allowed (hopefully! e.g. SHOW, WRITE SYS$OUTPUT)
for the  average document.  For documents owned by SYSTEM and NOT WORLD
WRITEABLE (for obvious reasons) the execution of any DCL command or command
procedure is allowed allowing maximum flexibility for the privileged document
author.
*/ 

SsiDoDcl (REQUEST_STRUCT *rqptr)

{
   /*
      First element is allowed DCL command.
      Second is actual DCL verb/command/syntax.
      Third, if non-empty, idicates a file specification is required.
      Fourth, if 'P', indicates it is for privileged documents only!
              if '@', that parameter should be checked for "@file.com" hack!
   */
   static char  *SupportedDcl [] =
   {
      "cgi", "", "", "@",                    /* any document */
      "cmd", "", "", "P",                    /* privileged only! */
      "dir", "directory ", "", "@",          /* any document */
      "exec", "", "", "P",                   /* privileged only! */
      "file", "@", "Y", "P",                 /* privileged only! */
      "run", "run ", "Y", "P",               /* privileged only! */
      "say", "write sys$output", "", "@",    /* any document */
      "script", "", "", "@",                 /* any document */
      "show", "show", "", "@",               /* any document */
      "vdir", "directory ", "", "@",         /* any document */
      "virtual", "@", "Y", "P",              /* privileged only! */
      "vrun", "run ", "Y", "P",              /* privileged only! */
      "", "", "", "P"  /* must be terminated by empty/privileged command! */
   };

   int  idx, status;
   char  *cptr, *dptr, *sptr, *zptr;
   char  DclCommand [1024],
         MappedFile [ODS_MAX_FILE_NAME_LENGTH+1],
         ScriptName [ODS_MAX_FILE_NAME_LENGTH+1],
         MappedScript [ODS_MAX_FILE_NAME_LENGTH+1],
         MappedRunTime [ODS_MAX_FILE_NAME_LENGTH+1],
         TagValue [256];
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiDoDcl() !&Z",
                 rqptr->SsiTaskPtr->StatementBeginPtr);

   tkptr = rqptr->SsiTaskPtr;

   if (tkptr->TraceState) SsiTraceStatement (rqptr);

   dptr = rqptr->SsiTaskPtr->StatementBeginPtr;
   while (*dptr && !isspace(*dptr)) dptr++;
   while (*dptr && isspace(*dptr)) dptr++;

   for (idx = 0; *SupportedDcl[idx]; idx += 4)
   {
      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z !&Z !&Z !&Z",
                    SupportedDcl[idx], SupportedDcl[idx+1],
                    SupportedDcl[idx+2], SupportedDcl[idx+3]);
      /* compare the user-supplied DCL command to the list of allowed */
      cptr = SupportedDcl[idx];
      sptr = dptr;
      while (*cptr && *sptr != '=' && toupper(*cptr) == toupper(*sptr))
         { cptr++; sptr++; }
      if (!*cptr && *sptr == '=') break;
   }

   if (!SupportedDcl[idx][0] &&
       !strsame (dptr = rqptr->SsiTaskPtr->StatementBeginPtr, "#exec ", 6))
   {
      /************************/
      /* unsupported command! */
      /************************/

      SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_DCL_UNSUPPORTED), FI_LI);
      SsiEnd (rqptr);
      return;
   }

   /*************************/
   /* supported DCL command */
   /*************************/

   if (rqptr->rqPathSet.SsiExecPtr)
   {
      /**************************/
      /* path SETing controlled */
      /**************************/

      cptr = rqptr->rqPathSet.SsiExecPtr;
      while (*cptr == '#') cptr++;
      while (*cptr && *cptr != '*')
      {
         sptr = SupportedDcl[idx];
         while (*cptr && *cptr != ',' && *sptr &&
                tolower(*cptr) == tolower(*sptr))
            { cptr++; sptr++; }
         if (!*sptr && (!*cptr || *cptr == ','))
         {
            cptr = "*";
            break;
         }
         while (*cptr && *cptr != ',') cptr++;
         while (*cptr == ',') cptr++;
      }
      if (!*cptr)
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_DCL_DISABLED), FI_LI);
         SsiEnd (rqptr);
         return;
      }
   }
   else
   if (SupportedDcl[idx+3][0] == 'P')
   {
      /******************************/
      /* "privileged" DCL requested */
      /******************************/

      if (!Config.cfSsi.ExecEnabled)
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_DCL_DISABLED), FI_LI);
         SsiEnd (rqptr);
         return;
      }

      if (!rqptr->rqPathSet.PrivSsi)
      {
         /* SYSTEM is UIC [1,4] */
         if (tkptr->FileContentPtr->UicGroup != 1 ||
             tkptr->FileContentPtr->UicMember != 4)
         {
            SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_DCL_NOT_SYSTEM), FI_LI);
            SsiEnd (rqptr);
            return;
         }

         /* protect word: wwggooss, protect bits: dewr, 1 denies access */
         if (!(tkptr->FileContentPtr->Protection & 0x2000))
         {
            SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_DCL_NOT_WORLD),
                        FI_LI);
            SsiEnd (rqptr);
            return;
         }
      }
   }

   /****************************************************/
   /* create the DCL command from array and parameters */
   /****************************************************/

   cptr = SupportedDcl[idx+1];
   sptr = DclCommand;
   while (*cptr) *sptr++ = *cptr++;
   *sptr = '\0';

   if (SupportedDcl[idx+2][0])
   {
      /* file specification involved */
      dptr += SsiGetFileSpec (rqptr, dptr, TagValue, sizeof(TagValue));
   }
   else
   {
      /* some DCL verb/parameters or another */
      dptr += SsiGetTagValue (rqptr, dptr, TagValue, sizeof(TagValue));
      if (TagValue[0] && sptr > DclCommand) *sptr++ = ' ';
   }
   for (cptr = TagValue; *cptr; *sptr++ = *cptr++);
   *sptr = '\0';
   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", DclCommand);

   /************************/
   /* check for this issue */
   /************************/

   if (SupportedDcl[idx+3][0] == '@')
   {
      for (cptr = TagValue; *cptr && *cptr != '@' && *cptr != '\''; cptr++);
      if (*cptr)
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_DCL_UNSUPPORTED), FI_LI);
         SsiEnd (rqptr);
         return;
      }
   }

   /************/
   /* continue */
   /************/

   /* by default HTML-forbidden characters in DCL output are escaped */
   rqptr->rqOutput.BufferEscapeHtml = true;

   while (*dptr)
   {
      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", dptr);

      if (rqptr->SsiTaskPtr->StopProcessing) break;

      while (*dptr && isspace(*dptr)) dptr++;
      if (!*dptr) break;

      if (strsame (dptr, "PAR=", 4))
      {
         /* parameters to the command procedure, directory, etc. */
         dptr += SsiGetTagValue (rqptr, dptr, TagValue, sizeof(TagValue));
         if (TagValue[0]) *sptr++ = ' ';
         for (cptr = TagValue; *cptr; *sptr++ = *cptr++);
         *sptr = '\0';
      }
      else
      if (strsame (dptr, "TYPE=", 5))
      {
         dptr += SsiGetTagValue (rqptr, dptr, TagValue, sizeof(TagValue));
         if (TagValue[0] && strsame (TagValue, "text/html", -1))
            rqptr->rqOutput.BufferEscapeHtml = tkptr->TraceState;
         else
            rqptr->rqOutput.BufferEscapeHtml = true;
      }
      else
      if (*dptr)
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_TAG_UNKNOWN), FI_LI);
         SsiEnd (rqptr);
         return;
      }
   }

   if (rqptr->SsiTaskPtr->StopProcessing)
   {
      SsiEnd (rqptr);
      return;
   }

   if (strsame (SupportedDcl[idx], "cgi", -1) ||
       strsame (SupportedDcl[idx], "script", -1))
   {
      /* it's lies, all lies! (fool mapping into not changing request data) */
      rqptr->rqCgi.CalloutInProgress = true;

      /* initialize with some nulls */
      *(unsigned long*)MappedFile =
         *(unsigned long*)ScriptName =
         *(unsigned long*)MappedScript =
         *(unsigned long*)MappedRunTime = 0;

      cptr = MapUrl_Map (DclCommand,
                         sizeof(DclCommand),
                         MappedFile,
                         sizeof(MappedFile),
                         ScriptName,
                         sizeof(ScriptName),
                         MappedScript,
                         sizeof(MappedScript),
                         MappedRunTime,
                         sizeof(MappedRunTime),
                         NULL, rqptr);

      rqptr->rqCgi.CalloutInProgress = false;

      if (!cptr[0] && cptr[1])
      {
         SsiProblem (rqptr, "!AZ", cptr+1, FI_LI);
         SsiEnd (rqptr);
         return;
      }
      if (!ScriptName[0])
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SCRIPT_NOT_FOUND), FI_LI);
         SsiEnd (rqptr);
         return;
      }

      rqptr->rqCgi.AbsorbHeader = rqptr->rqCgi.BufferRecords = true;

      for (cptr = MappedScript; *cptr && *cptr != ':'; cptr++);
      if (*(unsigned short*)cptr == '::')
      {
         /* hmmm, 'ScriptName' could present a bit of a problem here */
         DECnetBegin (rqptr, &SsiParse, MappedScript, MappedRunTime);
      }
      else
      if (ScriptName[0] == '+')
      {
         ScriptName[0] = '/';
         DclBegin (rqptr, &SsiParse, NULL,
                   ScriptName, NULL, MappedScript, MappedRunTime, NULL);
      }
      else
         DclBegin (rqptr, &SsiParse, NULL,
                   ScriptName, MappedScript, NULL, MappedRunTime, NULL);
   }
   else
      DclBegin (rqptr, &SsiParse, DclCommand, NULL, NULL, NULL, NULL, NULL);
}

/*****************************************************************************/
/*
Generate an "Index of" directory listing by calling DirBegin() task.
*/

SsiDoDir (REQUEST_STRUCT *rqptr)

{
   int  status;
   char  *cptr, *dptr;
   char  DirSpec [256],
         RealPath [256],
         TagValue [256];

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiDoDir() !&Z",
                 rqptr->SsiTaskPtr->StatementBeginPtr);

   if (rqptr->SsiTaskPtr->TraceState) SsiTraceStatement (rqptr);

   RealPath[0] = TagValue[0] = '\0';

   dptr = rqptr->SsiTaskPtr->StatementBeginPtr;
   while (*dptr && !isspace(*dptr)) dptr++;

   while (*dptr)
   {
      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", dptr);

      if (rqptr->SsiTaskPtr->StopProcessing) break;

      while (*dptr && isspace(*dptr)) dptr++;
      if (!*dptr) break;

      if (strsame (dptr, "FILE=", 5) || strsame (dptr, "VIRTUAL=", 8))
         dptr += SsiGetFileSpec (rqptr, dptr, DirSpec, sizeof(DirSpec));
      else
      if (strsame (dptr, "PAR=", 4))
         dptr += SsiGetTagValue (rqptr, dptr, TagValue, sizeof(TagValue));
      else
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_TAG_UNKNOWN), FI_LI);
         SsiEnd (rqptr);
         return;
      }
   }

   if (rqptr->SsiTaskPtr->StopProcessing)
   {
      SsiEnd (rqptr);
      return;
   }

   MapUrl_Map (RealPath, sizeof(RealPath), DirSpec,
               0, NULL, 0, NULL, 0, NULL, 0, NULL, rqptr);

   DirBegin (rqptr, &SsiParse, DirSpec, TagValue, RealPath, true);
}

/*****************************************************************************/
/*
Output a server or user-assigned variable.
*/

SsiDoEcho (REQUEST_STRUCT *rqptr)

{
   int  status;
   char  String [256],
         FormatString [256];
   char  *cptr, *dptr, *sptr, *zptr,
         *VarPtr;
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiDoEcho() !&Z",
                 rqptr->SsiTaskPtr->StatementBeginPtr);

   tkptr = rqptr->SsiTaskPtr;

   if (tkptr->TraceState) SsiTraceStatement (rqptr);

   FormatString[0] = '\0';

   dptr = rqptr->SsiTaskPtr->StatementBeginPtr;
   /* two formats: '<!--#"' and '<!--#echo' */
   if (*(unsigned short*)dptr == '#\"')
      dptr++;
   else
   {
      while (*dptr && !isspace(*dptr)) dptr++;
      while (*dptr && isspace(*dptr)) dptr++;
   }

   /* keep this so we can check for some OSU specific syntax */
   VarPtr = dptr;

   while (*dptr)
   {
      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", dptr);

      if (rqptr->SsiTaskPtr->StopProcessing) break;
 
      while (*dptr && isspace(*dptr)) dptr++;
      if (!*dptr) break;

      if (*dptr == '\"')
      {
         /* format is <!--#echo "blah-blah" --> */
         dptr += SsiGetTagValue (rqptr, dptr, cptr = String, sizeof(String));
      }
      else
      if (strsame (dptr, "VALUE=", 6) ||
          strsame (dptr, "VAR=", 4))
      {
         /* format is <!--#echo var="" --> */
         dptr += SsiGetTagValue (rqptr, dptr, cptr = String, sizeof(String));
      }
      else
      {
         /* format is <!--#echo NAME[=format] --> */
         zptr = (sptr = String) + sizeof(String);
         while (*dptr && !isspace(*dptr) && *dptr != '=' && sptr < zptr)
            *sptr++ = toupper(*dptr++);
         if (sptr >= zptr)
         {
            SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_GENERAL_OVERFLOW), FI_LI);
            SsiEnd (rqptr);
            return;
         }
         *sptr = '\0';
         if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
            WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", String);

         if (*dptr == '=')
            dptr += SsiGetTagValue (rqptr, dptr,
                                    FormatString, sizeof(FormatString));
         else
            FormatString[0] = '\0';

         cptr = SsiGetVar (rqptr, String, FormatString, false);
      }

      if (rqptr->SsiTaskPtr->StopProcessing) break;

      if (toupper(VarPtr[5]) == 'A')
      {
         /* OSU-compliant echoes */
         if (strsame (VarPtr, "VAR=\"ACCESSES_ORDINAL", 21) &&
             !isalpha(VarPtr[21]))
         {
            tkptr->AccessOrdinal = true;
            tkptr->AccessSinceText[0] = tkptr->FormatString[0] = '\0';
            /* if the file's access count has been previously determined */
            if (tkptr->AccessCount)
               SsiAccessesOutput (rqptr);
            else
               SsiAccessesOpen (rqptr);
            /* generated asynchronously */
            return;
         }
         if (strsame (VarPtr, "VAR=\"ACCESSES", 13) &&
             !isalpha(VarPtr[13]))
         {
            tkptr->AccessOrdinal = false;
            tkptr->AccessSinceText[0] = tkptr->FormatString[0] = '\0';
            /* if the file's access count has been previously determined */
            if (tkptr->AccessCount)
               SsiAccessesOutput (rqptr);
            else
               SsiAccessesOpen (rqptr);
            /* generated asynchronously */
            return;
         }
      }

      if (tkptr->FlowControlIsExecuting[tkptr->FlowControlIndex])
      {
         NetWriteBuffered (rqptr, NULL, cptr, strlen(cptr));
         tkptr->TraceOutput = true;
      }
   }

   if (rqptr->SsiTaskPtr->StopProcessing)
   {
      SsiEnd (rqptr);
      return;
   }

   SysDclAst (&SsiParse, rqptr);
}

/*****************************************************************************/
/*
Flow control statement. If the current flow control is executing then it now
disallowed. If the current flow control structure (including "#if"s, "#orif"s
and other "#elif"s) have not allowed execution then evaluate the conditional
and allow execution if true.
*/

SsiDoElif (REQUEST_STRUCT *rqptr)

{
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiDoElif() !&Z",
                 rqptr->SsiTaskPtr->StatementBeginPtr);

   tkptr = rqptr->SsiTaskPtr;

   tkptr->SuppressLine = true;

   /* if previous flow control was not an "#if" or "#elif" */
   if (tkptr->FlowControlState[tkptr->FlowControlIndex] != SSI_STATE_IF &&
       tkptr->FlowControlState[tkptr->FlowControlIndex] != SSI_STATE_ELIF)
   {
      SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_FLOW_CONTROL), FI_LI);
      SsiEnd (rqptr);
      return;
   }

   tkptr->FlowControlState[tkptr->FlowControlIndex] = SSI_STATE_ELIF;

   if (tkptr->FlowControlHasExecuted[rqptr->SsiTaskPtr->FlowControlIndex])
   {
      /* if flow control has already output just continue parsing */
      tkptr->FlowControlIsExecuting[tkptr->FlowControlIndex] = false;
      SysDclAst (&SsiParse, rqptr);
      return;
   }

   /* if the parent level is executing and it evaluates true then execute */
   if (tkptr->FlowControlIsExecuting[tkptr->FlowControlIndex-1])
   {
      if (tkptr->TraceState) SsiTraceStatement (rqptr);
      tkptr->FlowControlHasExecuted[tkptr->FlowControlIndex] =
         tkptr->FlowControlIsExecuting[tkptr->FlowControlIndex] =
            SsiEvaluate (rqptr);
   }

   /* declare an AST to execute the next function */
   SysDclAst (&SsiParse, rqptr);
}

/*****************************************************************************/
/*
Flow control statement.  If the current flow control structure (including
"#if"s, "#orif"s and/or "#elif"s) have not allowed execution then this will.
*/

SsiDoElse (REQUEST_STRUCT *rqptr)

{
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiDoElse() !&Z",
                 rqptr->SsiTaskPtr->StatementBeginPtr);

   tkptr = rqptr->SsiTaskPtr;

   tkptr->SuppressLine = true;

   /* if previous flow control was not an "#if" or "#elif" */
   if (tkptr->FlowControlState[tkptr->FlowControlIndex] != SSI_STATE_IF &&
       tkptr->FlowControlState[tkptr->FlowControlIndex] != SSI_STATE_ELIF)
   {
      SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_FLOW_CONTROL), FI_LI);
      SsiEnd (rqptr);
      return;
   }

   tkptr->FlowControlState[tkptr->FlowControlIndex] = SSI_STATE_ELSE;

   /* if there has been no output so far */
   if (tkptr->FlowControlIsExecuting[tkptr->FlowControlIndex-1] &&
       !tkptr->FlowControlHasExecuted[tkptr->FlowControlIndex])
   {
      if (tkptr->TraceState) SsiTraceStatement (rqptr);
      tkptr->FlowControlHasExecuted[tkptr->FlowControlIndex] =
         tkptr->FlowControlIsExecuting[tkptr->FlowControlIndex] = true;
   }
   else
      tkptr->FlowControlIsExecuting[tkptr->FlowControlIndex] = false;

   /* declare an AST to execute the next function */
   SysDclAst (&SsiParse, rqptr);
}

/*****************************************************************************/
/*
OSU-compliant, marking the end of an #includable "part".
*/

SsiDoEnd (REQUEST_STRUCT *rqptr)

{
   char  String [SSI_INCLUDE_PART_MAX+1];
   char  *cptr, *dptr, *sptr, *zptr;
   SSI_TASK  *tkptr,
             *ParentDocumentTaskPtr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiDoEnd() !&Z",
                 rqptr->SsiTaskPtr->StatementBeginPtr);

   tkptr = rqptr->SsiTaskPtr;

   if (!tkptr->OsuCompliant ||
       !tkptr->CurrentPart[0])
   {
      /* declare an AST to execute the next function */
      SysDclAst (&SsiParse, rqptr);
      return;
   }

   tkptr->SuppressLine = true;

   if (tkptr->TraceState) SsiTraceStatement (rqptr);

   dptr = tkptr->StatementBeginPtr;
   while (*dptr && !isspace(*dptr)) dptr++;
   while (*dptr && isspace(*dptr)) dptr++;

   while (*dptr)
   {
      while (*dptr && (*dptr == '\"' || isspace(*dptr))) dptr++;
      if (!*dptr) break;

      zptr = (sptr = String) + sizeof(String);
      while (*dptr && *dptr != '\"' && !isspace(*dptr) && sptr < zptr)
         *sptr++ = *dptr++;
      if (sptr >= zptr)
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_GENERAL_OVERFLOW), FI_LI);
         SsiEnd (rqptr);
         return;
      }
      *sptr = '\0';

      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                    "!&Z !&Z", String, tkptr->CurrentPart);
      if (strcmp (String, tkptr->CurrentPart)) continue;

      /* end of the #included part */
      tkptr->FlowControlHasExecuted[tkptr->FlowControlIndex] =
         tkptr->FlowControlIsExecuting[tkptr->FlowControlIndex] = false;
      break;
   }

   tkptr->SuppressLine = true;

   if (tkptr->FlowControlIsExecuting[tkptr->FlowControlIndex])
   {
      /* not yet found, declare an AST to execute the next function */
      SysDclAst (&SsiParse, rqptr);
      return;
   }

   /* if previous flow control was not a #begin */
   if (tkptr->FlowControlState[tkptr->FlowControlIndex] != SSI_STATE_BEGIN)
   {
      SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_FLOW_CONTROL), FI_LI);
      SsiEnd (rqptr);
      return;
   }

   if (tkptr->FlowControlIndex)
   {
      tkptr->FlowControlIndex--;
      /* declare an AST to execute the next function */
      SysDclAst (&SsiParse, rqptr);
      return;
   }

   SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_FLOW_CONTROL), FI_LI);
   SsiEnd (rqptr);
}

/*****************************************************************************/
/*
Flow control statement.
*/

SsiDoEndif (REQUEST_STRUCT *rqptr)

{
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiDoEndIf()");

   tkptr = rqptr->SsiTaskPtr;

   if (tkptr->TraceState) SsiTraceStatement (rqptr);

   tkptr->SuppressLine = true;

   if (tkptr->FlowControlIndex)
   {
      tkptr->FlowControlIndex--;
      /* declare an AST to execute the next function */
      SysDclAst (&SsiParse, rqptr);
      return;
   }

   SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_FLOW_CONTROL), FI_LI);
   SsiEnd (rqptr);
}

/*****************************************************************************/
/*
Exit from current document processing here!
*/

SsiDoExit (REQUEST_STRUCT *rqptr)

{
   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiDoExit()");

   if (rqptr->SsiTaskPtr->TraceState) SsiTraceStatement (rqptr);

   SsiEnd (rqptr);
}

/*****************************************************************************/
/*
Output the specified file's creation date and time.
*/

SsiDoFCreated (REQUEST_STRUCT *rqptr)

{
   char  *dptr;
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiDoFCreated() !&Z",
                 rqptr->SsiTaskPtr->StatementBeginPtr);

   tkptr = rqptr->SsiTaskPtr;

   if (tkptr->TraceState) SsiTraceStatement (rqptr);

   tkptr->ScratchFileName[0] = tkptr->FormatString[0] = '\0';

   dptr = rqptr->SsiTaskPtr->StatementBeginPtr;
   while (*dptr && !isspace(*dptr)) dptr++;

   while (*dptr)
   {
      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", dptr);

      if (rqptr->SsiTaskPtr->StopProcessing) break;

      while (*dptr && isspace(*dptr)) dptr++;
      if (!*dptr) break;

      if (strsame (dptr, "FILE=", 5) || strsame (dptr, "VIRTUAL=", 8))
         dptr += SsiGetFileSpec (rqptr, dptr, tkptr->ScratchFileName,
                                              sizeof(tkptr->ScratchFileName));
      else
      if (strsame (dptr, "FMT=", 4))
         dptr += SsiGetTagValue (rqptr, dptr, tkptr->FormatString,
                                 sizeof(tkptr->FormatString));
      else
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_TAG_UNKNOWN), FI_LI);
         SsiEnd (rqptr);
         return;
      }
   }

   if (rqptr->SsiTaskPtr->StopProcessing)
   {
      SsiEnd (rqptr);
      return;
   }

   SsiFileDetails (rqptr, FILE_FCREATED);
}

/*****************************************************************************/
/*
Output the specified file's last modification date and time.
*/

SsiDoFLastMod (REQUEST_STRUCT *rqptr)

{
   char  *dptr;
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiDoFLastMod() !&Z",
                 rqptr->SsiTaskPtr->StatementBeginPtr);

   tkptr = rqptr->SsiTaskPtr;

   if (tkptr->TraceState) SsiTraceStatement (rqptr);

   tkptr->ScratchFileName[0] = tkptr->FormatString[0] = '\0';

   dptr = rqptr->SsiTaskPtr->StatementBeginPtr;
   while (*dptr && !isspace(*dptr)) dptr++;

   while (*dptr)
   {
      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", dptr);

      if (rqptr->SsiTaskPtr->StopProcessing) break;

      while (*dptr && isspace(*dptr)) dptr++;
      if (!*dptr) break;

      if (strsame (dptr, "FILE=", 5) || strsame (dptr, "VIRTUAL=", 8))
         dptr += SsiGetFileSpec (rqptr, dptr, tkptr->ScratchFileName,
                                              sizeof(tkptr->ScratchFileName));
      else
      if (strsame (dptr, "FMT=", 4))
         dptr += SsiGetTagValue (rqptr, dptr, tkptr->FormatString,
                                 sizeof(tkptr->FormatString));
      else
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_TAG_UNKNOWN), FI_LI);
         SsiEnd (rqptr);
         return;
      }
   }

   if (rqptr->SsiTaskPtr->StopProcessing)
   {
      SsiEnd (rqptr);
      return;
   }

   SsiFileDetails (rqptr, FILE_FLASTMOD);
}

/*****************************************************************************/
/*
Output the specified file's size.
*/

SsiDoFSize (REQUEST_STRUCT *rqptr)

{
   char  *dptr;
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiDoFSize() !&Z",
                 rqptr->SsiTaskPtr->StatementBeginPtr);

   tkptr = rqptr->SsiTaskPtr;

   if (tkptr->TraceState) SsiTraceStatement (rqptr);

   tkptr->ScratchFileName[0] = tkptr->FormatString[0] = '\0';

   dptr = rqptr->SsiTaskPtr->StatementBeginPtr;
   while (*dptr && !isspace(*dptr)) dptr++;

   while (*dptr)
   {
      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", dptr);

      if (rqptr->SsiTaskPtr->StopProcessing) break;

      while (*dptr && isspace(*dptr)) dptr++;
      if (!*dptr) break;

      if (strsame (dptr, "FILE=", 5) || strsame (dptr, "VIRTUAL=", 8))
         dptr += SsiGetFileSpec (rqptr, dptr, tkptr->ScratchFileName,
                                              sizeof(tkptr->ScratchFileName));
      else
      if (strsame (dptr, "FMT=", 4))
         dptr += SsiGetTagValue (rqptr, dptr, tkptr->FormatString,
                                 sizeof(tkptr->FormatString));
      else
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_TAG_UNKNOWN), FI_LI);
         SsiEnd (rqptr);
         return;
      }
   }

   if (rqptr->SsiTaskPtr->StopProcessing)
   {
      SsiEnd (rqptr);
      return;
   }

   SsiFileDetails (rqptr, FILE_FSIZE);
}

/*****************************************************************************/
/*
Directive for changing/generating HTTP response headers and/or fields.
*/

SsiDoModified (REQUEST_STRUCT *rqptr)

{
   static char  Expires [64] = "Expires: ",
                LastModified [64] = "Last-Modified: ";
   /* point immediately after the hard-wired strings */
   static char  *ExpiresPtr = Expires + 9,
                *LastModifiedPtr = LastModified + 15;
   /* available == size - start - '\r' - '\n' - '\0' */
   static int  ExpiresAvailable = sizeof(Expires) - 12;

   BOOL  DoIfModifiedSince,
         DoLastModified;
   int  status;
   char  *dptr;
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiDoModified() !&Z",
                 rqptr->SsiTaskPtr->StatementBeginPtr);

   tkptr = rqptr->SsiTaskPtr;

   if (tkptr->TraceState) SsiTraceStatement (rqptr);

   DoLastModified = DoIfModifiedSince = false;
   *ExpiresPtr = '\0';
   tkptr->ScratchFileName[0] = '\0';

   dptr = rqptr->SsiTaskPtr->StatementBeginPtr;
   while (*dptr && !isspace(*dptr)) dptr++;

   while (*dptr)
   {
      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", dptr);

      if (tkptr->StopProcessing) break;

      while (*dptr && isspace(*dptr)) dptr++;
      if (!*dptr) break;

      if (strsame (dptr, "EXPIRES=", 8))
         dptr += SsiGetTagValue (rqptr, dptr, ExpiresPtr, ExpiresAvailable);
      else
      if (strsame (dptr, "FILE=", 5) || strsame (dptr, "VIRTUAL=", 8))
         dptr += SsiGetFileSpec (rqptr, dptr, tkptr->ScratchFileName,
                                              sizeof(tkptr->ScratchFileName));
      else
      if (strsame (dptr, "FMT=", 4))
         dptr += SsiGetTagValue (rqptr, dptr, tkptr->FormatString,
                                 sizeof(tkptr->FormatString));
      else
      if (strsame (dptr, "IF-MODIFIED-SINCE", 17))
      {
         dptr += 17;
         DoLastModified = false;
         DoIfModifiedSince = true;
      }
      else
      if (strsame (dptr, "LAST-MODIFIED", 13))
      {
         dptr += 13;
         DoLastModified = true;
         DoIfModifiedSince = false;
      }
      else
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_TAG_UNKNOWN), FI_LI);
         SsiEnd (rqptr);
         return;
      }
   }

   if (rqptr->SsiTaskPtr->StopProcessing)
   {
      SsiEnd (rqptr);
      return;
   }

   if (DoIfModifiedSince)
   {
      /******************************/
      /* check "If-Modified-Since:" */
      /******************************/

      if (!tkptr->LastModifiedBinTime[0] &&
          !tkptr->LastModifiedBinTime[1])
      {
         /* no files' RDTs have been checked */
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_TAG_VALUE), FI_LI);
         SsiEnd (rqptr);
         return;
      }

      if (!rqptr->PragmaNoCache)
      {
         if (VMSnok (status =
             HttpIfModifiedSince (rqptr, &tkptr->LastModifiedBinTime, -1)))
         {
            /****************/
            /* not modified */
            /****************/

            /* status LIB$_NEGTIM if not modified, header sent, quit now */
            SsiEnd (rqptr);
            return;
         }
      }

      /******************/
      /* modified since */
      /******************/

      /* supply a "Last-Modified:" response header */
      if (VMSnok (status =
          HttpGmTimeString (LastModifiedPtr, &tkptr->LastModifiedBinTime)))
      {
         SsiProblem (rqptr, "!&m", status, FI_LI);
         SsiEnd (rqptr);
         return;
      }
      strcat (LastModifiedPtr, "\r\n");
      ResponseHeaderAppend (rqptr, LastModified, 0);

      /* now just continue on */
      SysDclAst (&SsiParse, rqptr);
      return;
   }

   if (DoLastModified)
   {
      /*****************************/
      /* explicit "Last-Modified:" */
      /*****************************/

      if (!tkptr->LastModifiedBinTime[0] &&
          !tkptr->LastModifiedBinTime[1])
      {
         /* no files' RDTs have been checked */
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_TAG_VALUE), FI_LI);
         SsiEnd (rqptr);
         return;
      }

      if (VMSnok (status =
          HttpGmTimeString (LastModifiedPtr, &tkptr->LastModifiedBinTime)))
      {
         SsiProblem (rqptr, "!&m", status, FI_LI);
         SsiEnd (rqptr);
         return;
      }
      strcat (LastModifiedPtr, "\r\n");
      ResponseHeaderAppend (rqptr, LastModified, 0);

      /* just continue on */
      SysDclAst (&SsiParse, rqptr);
      return;
   }

   if (*ExpiresPtr)
   {
      /***********************/
      /* explicit "Expires:" */
      /***********************/

      strcat (ExpiresPtr, "\r\n");
      ResponseHeaderAppend (rqptr, Expires, 0);

      /* just continue on */
      SysDclAst (&SsiParse, rqptr);
      return;
   }

   if (tkptr->ScratchFileName[0])
   {
      /******************/
      /* get file's RDT */
      /******************/

      /* get the revision timestamp of the specified file */
      SsiFileDetails (rqptr, FILE_LAST_MODIFIED);
      return;
   }

   /* hmmm, nothing specified, check the modification date of this file */
   strcpy (tkptr->ScratchFileName, tkptr->FileContentPtr->FileName);
   SsiFileDetails (rqptr, FILE_LAST_MODIFIED);
   return;
}

/*****************************************************************************/
/*
Flow control statement.  If the parent level is executing evalutate the
conditional and allow execution if true.
*/

SsiDoIf (REQUEST_STRUCT *rqptr)

{
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiDoIf() !&Z",
                 rqptr->SsiTaskPtr->StatementBeginPtr);

   tkptr = rqptr->SsiTaskPtr;

   tkptr->SuppressLine = true;

   tkptr->FlowControlIndex++;
   if (tkptr->FlowControlIndex > SSI_MAX_FLOW_CONTROL)
   {
      tkptr->FlowControlIndex = 0;
      SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_FLOW_CONTROL), FI_LI);
      SsiEnd (rqptr);
      return;
   }

   tkptr->FlowControlState[tkptr->FlowControlIndex] = SSI_STATE_IF;

   /* if the parent level is executing and it evaluates true then execute */
   if (tkptr->FlowControlIsExecuting[tkptr->FlowControlIndex-1])
   {
      if (tkptr->TraceState) SsiTraceStatement (rqptr);
      tkptr->FlowControlHasExecuted[tkptr->FlowControlIndex] =
         tkptr->FlowControlIsExecuting[tkptr->FlowControlIndex] =
            SsiEvaluate (rqptr);
   }
   else
      tkptr->FlowControlHasExecuted[tkptr->FlowControlIndex] =
         tkptr->FlowControlIsExecuting[tkptr->FlowControlIndex] = false;

   /* declare an AST to execute the next function */
   SysDclAst (&SsiParse, rqptr);
}

/*****************************************************************************/
/*
Include the contents of the specified file by calling FileBegin() task.  The
tag content="text/..." makes any file extension acceptable as the text type an
presents it as that ("text/html" is included unescaped, "text/plain" escaped).  
This makes a file with any extension accepted as text!  The tag
type="text/plain" (older variant) forces a "text/html" file to be escaped and
presented as a plain text file.
*/

SsiDoInclude (REQUEST_STRUCT *rqptr)

{
   BOOL  ok;
   int  status;
   char  *cptr, *dptr, *sptr, *zptr;
   char  FileContent [256],
         FileFormat [256],
         FileName [256];
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiDoInclude() !&Z",
                 rqptr->SsiTaskPtr->StatementBeginPtr);

   tkptr = rqptr->SsiTaskPtr;

   if (tkptr->TraceState) SsiTraceStatement (rqptr);

   tkptr->IncludePart[0] = tkptr->TheFileNameVar[0] = '\0';
   FileContent[0] = FileName[0] = FileFormat[0] = '\0';

   dptr = tkptr->StatementBeginPtr;
   while (*dptr && !isspace(*dptr)) dptr++;

   while (*dptr)
   {
      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", dptr);

      if (tkptr->StopProcessing) break;

      while (*dptr && isspace(*dptr)) dptr++;
      if (!*dptr) break;

      if (strsame (dptr, "FILE=", 5) ||
          strsame (dptr, "VIRTUAL=", 8))
         dptr += SsiGetFileSpec (rqptr, dptr, FileName, sizeof(FileName));
      else
      if (strsame (dptr, "TYPE=", 5))
         dptr += SsiGetTagValue (rqptr, dptr, FileFormat, sizeof(FileFormat));
      else
      if (strsame (dptr, "CONTENT=", 8))
         dptr += SsiGetTagValue (rqptr, dptr, FileContent, sizeof(FileContent));
      else
      if (strsame (dptr, "FMT=", 4))
         dptr += SsiGetTagValue (rqptr, dptr, tkptr->FormatString,
                                 sizeof(tkptr->FormatString));
      else
      if (tkptr->OsuCompliant &&
          strsame (dptr, "PART=", 5))
         dptr += SsiGetTagValue (rqptr, dptr, tkptr->IncludePart,
                                 sizeof(tkptr->IncludePart));
      else
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_TAG_UNKNOWN), FI_LI);
         SsiEnd (rqptr);
         return;
      }               
   }

   if (tkptr->StopProcessing)
   {
      SsiEnd (rqptr);
      return;
   }

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                 "!&Z !&Z !&Z", FileName, FileContent, FileFormat);

   if (tkptr->OsuCompliant)
   {
      /* OSU only includes other SSI files */
      cptr = ConfigContentTypeSsi;
   }
   else
   if (!(cptr = FileContent)[0])
   {
      /* find the file extension and set the content type */
      for (cptr = FileName; *cptr && *cptr != ']'; cptr++);
      while (*cptr && *cptr != '.') cptr++;
      cptr = ConfigContentType (NULL, cptr);
   }

   if (!strsame (cptr, "text/", 5))
   {
      SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_INCLUDE_NOT_TEXT), FI_LI);
      SsiEnd (rqptr);
      return;
   }

   /* set the variable file name */
   strcpy (tkptr->TheFileNameVar, FileName);

   if (FileFormat[0]) cptr = FileFormat;
   if (ConfigSameContentType (cptr, "text/plain", -1))
   {
      /* it's plain text, so make it look like it, with escaped HTML */
      FileSetPreTag (rqptr, true);
      FileSetEscapeHtml (rqptr, true);
   }
   if (ConfigSameContentType (cptr, ConfigContentTypeSsi, -1))
   {
      /* buffer the file contents and pass them to the SSI engine */
      FileSetContentHandler (rqptr, &SsiBegin, SsiSizeMax);
   }
   FileSetCacheAllowed (rqptr, true);
   FileSetAuthorizePath (rqptr, true);

   /* ultimately return to the SSI engine parser */
   FileBegin (rqptr, &SsiParse, &SsiIncludeError, NULL, FileName, cptr);
}

/*****************************************************************************/
/*
*/

SsiIncludeError (REQUEST_STRUCT *rqptr)

{
   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                 "SsiIncludeError() !&F !&Z",
                 &SsiIncludeError, rqptr->SsiTaskPtr->FormatString);

   if (rqptr->SsiTaskPtr->FormatString[0] == '?')
   {
      /* reset the file variable data to indicate it was not found */
      rqptr->SsiTaskPtr->TheFileNameVar[0] = '\0';

      /* declare an AST to continue processing */
      SysDclAst (&SsiParse, rqptr);
      return;
   }
   else
   {
      /* report the error and stop processing */
      SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_INCLUDE_ACCESS), FI_LI);
      SsiEnd (rqptr);
      return;
   }
}

/*****************************************************************************/
/*
Flow control statement. If the preceding control statement was an "#if" or
"#elif" and it did not evaluate true then this allows another chance to
execute this segment. It is essentially an OR on the last evalution. The
evaluation here is not performed if the previous allowed execution.
*/

SsiDoOrif (REQUEST_STRUCT *rqptr)

{
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiDoOrIf() !&Z",
                 rqptr->SsiTaskPtr->StatementBeginPtr);

   tkptr = rqptr->SsiTaskPtr;

   tkptr->SuppressLine = true;

   /* if previous flow control was not an "#if" or "#elif" */
   if (tkptr->FlowControlState[tkptr->FlowControlIndex] != SSI_STATE_IF &&
       tkptr->FlowControlState[tkptr->FlowControlIndex] != SSI_STATE_ELIF)
   {
      SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_FLOW_CONTROL), FI_LI);
      SsiEnd (rqptr);
      return;
   }

   /* if the parent level is executing and the current is not then evaluate */
   if (tkptr->FlowControlIsExecuting[tkptr->FlowControlIndex-1] &&
       !tkptr->FlowControlIsExecuting[tkptr->FlowControlIndex])
   {
      if (tkptr->TraceState) SsiTraceStatement (rqptr);
      tkptr->FlowControlHasExecuted[tkptr->FlowControlIndex] =
         tkptr->FlowControlIsExecuting[tkptr->FlowControlIndex] =
            SsiEvaluate (rqptr);
   }

   /* declare an AST to execute the next function */
   SysDclAst (&SsiParse, rqptr);
}

/*****************************************************************************/
/*
Print all server and user-assigned variables. Usually used for no more than
document debugging. Variable names and values have HTML- forbidden characters
escaped.
*/

SsiDoPrintEnv (REQUEST_STRUCT *rqptr)

{
   BOOL  EscapeHtmlBuffer;
   int  status,
        Length;
   char  *cptr, *sptr;
   char  VarName [256];
   LIST_ENTRY  *leptr;
   SSI_TASK  *tkptr;
   SSI_VAR  *varptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiDoPrintEnv()");

   tkptr = rqptr->SsiTaskPtr;

   if (tkptr->TraceState) SsiTraceStatement (rqptr);

   if (tkptr->TraceState)
      NetWriteBuffered (rqptr, NULL, "\n\n", 2);
   else
      NetWriteBuffered (rqptr, NULL, "<PRE>\n\n", 6);
   EscapeHtmlBuffer = rqptr->rqOutput.BufferEscapeHtml;
   rqptr->rqOutput.BufferEscapeHtml = true;
   tkptr->TraceOutput = true;

   /* SSI variables */
   cptr = SsiGetServerVar (rqptr, sptr = "COMPLIANCE", NULL);
   SsiPrintEnvVar (rqptr, sptr, cptr);
   cptr = SsiGetServerVar (rqptr, sptr = "CREATED", NULL);
   SsiPrintEnvVar (rqptr, sptr, cptr);
   cptr = SsiGetServerVar (rqptr, sptr = "DATE_GMT", NULL);
   SsiPrintEnvVar (rqptr, sptr, cptr);
   cptr = SsiGetServerVar (rqptr, sptr = "DATE_LOCAL", NULL);
   SsiPrintEnvVar (rqptr, sptr, cptr);
   cptr = SsiGetServerVar (rqptr, sptr = "DOCUMENT_DEPTH", NULL);
   SsiPrintEnvVar (rqptr, sptr, cptr);
   cptr = SsiGetServerVar (rqptr, sptr = "DOCUMENT_NAME", NULL);
   SsiPrintEnvVar (rqptr, sptr, cptr);
   cptr = SsiGetServerVar (rqptr, sptr = "DOCUMENT_ROOT", NULL);
   SsiPrintEnvVar (rqptr, sptr, cptr);
   cptr = SsiGetServerVar (rqptr, sptr = "DOCUMENT_URI", NULL);
   SsiPrintEnvVar (rqptr, sptr, cptr);

   if (rqptr->RedirectErrorStatusCode)
   {
      /* SSI variables that are only present during an error report */
      cptr = SsiGetServerVar (rqptr, sptr = "ERROR_LINE", NULL);
      SsiPrintEnvVar (rqptr, sptr, cptr);
      cptr = SsiGetServerVar (rqptr, sptr = "ERROR_MODULE", NULL);
      SsiPrintEnvVar (rqptr, sptr, cptr);
      cptr = SsiGetServerVar (rqptr, sptr = "ERROR_REPORT", NULL);
      SsiPrintEnvVar (rqptr, sptr, cptr);
      cptr = SsiGetServerVar (rqptr, sptr = "ERROR_REPORT2", NULL);
      SsiPrintEnvVar (rqptr, sptr, cptr);
      cptr = SsiGetServerVar (rqptr, sptr = "ERROR_REPORT3", NULL);
      SsiPrintEnvVar (rqptr, sptr, cptr);
      cptr = SsiGetServerVar (rqptr, sptr = "ERROR_STATUS_CLASS", NULL);
      SsiPrintEnvVar (rqptr, sptr, cptr);
      cptr = SsiGetServerVar (rqptr, sptr = "ERROR_STATUS_CODE", NULL);
      SsiPrintEnvVar (rqptr, sptr, cptr);
      cptr = SsiGetServerVar (rqptr, sptr = "ERROR_STATUS_EXPLANATION", NULL);
      SsiPrintEnvVar (rqptr, sptr, cptr);
      cptr = SsiGetServerVar (rqptr, sptr = "ERROR_STATUS_TEXT", NULL);
      SsiPrintEnvVar (rqptr, sptr, cptr);
      cptr = SsiGetServerVar (rqptr, sptr = "ERROR_TYPE", NULL);
      SsiPrintEnvVar (rqptr, sptr, cptr);
   }

   cptr = SsiGetServerVar (rqptr, sptr = "FILE_NAME", NULL);
   SsiPrintEnvVar (rqptr, sptr, cptr);
   /* OSU-compliant variable */
   cptr = SsiGetServerVar (rqptr, sptr = "HW_NAME", NULL);
   SsiPrintEnvVar (rqptr, sptr, cptr);
   cptr = SsiGetServerVar (rqptr, sptr = "LAST_MODIFIED", NULL);
   SsiPrintEnvVar (rqptr, sptr, cptr);
   cptr = SsiGetServerVar (rqptr, sptr = "QUERY_STRING_UNESCAPED", NULL);
   SsiPrintEnvVar (rqptr, sptr, cptr);
   cptr = SsiGetServerVar (rqptr, sptr = "PARENT_FILE_NAME", NULL);
   SsiPrintEnvVar (rqptr, sptr, cptr);
   /* OSU-compliant variable */
   cptr = SsiGetServerVar (rqptr, sptr = "SERVER_VERSION", NULL);
   SsiPrintEnvVar (rqptr, sptr, cptr);
   cptr = SsiGetServerVar (rqptr, sptr = "THE_FILE_NAME", NULL);
   SsiPrintEnvVar (rqptr, sptr, cptr);
   cptr = SsiGetServerVar (rqptr, sptr = "THIS_FILE_NAME", NULL);
   SsiPrintEnvVar (rqptr, sptr, cptr);
   /* OSU-compliant variable */
   cptr = SsiGetServerVar (rqptr, sptr = "VMS_VERSION", NULL);
   SsiPrintEnvVar (rqptr, sptr, cptr);

   /* CGI variables */
   NetWriteBuffered (rqptr, NULL, "\n", 1);
   cptr = tkptr->CgiBufferPtr;
   for (;;)
   {
      if (!(Length = *(short*)cptr)) break;
      SsiPrintEnvVar (rqptr, cptr+sizeof(short)+DclCgiVariablePrefixLength,
                      NULL);
      cptr += Length + sizeof(short);
   }

   /* user variables */
   for (leptr = tkptr->SsiVarList.HeadPtr;
        leptr;
        leptr = leptr->NextPtr)
   {
      /* separate the first user variable using a blank line */
      if (leptr == tkptr->SsiVarList.HeadPtr)
         NetWriteBuffered (rqptr, NULL, "\n", 1);
      varptr = (SSI_VAR*)leptr;
      SsiPrintEnvVar (rqptr, varptr->NamePtr, varptr->ValuePtr);
   }

   rqptr->rqOutput.BufferEscapeHtml = EscapeHtmlBuffer;
   if (!tkptr->TraceState)
      NetWriteBuffered (rqptr, NULL, "</PRE>", 6);

   SysDclAst (&SsiParse, rqptr);
}

/*****************************************************************************/
/*
Simply do the "printenv" of a single variable for SsiDoPrintEnv().
*/

SsiPrintEnvVar
(
REQUEST_STRUCT *rqptr,
char* VarName,
char* VarValue
)
{
   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiPrintEnvVar() !AZ=!AZ",
                 VarName, VarValue);

   NetWriteBuffered (rqptr, NULL, VarName, strlen(VarName));
   if (VarValue)
   {
      NetWriteBuffered (rqptr, NULL, "=", 1);
      NetWriteBuffered (rqptr, NULL, VarValue, strlen(VarValue));
   }
   NetWriteBuffered (rqptr, NULL, "\n", 1);
}

/*****************************************************************************/
/*
Set a user variable. Server variables cannot be set. Existing variable names
are of course set to the new value. New variables are created ex nihlo and
added to a simple linked list.
*/

SsiDoSet (REQUEST_STRUCT *rqptr)

{
   BOOL  PreTagFileContents;
   int  status,
        Size,
        VarNameLength,
        VarValueLength;
   char  *cptr, *dptr;
   char  VarName [256],
         VarValue [1024];
   SSI_VAR  *varptr;
   LIST_ENTRY  *leptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiDoSet() !&Z",
                 rqptr->SsiTaskPtr->StatementBeginPtr);

   rqptr->SsiTaskPtr->SuppressLine = true;

   if (rqptr->SsiTaskPtr->TraceState) SsiTraceStatement (rqptr);

   dptr = rqptr->SsiTaskPtr->StatementBeginPtr;
   while (*dptr && !isspace(*dptr)) dptr++;

   VarName[0] = VarValue[0]= '\0';
   VarNameLength = VarValueLength = 0;

   while (*dptr)
   {
      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", dptr);

      if (rqptr->SsiTaskPtr->StopProcessing)
      {
         SsiEnd (rqptr);
         return;
      }

      while (*dptr && isspace(*dptr)) dptr++;
      if (!*dptr) break;

      if (strsame (dptr, "VAR=", 4))
      {
         dptr += SsiGetTagValue (rqptr, dptr, VarName, sizeof(VarName));
         VarNameLength = strlen(VarName);
         continue;
      }

      if (strsame (dptr, "VALUE=", 6))
      {
         dptr += SsiGetTagValue (rqptr, dptr, VarValue, sizeof(VarValue));
         VarValueLength = strlen(VarValue);
      }
      else
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_TAG_UNKNOWN), FI_LI);
         SsiEnd (rqptr);
         return;
      }               

      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z !&Z", VarName, VarValue);

      if (rqptr->SsiTaskPtr->StopProcessing)
      {
         SsiEnd (rqptr);
         return;
      }

      if (!VarName[0])
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_VARIABLE), FI_LI);
         SsiEnd (rqptr);
         return;
      }
      for (cptr = VarName; *cptr; cptr++)
         if (!isalnum(*cptr) && *cptr != '_') break;
      if (*cptr)
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_VARIABLE), FI_LI);
         SsiEnd (rqptr);
         return;
      }

      if (SsiGetServerVar (rqptr, VarName, NULL))
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_VARIABLE), FI_LI);
         SsiEnd (rqptr);
         return;
      }

      /* allow for terminating nulls then round to a set amount */
      Size = sizeof(SSI_VAR) + VarNameLength + VarValueLength + 2;
      if (Size < 128) Size = 128; else
         if (Size < 256) Size = 256; else
            if (Size < 1024) Size = 1024; else
               if (Size < 4096) Size = 4096;
      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!UL", Size);

      /* look for an existing user variable */
      varptr = NULL;
      for (leptr = rqptr->SsiTaskPtr->SsiVarList.HeadPtr;
           leptr;
           leptr = leptr->NextPtr)
      {
         varptr = (SSI_VAR*)leptr;
         if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
            WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                       "!&Z !&Z", varptr->NamePtr, varptr->ValuePtr);
         /* break if variable name found in list */
         if (toupper(*VarName) == toupper(*varptr->NamePtr) &&
             strsame (VarName, varptr->NamePtr, -1))
            break;
         varptr = NULL;
      }
      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", varptr);

      if (!varptr)
      {
         /* new user variable */
         if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
            WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                       "NEW !UL !&Z", Size, VarValue);

         varptr = VmGetHeap (rqptr, Size);
         ListAddTail (&rqptr->SsiTaskPtr->SsiVarList, varptr);

         varptr->Size = Size;
         varptr->NamePtr = varptr->Data;
         memcpy (varptr->NamePtr, VarName, VarNameLength+1);
         varptr->NameLength = VarNameLength;
         varptr->ValuePtr = varptr->NamePtr + VarNameLength+1;
         memcpy (varptr->ValuePtr, VarValue, VarValueLength+1);
         varptr->ValueLength = VarValueLength;
      }
      else
      {
         /* found an existing user variable */
         if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
            WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                       "EXISTING !UL !UL !&Z", varptr->Size, Size, VarValue);

         if (varptr->Size < Size)
         {
            /* size needs to be increased */
            ListRemove (&rqptr->SsiTaskPtr->SsiVarList, varptr);
            VmFreeFromHeap (rqptr, varptr, FI_LI);
            varptr = VmGetHeap (rqptr, Size);
            ListAddTail (&rqptr->SsiTaskPtr->SsiVarList, varptr);

            varptr->Size = Size;
            varptr->NamePtr = varptr->Data;
            memcpy (varptr->NamePtr, VarName, VarNameLength+1);
            varptr->NameLength = VarNameLength;
            varptr->ValuePtr = varptr->NamePtr + VarNameLength+1;
            memcpy (varptr->ValuePtr, VarValue, VarValueLength+1);
            varptr->ValueLength = VarValueLength;
         }
         else
         {
            /* variable memory stayed the same */
            memcpy (varptr->ValuePtr, VarValue, VarValueLength+1);
            varptr->ValueLength = VarValueLength;
         }
      }

      if (rqptr->SsiTaskPtr->TraceState)
         SsiTraceSetVar (rqptr, VarName, VarValue);

      /* ready for the next round (if any) */
      VarValue[0]= '\0';
      VarValueLength = 0;
   }

   SysDclAst (&SsiParse, rqptr);
}

/*****************************************************************************/
/*
Stop processing the document here!
*/

SsiDoStop (REQUEST_STRUCT *rqptr)

{
   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiDoStop()");

   if (rqptr->SsiTaskPtr->TraceState) SsiTraceStatement (rqptr);

   rqptr->SsiTaskPtr->StopProcessing = true;

   SsiEnd (rqptr);
}

/*****************************************************************************/
/*
Turn the SSI trace on or off ("#config trace=1" is prefered, this is kept for
backward compatibility).
*/

SsiDoTrace (REQUEST_STRUCT *rqptr)

{
   char  *cptr, *dptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiDoTrace() !&Z",
                 rqptr->SsiTaskPtr->StatementBeginPtr);

   dptr = rqptr->SsiTaskPtr->StatementBeginPtr;
   while (*dptr && !isspace(*dptr)) dptr++;
   while (*dptr && isspace(*dptr)) dptr++;
   for (cptr = dptr; *cptr && !isspace(*cptr); cptr++);
   *cptr = '\0';

   if ((toupper(dptr[0]) == 'O' && toupper(dptr[1]) == 'N') ||
       toupper(dptr[0]) == 'Y' || 
       dptr[0] == '1')
   {
      if (!rqptr->SsiTaskPtr->TraceState)
      {
         NetWriteBuffered (rqptr, NULL, "<PRE>", 5);
         rqptr->rqOutput.BufferEscapeHtml = true;
         rqptr->SsiTaskPtr->TraceState = true;
      }
      SsiTraceStatement (rqptr);
   }
   else
   if ((toupper(dptr[0]) == 'O' && toupper(dptr[1]) == 'F') ||
       toupper(dptr[0]) == 'N' || 
       dptr[0] == '0')
   {
      if (rqptr->SsiTaskPtr->TraceState)
      {
         SsiTraceStatement (rqptr);
         rqptr->rqOutput.BufferEscapeHtml = false;
         NetWriteBuffered (rqptr, NULL, "</PRE>", 6);
         rqptr->SsiTaskPtr->TraceState = false;
      }
   }
   else
   if (rqptr->SsiTaskPtr->TraceState)
      SsiTraceStatement (rqptr);

   SysDclAst (&SsiParse, rqptr);
}

/*****************************************************************************/
/*
Display the current SSI file record.
*/

SsiTraceLine
(
REQUEST_STRUCT *rqptr,
char *cptr
)
{
   int  status;
   char  ch;
   char  *sptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiTraceLine()");

   for (sptr = cptr; *sptr && *sptr != '\n'; sptr++);
   ch = *sptr;
   *sptr = '\0';
   status = NetWriteFao (rqptr,
"!&?\n\r\r<FONT COLOR=\"#0000cc\"><B>[!4ZL:!UL]!&;AZ</B></FONT>\n",
               rqptr->SsiTaskPtr->TraceOutput,
               rqptr->SsiTaskPtr->LineNumber,
               rqptr->SsiTaskPtr->FlowControlIndex,
               cptr);
   if (VMSnok (status)) ErrorNoticed (status, "NetWriteFao()", FI_LI);
   *sptr = ch;
   rqptr->SsiTaskPtr->TraceOutput = false;
}

/*****************************************************************************/
/*
Display the current SSI statement.
*/

SsiTraceStatement (REQUEST_STRUCT *rqptr)

{
   int  status;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiTraceStatement()");

   status = NetWriteFao (rqptr,
"<FONT COLOR=\"#cc0000\"><B>[!&;AZ]</B></FONT>",
                         rqptr->SsiTaskPtr->StatementBeginPtr);
   if (VMSnok (status)) ErrorNoticed (status, "NetWriteFao()", FI_LI);
   rqptr->SsiTaskPtr->TraceOutput = true;
}

/*****************************************************************************/
/*
Display the name and value of a variable as it is retrieved.
*/

SsiTraceGetVar
(
REQUEST_STRUCT *rqptr,
char *VarName,
char *VarValue
)
{
   int  status;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                 "SsiTraceGetVar() !AZ=!AZ", VarName, VarValue);

   status = NetWriteFao (rqptr,
"<FONT COLOR=\"#cc00cc\"><B>[!&;AZ:!&;AZ]</B></FONT>",
                         VarName, VarValue);
   if (VMSnok (status)) ErrorNoticed (status, "NetWriteFao()", FI_LI);
   rqptr->SsiTaskPtr->TraceOutput = true;
}

/*****************************************************************************/
/*
Display the name and value of a variable as it is stored.
*/

SsiTraceSetVar
(
REQUEST_STRUCT *rqptr,
char *VarName,
char *VarValue
)
{
   int  status;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiTraceSetVar()");

   status = NetWriteFao (rqptr,
"<FONT COLOR=\"#cc00cc\"><B>[!&;AZ=!&;AZ]</B></FONT>",
                         VarName, VarValue);
   if (VMSnok (status)) ErrorNoticed (status, "NetWriteFao()", FI_LI);
   rqptr->SsiTaskPtr->TraceOutput = true;
}

/*****************************************************************************/
/*
First search the server-assigned variables, then the user-assigned variables.
If found return a pointer to a symbol's value string.  If no such symbol found
return a NULL.
*/

char* SsiGetVar
(
REQUEST_STRUCT *rqptr,
char *VarName,
char *Format,
BOOL CheckOnly
)
{
   char  *ValuePtr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                 "SsiGetVar() !AZ \'!AZ\' !UL", VarName, Format, CheckOnly);

   ValuePtr = SsiGetServerVar (rqptr, VarName, Format);
   if (rqptr->SsiTaskPtr->StopProcessing) return (ValuePtr);
   if (ValuePtr)  goto SsiGetVarReturn;

   ValuePtr = SsiGetUserVar (rqptr, VarName);
   if (rqptr->SsiTaskPtr->StopProcessing) return (ValuePtr);
   if (ValuePtr)  goto SsiGetVarReturn;

   if (CheckOnly) goto SsiGetVarReturn;

   /* variable not found */
   ValuePtr = MsgFor(rqptr,MSG_SSI_VARIABLE_NOT_FOUND);

   SsiGetVarReturn:

      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", ValuePtr);
      return (ValuePtr);
}

/*****************************************************************************/
/*
Return a pointer to the value of a server-assigned variable, NULL if no such
variable.  The calling routine might want to check for an error message being
generated by bad format or time from SsiTimeString().
*/

char* SsiGetServerVar
(
REQUEST_STRUCT *rqptr,
char *VarName,
char *VarParam
)
{
#define SIZEOF_TIME_STRING 64

   static $DESCRIPTOR (NumberFaoDsc, "!UL\0");
   static $DESCRIPTOR (StringFaoDsc, "!AZ\0");
   static $DESCRIPTOR (ReportFaoDsc, "!AZ ... <TT>!AZ</TT>\0");
   static $DESCRIPTOR (Report2FaoDsc, "<!!-- !AZ \"!AZ\"-->\0");
   static $DESCRIPTOR (StringDsc, "");

   int  status,
        DocumentDepth,
        StatusCode;
   unsigned short  Length;
   unsigned long  BinTime [2];
   char  *cptr, *sptr, *zptr;
   char  String [256];
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                 "SsiGetServerVar() !AZ \'!AZ\'", VarName, VarParam);

   tkptr = rqptr->SsiTaskPtr;

   cptr = VarName;

   switch (toupper(cptr[0]))
   {
      case 'C' :

         if (strsame (cptr, "COMPLIANCE", -1))
         {
            cptr = VmGetHeap (rqptr, 16);
            StringDsc.dsc$a_pointer = cptr;
            StringDsc.dsc$w_length = 15;
            sys$fao (&NumberFaoDsc, &Length, &StringDsc,
                     tkptr->ComplianceLevel);
            return (cptr);
         }

         if (strsame (cptr, "CREATED", -1))
         {
            cptr = VmGetHeap (rqptr, SIZEOF_TIME_STRING);
            SsiTimeString (rqptr,
               &tkptr->RootDocumentTaskPtr->FileContentPtr->CdtBinTime,
               VarParam, cptr, SIZEOF_TIME_STRING);
            return (cptr);
         }

         break;

      case 'D' :

         if (strsame (cptr, "DATE_LOCAL", -1))
         {
            cptr = VmGetHeap (rqptr, SIZEOF_TIME_STRING);
            SsiTimeString (rqptr, NULL, VarParam, cptr, SIZEOF_TIME_STRING);
            return (cptr);
         }

         if (strsame (cptr, "DATE_GMT", -1))
         {
            sys$gettim (&BinTime);
            TimeAdjustGMT (true, &BinTime);
            cptr = VmGetHeap (rqptr, SIZEOF_TIME_STRING+5);
            SsiTimeString (rqptr, &BinTime, VarParam, cptr, SIZEOF_TIME_STRING);
            strcat (cptr, " GMT");
            return (cptr);
         }

         if (strsame (cptr, "DOCUMENT_DEPTH", -1))
         {
            if (tkptr->DocumentDepthPtr)
               return (tkptr->DocumentDepthPtr);
            StringDsc.dsc$a_pointer = String;
            StringDsc.dsc$w_length = sizeof(String)-1;
            DocumentDepth = LIST_GET_COUNT (&rqptr->SsiTaskList);
            sys$fao (&NumberFaoDsc, &Length, &StringDsc, DocumentDepth);
            tkptr->DocumentDepthPtr = cptr = VmGetHeap (rqptr, Length);
            strcpy (cptr, String);
            return (cptr);
         }

         if (strsame (cptr, "DOCUMENT_NAME", -1))
            return (SsiGetCgiVar (rqptr, "PATH_TRANSLATED"));

         if (strsame (cptr, "DOCUMENT_ROOT", -1))
         {
            if (tkptr->DocumentRootPtr) return (tkptr->DocumentRootPtr);
            tkptr->DocumentRootPtr = sptr =
               VmGetHeap (rqptr, rqptr->rqHeader.PathInfoLength+1);
            zptr = NULL;
            cptr = rqptr->rqHeader.PathInfoPtr;
            while (*cptr)
            {
               if (*cptr == '/') zptr = sptr;
               *sptr++ = *cptr++;
            }
            if (zptr)
               *(zptr+1) = '\0';
            else
               *tkptr->DocumentRootPtr = '\0';
            return (tkptr->DocumentRootPtr);
         }

         if (strsame (cptr, "DOCUMENT_URI", -1))
            return (rqptr->rqHeader.PathInfoPtr);

          break;

      case 'E' :

         if (!rqptr->RedirectErrorStatusCode) return (NULL);

         StringDsc.dsc$a_pointer = String;
         StringDsc.dsc$w_length = sizeof(String)-1;

         if (strsame (cptr, "ERROR_LINE", -1))
            return (SsiGetCgiVar (rqptr, "FORM_ERROR_LINE"));

         if (strsame (cptr, "ERROR_MODULE", -1))
            return (SsiGetCgiVar (rqptr, "FORM_ERROR_MODULE"));

         if (strsame (cptr, "ERROR_REPORT", -1))
         {
            if (!(cptr = SsiGetCgiVar (rqptr, "FORM_ERROR_TEXT")))
               return (NULL);
            /* return if it's an ErrorGeneral() error */
            if (!(sptr = SsiGetCgiVar (rqptr, "FORM_ERROR_ABOUT")))
               return (cptr);
            if (!*sptr) return (cptr);
            sys$fao (&ReportFaoDsc, &Length, &StringDsc, cptr, sptr);
            cptr = VmGetHeap (rqptr, Length);
            strcpy (cptr, String);
            return (cptr);
         }

         if (strsame (cptr, "ERROR_REPORT2", -1))
         {
            if (!(cptr = SsiGetCgiVar (rqptr, "FORM_ERROR_VMS")))
               return ("");
            /* return if it's an ErrorGeneral() error */
            if (!(sptr = SsiGetCgiVar (rqptr, "FORM_ERROR_ABOUT2")))
               return ("");
            if (!*sptr) return (cptr);
            sys$fao (&Report2FaoDsc, &Length, &StringDsc, cptr, sptr);
            cptr = VmGetHeap (rqptr, Length);
            strcpy (cptr, String);
            return (cptr);
         }

         if (strsame (cptr, "ERROR_REPORT3", -1))
         {
            if (!(cptr = SsiGetCgiVar (rqptr, "FORM_ERROR_TEXT2")))
               return ("");
            return (cptr);
         }

         if (strsame (cptr, "ERROR_TYPE", -1))
            return (SsiGetCgiVar (rqptr, "FORM_ERROR_TYPE"));

         if (strsame (cptr, "ERROR_STATUS_CODE", -1))
            return (SsiGetCgiVar (rqptr, "FORM_ERROR_STATUS"));

         if (strsame (cptr, "ERROR_STATUS_CLASS", -1))
         {
            if (!(cptr = SsiGetCgiVar (rqptr, "FORM_ERROR_STATUS")))
               return (NULL);
            StatusCode = atoi(cptr);
            sys$fao (&NumberFaoDsc, &Length, &StringDsc, StatusCode / 100);
            cptr = VmGetHeap (rqptr, Length);
            strcpy (cptr, String);
            return (cptr);
         }

         if (strsame (cptr, "ERROR_STATUS_TEXT", -1))
            return (SsiGetCgiVar (rqptr, "FORM_ERROR_STATUS_TEXT"));

         if (strsame (cptr, "ERROR_STATUS_EXPLANATION", -1))
            return (SsiGetCgiVar (rqptr, "FORM_ERROR_STATUS_EXPLANATION"));

         break;

      case 'F' :

         if (strsame (cptr, "FILE_NAME", -1))
            return (SsiGetCgiVar (rqptr, "PATH_TRANSLATED"));

         break;

      case 'H' :

         /* an OSU-compliant variable */
         if (strsame (cptr, "HW_NAME", -1)) return (SysInfo.HwName);

         break;

      case 'L' :

         if (strsame (cptr, "LAST_MODIFIED", -1))
         {
            cptr = VmGetHeap (rqptr, SIZEOF_TIME_STRING);
            SsiTimeString (rqptr,
               &tkptr->RootDocumentTaskPtr->FileContentPtr->RdtBinTime,
               VarParam, cptr, SIZEOF_TIME_STRING);
            return (cptr);
         }

         break;

      case 'G' :

         /* an OSU-compliant variable */
         if (strsame (cptr, "GETENV", -1))
         {
            if (!(sptr = getenv (VarParam))) return (NULL);
            cptr = VmGetHeap (rqptr, Length = strlen(sptr)+1);
            memcpy (cptr, sptr, Length);
            return (cptr);
         }

         break;

      case 'Q' :

         if (strsame (cptr, "QUERY_STRING_UNESCAPED", -1))
            return (SsiGetCgiVar (rqptr, "QUERY_STRING"));

         break;

      case 'P' :

         if (strsame (cptr, "PARENT_FILE_NAME", -1))
         {
            if (tkptr->ParentDocumentTaskPtr)
               return (tkptr->ParentDocumentTaskPtr->FileContentPtr->FileName);
            else
               return ("");
         }

         break;

      case 'S' :

         /* an OSU-compliant variable */
         if (strsame (cptr, "SERVER_VERSION", -1))
            return (SsiGetCgiVar (rqptr, "SERVER_SOFTWARE"));

         break;

      case 'T' :

         if (strsame (cptr, "THE_FILE_NAME", -1))
            return (tkptr->TheFileNameVar);

         if (strsame (cptr, "THIS_FILE_NAME", -1))
            return (tkptr->FileContentPtr->FileName);

         break;

      case 'V' :

         /* an OSU-compliant variable */
         if (strsame (cptr, "VMS_VERSION", -1)) return (SysInfo.Version);

         break;
   }

   return (SsiGetCgiVar (rqptr, cptr));
}

/*****************************************************************************/
/*
Search the CGI-assigned variables. If found return a pointer to a symbol's
value string. If no such symbol found return a NULL.
*/

char* SsiGetCgiVar
(
REQUEST_STRUCT *rqptr,
char *VarName
)
{
   unsigned short  Length;
   char  *cptr, *sptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiGetCgiVar() !&Z", VarName);

   cptr = rqptr->SsiTaskPtr->CgiBufferPtr;
   for (;;)
   {
      if (!(Length = *(short*)cptr)) break;
      for (sptr = cptr+sizeof(short)+DclCgiVariablePrefixLength;
           *sptr && *sptr != '=';
           sptr++);
      *sptr = '\0';
      if (strsame (cptr+sizeof(short)+DclCgiVariablePrefixLength,
          VarName, -1))
      {
         *sptr = '=';
         break;
      }
      *sptr = '=';
      cptr += Length + sizeof(short);
   }

   if (Length)
   {
      /* found */
      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", sptr+1);
      return (sptr+1);
   }

   /* not found */
   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", NULL);
   return (NULL);
}

/*****************************************************************************/
/*
Search the user-assigned variables. If found return a pointer to a symbol's
value string. If no such symbol found return a NULL.
*/

char* SsiGetUserVar
(
REQUEST_STRUCT *rqptr,
char *VarName
)
{
   char  *StringPtr;
   SSI_VAR  *varptr;
   LIST_ENTRY  *leptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiGetUserVar() !&Z", VarName);

   /* look through the user-assigned list of variables */
   for (leptr = rqptr->SsiTaskPtr->SsiVarList.HeadPtr;
        leptr;
        leptr = leptr->NextPtr)
   {
      varptr = (SSI_VAR*)leptr;
      /* return if variable name found in list */
      if (toupper(VarName[0]) == toupper(*varptr->NamePtr) &&
          strsame (VarName, varptr->NamePtr, -1))
      {
         if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
            WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", varptr->ValuePtr);
         return (varptr->ValuePtr);
      }
   }

   /* no such variable found */
   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", NULL);
   return (NULL);
}

/*****************************************************************************/
/*
Used by flow-control statements that do an evaluation to make a decision.
*/

BOOL SsiEvaluate (REQUEST_STRUCT *rqptr)

{
   BOOL  NegateResult,
            Result;
   int  NumberOne,
        NumberTwo;
   char  *dptr;
   char  ValueOne [256],
         ValueTwo [256];
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiEvaluate() !&Z",
                 rqptr->SsiTaskPtr->StatementBeginPtr);

   tkptr = rqptr->SsiTaskPtr;

   Result = false;

   dptr = rqptr->SsiTaskPtr->StatementBeginPtr;
   while (*dptr && !isspace(*dptr)) dptr++;

   while (*dptr)
   {
      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", dptr);

      if (rqptr->SsiTaskPtr->StopProcessing) return (false);

      while (*dptr && isspace(*dptr)) dptr++;
      if (!*dptr) break;

      if (*dptr == '!')
      {
         dptr++;
         NegateResult = true;
      }
      else
         NegateResult = false;

      if (strsame (dptr, "VALUE=", 6) ||
          strsame (dptr, "VAR=", 4) ||
          strsame (dptr, "PAR=", 4))
      {
         dptr += SsiGetTagValue (rqptr, dptr, ValueOne, sizeof(ValueOne));
         if (tkptr->StopProcessing) { Result = false; break; }
         if (isdigit(ValueOne[0]))
            Result = atoi(ValueOne);
         else
            Result = ValueOne[0];
         if (NegateResult) Result = !Result;
         continue;
      }

      if (strsame (dptr, "SRCH=", 5))
      {
         dptr += SsiGetTagValue (rqptr, dptr, ValueTwo, sizeof(ValueTwo));
         if (tkptr->StopProcessing) { Result = false; break; }
         Result = StringMatchGreedy (rqptr, ValueOne, ValueTwo);
      }
      else
      if (strsame (dptr, "EQS=", 4))
      {
         dptr += SsiGetTagValue (rqptr, dptr, ValueTwo, sizeof(ValueTwo));
         if (tkptr->StopProcessing) { Result = false; break; }
         Result = strsame (ValueOne, ValueTwo, -1);
      }
      else
      if (strsame (dptr, "EQ=", 3))
      {
         dptr += SsiGetTagValue (rqptr, dptr, ValueTwo, sizeof(ValueTwo));
         if (tkptr->StopProcessing) { Result = false; break; }
         NumberOne = NumberTwo = 0;
         NumberOne = atoi(ValueOne);
         NumberTwo = atoi(ValueTwo);
         Result = (NumberOne == NumberTwo);
      }
      else
      if (strsame (dptr, "GT=", 3))
      {
         dptr += SsiGetTagValue (rqptr, dptr, ValueTwo, sizeof(ValueTwo));
         if (tkptr->StopProcessing) { Result = false; break; }
         NumberOne = NumberTwo = 0;
         NumberOne = atoi(ValueOne);
         NumberTwo = atoi(ValueTwo);
         Result = (NumberOne > NumberTwo);
      }
      else
      if (strsame (dptr, "LT=", 3))
      {
         dptr += SsiGetTagValue (rqptr, dptr, ValueTwo, sizeof(ValueTwo));
         if (tkptr->StopProcessing) { Result = false; break; }
         NumberOne = NumberTwo = 0;
         NumberOne = atoi(ValueOne);
         NumberTwo = atoi(ValueTwo);
         Result = (NumberOne < NumberTwo);
      }
      else
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_TAG_UNKNOWN), FI_LI);
         Result = false;
         break;
      }               

      if (NegateResult) Result = !Result;
      if (!Result) break;
   }

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&?TRUE\rFALSE\r", Result);

   return (Result);
}

/*****************************************************************************/
/*
Using the locale formatting capabilities of function strftime(), output the 
time represented by the specified VMS quadword, binary time.  If 
'BinaryTimePtr' is NULL then default to the current time.  Returns number of 
characters placed into 'TimeString', or zero if an error.
*/ 

int SsiTimeString
(
REQUEST_STRUCT *rqptr,
unsigned long *BinaryTimePtr,
char *TimeFmtPtr,
char *TimeString,
int SizeOfTimeString
)
{
   static BOOL  InitDone;

   int  Length;
   unsigned long  BinaryTime [2];
   struct tm  UnixTime;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                 "SsiTimeString() !%D \'!AZ\'", BinaryTimePtr, TimeFmtPtr);

   if (!TimeFmtPtr)
      TimeFmtPtr = rqptr->SsiTaskPtr->TimeFmtPtr;
   else
   if (!TimeFmtPtr[0]) 
      TimeFmtPtr = rqptr->SsiTaskPtr->TimeFmtPtr;
   if (!BinaryTimePtr) sys$gettim (BinaryTimePtr = &BinaryTime);
   TimeVmsToUnix (BinaryTimePtr, &UnixTime);

   if (!InitDone)
   {
      setlocale (LC_TIME, "");
      InitDone = true;
   }
   if (!(Length =
         strftime (TimeString, SizeOfTimeString, TimeFmtPtr, &UnixTime)))
   {
      SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_DATE_TIME), FI_LI);
      return (0);
   }
   return (Length);
}

/*****************************************************************************/
/*
Get the value of a tag parameter (e.g. tag_name="value"). Allows variable
substitution into tag values, a la Apache. Tag values can have variable values
substituted into them using a leading "{" and trailing '}' character sequence
with the variable name between. Otherwise reserved characters may be escaped
using a leading backslash. If comma-separated numbers are appended to a
substitution variable these become starting and ending indices, extracting
that range from the variable (a single number sets the count from zero).
Returns the number of characters scanned to get the value; note that this is
not necessarily the same as the number of characters in the variable value!
*/ 

int SsiGetTagValue
(
REQUEST_STRUCT *rqptr,
char *String,
char *Value,
int SizeOfValue
)
{
   BOOL  IsVarEquals,
         Negate,
         VarDidSubstitution,
         VarHadSpace,
         VarQuoted;
   int  ExtractCount,
        StartIndex;
   char  *cptr, *sptr, *vptr, *vzptr, *zptr,
         *ValueEqualsPtr,
         *ValueSpacePtr,
         *VarEqualsPtr;
   char  FormatString [256],
         VarName [256];

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiGetTagValue() !&Z", String);

   if (!*(cptr = String)) return (0);

   VarQuoted = VarDidSubstitution = IsVarEquals = VarHadSpace = false;
   ValueEqualsPtr = VarEqualsPtr = NULL;

   zptr = (sptr = Value) + SizeOfValue;

   if (String[0] == '=')
   {
      /* this is the second bite of a #echo var="NAME=fmt" */
      VarQuoted = true;
   }
   else
   if (strsame (String, "VAR=", 4))
      IsVarEquals = true;

   for (cptr = String; *cptr && *cptr != '=' && *cptr != '\"'; cptr++);
   if (*cptr == '=') cptr++;
   if (*cptr == '\"')
   {
      cptr++;
      VarQuoted = true;
   }

   while (((*cptr && VarQuoted && *cptr != '\"') ||
           (*cptr && !VarQuoted && !isspace(*cptr))) &&
          sptr < zptr)
   {
      if (*cptr != '{')
      {
         /*********************/
         /* literal character */
         /*********************/

         if (*cptr == '=')
         {
            VarEqualsPtr = cptr;
            ValueEqualsPtr = sptr;
         }
         else
         if (!VarEqualsPtr && (*cptr == ' ' || *cptr == '\t'))
            VarHadSpace = true;

         /* escape character? */
         if (*cptr == '\\') cptr++;
         if (*cptr && sptr < zptr) *sptr++ = *cptr++;
         continue;
      }

      /*************************/
      /* variable substitution */
      /*************************/

      VarDidSubstitution = true;
      cptr++;
      vzptr = (vptr = VarName) + sizeof(VarName);
      while (*cptr && (isalnum(*cptr) || *cptr == '_') && vptr < vzptr)
         *vptr++ = *cptr++;
      *vptr = '\0';

      if (*cptr == ',')
      {
         cptr++;
         StartIndex = 0;
         ExtractCount = 999999999;

         if (isdigit(*cptr))
         {
            /* substring */
            StartIndex = atoi(cptr);
            while (isdigit(*cptr)) cptr++;
            if (*cptr == ',') cptr++;
            if (isdigit(*cptr))
            {
               /* two numbers provide a start index and an extract count */
               ExtractCount = atoi(cptr);
               while (isdigit(*cptr)) cptr++;
               if (*cptr == ',') cptr++;
            }
            else
            {
               /* one number extracts from the start of the string */
               ExtractCount = StartIndex;
               StartIndex = 0;
            }
         }
         if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
            WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                       "!UL !UL", StartIndex, ExtractCount);

         if (isalpha(*cptr))
         {
            /* "function" on variable */
            if (strsame (cptr, "length", 6))
            {
               static $DESCRIPTOR (LengthFaoDsc, "!UL\0");
               int  Length;
               char  String [32];
               $DESCRIPTOR (StringDsc, String);

               vptr = SsiGetVar (rqptr, VarName, NULL, false);
               if (rqptr->SsiTaskPtr->StopProcessing)
                  return (cptr - String);

               Length = 0;
               while (StartIndex-- && *vptr) vptr++;
               while (ExtractCount-- && *vptr) { vptr++; Length++; }
               sys$fao (&LengthFaoDsc, 0, &StringDsc, Length);
               vptr = String;
               while (*vptr && sptr < zptr) *sptr++ = *vptr++;
               while (isalpha(*cptr)) cptr++;
            }
            else
            if (strsame (cptr, "exists", 6))
            {
               vptr = SsiGetVar (rqptr, VarName, NULL, true);
               if (vptr)
                  vptr = "true";
               else
                  vptr = "";
               while (*vptr && sptr < zptr) *sptr++ = *vptr++;
               while (isalpha(*cptr)) cptr++;
            }
            else
            {
               SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_VARIABLE), FI_LI);
               return (cptr - String);
            }
         }
         else
         {
            vptr = SsiGetVar (rqptr, VarName, NULL, false);
            if (rqptr->SsiTaskPtr->StopProcessing)
               return (cptr - String);
            while (StartIndex-- && *vptr) vptr++;
            while (ExtractCount-- && *vptr && sptr < zptr)
               *sptr++ = *vptr++;
         }
      }
      else
      {
         /* get all of variable */
         vptr = SsiGetVar (rqptr, VarName, NULL, false);
         if (rqptr->SsiTaskPtr->StopProcessing)
            return (cptr - String);
         while (*vptr && sptr < zptr) *sptr++ = *vptr++;
      }

      if (*cptr != '}')
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_SSI_VARIABLE), FI_LI);
         return (cptr - String);
      }
      cptr++;
   }

   if (sptr >= zptr)
   {
      SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_GENERAL_OVERFLOW), FI_LI);
      return (cptr - String);
   }
   *sptr = '\0';
   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", Value);
   if (*cptr == '\"') cptr++;

   if (rqptr->SsiTaskPtr->ComplianceLevel >= SSI_VAR_FMT_COMPLIANCE_LEVEL &&
       IsVarEquals &&
       VarQuoted &&
       !(VarDidSubstitution || VarHadSpace))
   {
      if (ValueEqualsPtr)
      {
         /* terminate variable name at the equate symbol */
         *(sptr = ValueEqualsPtr) = '\0';

         /* get the format string from immediately following the equate */
         cptr = VarEqualsPtr + 1;
         zptr = (sptr = FormatString) + sizeof(FormatString);
         while (*cptr && *cptr != '\"' && sptr < zptr) *sptr++ = *cptr++;
         if (sptr >= zptr)
         {
            SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_GENERAL_OVERFLOW), FI_LI);
            return (cptr - String);
         }
         *sptr = '\0';

         vptr = SsiGetVar (rqptr, Value, FormatString, false);
      }
      else
         vptr = SsiGetVar (rqptr, Value, NULL, false);

      if (rqptr->SsiTaskPtr->StopProcessing)
         return (cptr - String);
      zptr = (sptr = Value) + SizeOfValue;
      while (*vptr && sptr < zptr) *sptr++ = *vptr++;
      if (sptr >= zptr)
      {
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_GENERAL_OVERFLOW), FI_LI);
         return (cptr - String);
      }
      *sptr = '\0';
   }

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", Value);

   return (cptr - String);
}

/*****************************************************************************/
/*
Get a 'FILE="file_name"' or a 'VIRTUAL="file_name"'.  Maximum number of 
characters allowed in value is 256.   Returns the number of characters scanned 
to get the value.  If the file name does not contain a device/directory (i.e.
is specified as if in the current directory) then the device/directory or the
current document is prepended to the file name.
*/ 

int SsiGetFileSpec
(
REQUEST_STRUCT *rqptr,
char *String,
char *FileName,
int SizeOfFileName
)
{
   int  len;
   char  *cptr, *sptr, *zptr;
   char  Scratch [256],
         VirtualFileName [256];
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiGetFileSpec() !&Z", String);

   tkptr = rqptr->SsiTaskPtr;

   len = SsiGetTagValue (rqptr, String, Scratch, sizeof(Scratch));
   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!UL !&Z", len, Scratch);

   if (toupper(String[0]) == 'V')
   {
      MapUrl_VirtualPath (rqptr->rqHeader.PathInfoPtr, Scratch,
                          VirtualFileName, sizeof(VirtualFileName));
      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", VirtualFileName);

      FileName[0] = '\0';
      cptr = MapUrl_Map (VirtualFileName, 0, FileName, SizeOfFileName,
                         NULL, 0, NULL, 0, NULL, 0, NULL, rqptr);
      if (!cptr[0])
      {
         FileName[0] = '\0';
         SsiProblem (rqptr, "!AZ", cptr+1, FI_LI);
         return (len);
      }
      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!UL !&Z", len, FileName);
      return (len);
   }
   else
   {
      zptr = (sptr = FileName) + SizeOfFileName;
      for (cptr = Scratch;
           *cptr && *cptr != ':' && *cptr != '[' && sptr < zptr;
           *sptr++ = *cptr++);
      if (*cptr)
      {
         /* looks like a full specification, just continue on */
         while (*cptr && sptr < zptr) *sptr++ = *cptr++;
         if (sptr >= zptr)
         {
            FileName[0] = '\0';
            SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_GENERAL_OVERFLOW), FI_LI);
            return (len);
         }
         *sptr = '\0';
         return (len);
      }

      zptr = (sptr = FileName) + SizeOfFileName;
      if (isupper(Scratch[0]))
      {
         for (cptr = tkptr->FileContentPtr->FileName;
              *cptr && *cptr != ']' && *(unsigned short*)cptr != '][' &&
                 sptr < zptr;
              *sptr++ = toupper(*cptr++));
         if (*cptr == ']' && sptr < zptr) *sptr++ = *cptr++;
         for (cptr = Scratch; *cptr && sptr < zptr; *sptr++ = toupper(*cptr++));
      }
      else
      {
         for (cptr = tkptr->FileContentPtr->FileName;
              *cptr && *cptr != ']' && *(unsigned short*)cptr != '][' &&
                 sptr < zptr;
              *sptr++ = tolower(*cptr++));
         if (*cptr == ']' && sptr < zptr) *sptr++ = *cptr++;
         for (cptr = Scratch; *cptr && sptr < zptr; *sptr++ = tolower(*cptr++));
      }
      if (sptr >= zptr)
      {
         FileName[0] = '\0';
         SsiProblem (rqptr, "!AZ", MsgFor(rqptr,MSG_GENERAL_OVERFLOW), FI_LI);
         return (len);
      }
      *sptr = '\0';

      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", FileName);
      return (len);
   }
}

/*****************************************************************************/
/*
Retrieve and display the specified file's specified attribute (size,
modification time, etc.)

This function uses the ACP-QIO interface detailed in the "OpenVMS I/O User's 
Reference Manual".
*/ 

SsiFileDetails
(
REQUEST_STRUCT *rqptr,
int FileDetailsItem
)
{
   int  status,
        FileNameLength;
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                 "SsiFileDetails() !AZ !UL \'!AZ\'",
                 rqptr->SsiTaskPtr->ScratchFileName, FileDetailsItem,
                 rqptr->SsiTaskPtr->FormatString);

   tkptr = rqptr->SsiTaskPtr;

   /* initialize file variable data */
   tkptr->TheFileNameVar[0] = '\0';

   tkptr->FileDetailsItem = FileDetailsItem;
   FileNameLength = strlen(tkptr->ScratchFileName);

   /* check if the user has access to this file! */
   if (rqptr->rqAuth.VmsUserProfileLength)
   {
      status = AuthVmsCheckUserAccess (rqptr, tkptr->ScratchFileName,
                                       FileNameLength);
      if (VMSnok (status))
      {
         SsiProblem (rqptr, "!&m", status, FI_LI);
         SsiEnd (rqptr);
         return;
      }
   }

   OdsParse (&tkptr->DetailsOds,
             tkptr->ScratchFileName, FileNameLength, NULL, 0,
             0, &SsiFileDetailsParseAst, rqptr);
}

/*****************************************************************************/
/*
AST called from SsiFileDetails() when asynchronous parse completes.  If status
OK set up and queue an ACP QIO to get file size and revision date/time, ASTing
to SsiFileDetailsAcpInfoAst().
*/

SsiFileDetailsParseAst (struct FAB *FabPtr)

{
   static $DESCRIPTOR (DeviceDsc, "");

   int  status;
   REQUEST_STRUCT  *rqptr;
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   rqptr = FabPtr->fab$l_ctx;

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                 "SsiFileDetailsParseAst() !&F sts:!&S stv:!&S",
                 &SsiFileDetailsParseAst, FabPtr->fab$l_sts, FabPtr->fab$l_stv);

   tkptr = rqptr->SsiTaskPtr;

   if (VMSnok (status = tkptr->DetailsOds.Fab.fab$l_sts))
   {
      SsiProblem (rqptr, "!&m", status, FI_LI);
      SsiEnd (rqptr);
      return;
   }

   /* get the variable file name */
   strcpy (tkptr->TheFileNameVar, tkptr->DetailsOds.ExpFileName);
   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&Z", tkptr->TheFileNameVar);

   if (tkptr->DetailsOds.Nam_fnb & NAM$M_SEARCH_LIST &&
       !tkptr->SearchListCount++)
   {
      /*******************************/
      /* search to get actual device */
      /*******************************/

      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SEARCH-LIST");

      if (rqptr->rqAuth.VmsUserProfileLength) sys$setprv (1, &SysPrvMask, 0, 0);
      OdsSearch (&tkptr->DetailsOds, &SsiFileDetailsParseAst, rqptr);
      if (rqptr->rqAuth.VmsUserProfileLength) sys$setprv (0, &SysPrvMask, 0, 0);
      return;
   }

   /************/
   /* ACP info */
   /************/

   if (rqptr->rqAuth.VmsUserProfileLength) sys$setprv (1, &SysPrvMask, 0, 0);
   OdsFileAcpInfo (&tkptr->DetailsOds, &SsiFileDetailsAcpInfoAst, rqptr); 
   if (rqptr->rqAuth.VmsUserProfileLength) sys$setprv (0, &SysPrvMask, 0, 0);
}

/****************************************************************************/
/*
AST called from SsiFileDetailsParseAST() when ACP QIO completes.  If status
indicates no such file then call any file open error processing function
originally supplied, otherwise report the error.  If status OK provide the
request file details.

This function uses the ACP-QIO interface detailed in the "OpenVMS I/O User's 
Reference Manual", and is probably as fast as we can get for this type of file
system functionality!
*/

SsiFileDetailsAcpInfoAst (REQUEST_STRUCT *rqptr)

{
   static $DESCRIPTOR (AbbrevOneByteFaoDsc, "!UL byte");
   static $DESCRIPTOR (AbbrevBytesFaoDsc, "!UL bytes");
   static $DESCRIPTOR (AbbrevOnekByteFaoDsc, "!UL kbyte");
   static $DESCRIPTOR (AbbrevkBytesFaoDsc, "!UL kbytes");
   static $DESCRIPTOR (AbbrevOneMByteFaoDsc, "!UL Mbyte");
   static $DESCRIPTOR (AbbrevMBytesFaoDsc, "!UL Mbytes");
   static $DESCRIPTOR (OneBlockFaoDsc, "!UL block");
   static $DESCRIPTOR (BlocksFaoDsc, "!UL blocks");
   static $DESCRIPTOR (BytesFaoDsc, "!AZ bytes");
   static $DESCRIPTOR (NumberFaoDsc, "!UL");

   int  status,
        NumBytes,
        SizeInBytes;
   unsigned short  Length;
   unsigned long  EndOfFileVbn;
   unsigned long  ScratchBinTime [2];
   char  *cptr, *sptr, *zptr,
         *FormatPtr;
   char  Scratch [256],
         String [256];
   SSI_TASK  *tkptr;
   $DESCRIPTOR (StringDsc, String);
   $DESCRIPTOR (ScratchDsc, Scratch);

   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI,
                 "SsiFileDetailsAcpInfoAst() !&F !&S",
                 &SsiFileDetailsAcpInfoAst,
                 rqptr->SsiTaskPtr->DetailsOds.FileQio.IOsb.Status);

   tkptr = rqptr->SsiTaskPtr;

   /* first, deassign the channel allocated by OdsFileAcpInfo() */
   sys$dassgn (tkptr->DetailsOds.FileQio.AcpChannel);

   if ((status = tkptr->DetailsOds.FileQio.IOsb.Status) == SS$_NOSUCHFILE)
      status = RMS$_FNF;
   if (VMSnok (status)) 
   {
      if (status == RMS$_FNF && tkptr->FormatString[0] == '?')
      {
         /* ignore file not found, just continue */
         tkptr->TheFileNameVar[0] = '\0';
         SysDclAst (&SsiParse, rqptr);
         return;
      }
      else
      {
         SsiProblem (rqptr, "!&m", status, FI_LI);
         SsiEnd (rqptr);
         return;
      }
   }

   /*******************/
   /* process details */
   /*******************/

   if (tkptr->FileDetailsItem == FILE_LAST_MODIFIED)
   {
      /*****************/
      /* last-modified */
      /*****************/

      if (!tkptr->LastModifiedBinTime[0] &&
          !tkptr->LastModifiedBinTime[1])
      {
         /* first file */
         tkptr->LastModifiedBinTime[0] =
            tkptr->DetailsOds.FileQio.RdtBinTime[0];
         tkptr->LastModifiedBinTime[1] =
            tkptr->DetailsOds.FileQio.RdtBinTime[1];
         SysDclAst (&SsiParse, rqptr);
         return;
      }

      if (tkptr->LastModifiedBinTime[0] ==
          tkptr->DetailsOds.FileQio.RdtBinTime[0] &&
          tkptr->LastModifiedBinTime[1] ==
          tkptr->DetailsOds.FileQio.RdtBinTime[1])
      {
         /* times are identical */
         SysDclAst (&SsiParse, rqptr);
         return;
      }

      /* if a positive time results the file has been modified */
      if (VMSok (status =
          lib$sub_times (&tkptr->LastModifiedBinTime,
                         &tkptr->DetailsOds.FileQio.RdtBinTime,
                         &ScratchBinTime)))
      {
         /* positive time, current content is later than this file' RDT */
         SysDclAst (&SsiParse, rqptr);
         return;
      }

      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "!&S", status);
      if (status == LIB$_NEGTIM)
      {
         /* current contents are earlier than this file's RDT */
         tkptr->LastModifiedBinTime[0] =
            tkptr->DetailsOds.FileQio.RdtBinTime[0];
         tkptr->LastModifiedBinTime[1] =
            tkptr->DetailsOds.FileQio.RdtBinTime[1];
         SysDclAst (&SsiParse, rqptr);
         return;
      }
      else
      {
         SsiProblem (rqptr, "!&m", status, FI_LI);
         SsiEnd (rqptr);
         return;
      }
   }

   if (tkptr->FormatString[0] == '?')
   {
      /* no output, just checking existance of file */
      SysDclAst (&SsiParse, rqptr);
      return;
   }

   if (tkptr->FileDetailsItem == FILE_FCREATED)
   {
      /*********************/
      /* date/time created */
      /*********************/

      /* output creation timestamp */
      if (!SsiTimeString (rqptr, &tkptr->DetailsOds.FileQio.CdtBinTime,
                          tkptr->FormatString, String, sizeof(String)))
      {
         SsiEnd (rqptr);
         return;
      }
      Length = strlen(String);
      if (tkptr->FlowControlIsExecuting[tkptr->FlowControlIndex])
      {
         NetWriteBuffered (rqptr, &SsiParse, String, Length);
         tkptr->TraceOutput = true;
      }
      else
         SysDclAst (rqptr, &SsiParse);
      return;
   }

   if (tkptr->FileDetailsItem == FILE_FLASTMOD)
   {
      /*********************/
      /* date/time revised */
      /*********************/

      /* output creation timestamp */
      if (!SsiTimeString (rqptr, &tkptr->DetailsOds.FileQio.RdtBinTime,
                          tkptr->FormatString, String, sizeof(String)))
      {
         SsiEnd (rqptr);
         return;
      }
      Length = strlen(String);
      if (tkptr->FlowControlIsExecuting[tkptr->FlowControlIndex])
      {
         NetWriteBuffered (rqptr, &SsiParse, String, Length);
         tkptr->TraceOutput = true;
      }
      else
         SysDclAst (rqptr, &SsiParse);
      return;
   }

   if (tkptr->FileDetailsItem == FILE_FSIZE)
   {
      /*************/
      /* file size */
      /*************/

      if (tkptr->FormatString[0])
         FormatPtr = tkptr->FormatString;
      else
         FormatPtr = rqptr->SsiTaskPtr->SizeFmtPtr;

      EndOfFileVbn =
         ((tkptr->DetailsOds.FileQio.RecAttr.fat$l_efblk & 0xffff) << 16) |
         ((tkptr->DetailsOds.FileQio.RecAttr.fat$l_efblk & 0xffff0000) >> 16);

      if (EndOfFileVbn <= 1)
         SizeInBytes = tkptr->DetailsOds.FileQio.RecAttr.fat$w_ffbyte;
      else
         SizeInBytes = ((EndOfFileVbn-1) << 9) +
                       tkptr->DetailsOds.FileQio.RecAttr.fat$w_ffbyte;

      if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
         WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "vbn:!UL ffb:!UL bytes:!UL",
                    EndOfFileVbn,
                    tkptr->DetailsOds.FileQio.RecAttr.fat$w_ffbyte,
                    SizeInBytes); 

      if (toupper(FormatPtr[0]) == 'A')  /* "abbrev" */
      {
         if (SizeInBytes < 1024)
         {
            if (SizeInBytes == 1)
               sys$fao (&AbbrevOneByteFaoDsc, &Length, &StringDsc, SizeInBytes);
            else
               sys$fao (&AbbrevBytesFaoDsc, &Length, &StringDsc, SizeInBytes);
         }
         else
         if (SizeInBytes < 1048576)
         {
            if ((NumBytes = SizeInBytes / 1024) == 1)
               sys$fao (&AbbrevOnekByteFaoDsc, &Length, &StringDsc, NumBytes);
            else
               sys$fao (&AbbrevkBytesFaoDsc, &Length, &StringDsc, NumBytes);
         }
         else
         {
            if ((NumBytes = SizeInBytes / 1048576) == 1)
               sys$fao (&AbbrevOneMByteFaoDsc, &Length, &StringDsc, NumBytes);
            else
               sys$fao (&AbbrevMBytesFaoDsc, &Length, &StringDsc, NumBytes);
         }
         String[Length] = '\0';
      }
      else
      if (toupper(FormatPtr[0]) == 'B' &&
          toupper(FormatPtr[1]) == 'Y')  /* "bytes" */
      {
         sys$fao (&NumberFaoDsc, &Length, &ScratchDsc, SizeInBytes);
         Scratch[Length] = '\0';
         sptr = String;
         cptr = Scratch;
         while (Length--)
         {
            *sptr++ = *cptr++;
            if (Length && !(Length % 3)) *sptr++ = ',';
         }
         for (cptr = " bytes"; *cptr; *sptr++ = *cptr++);
         *sptr = '\0';
         Length = sptr - String;
      }
      else
      if (toupper(FormatPtr[0]) == 'B' &&
          toupper(FormatPtr[1]) == 'L')  /* "blocks" */
      {
         if (EndOfFileVbn == 1)
            sys$fao (&OneBlockFaoDsc, &Length, &StringDsc, EndOfFileVbn);
         else
            sys$fao (&BlocksFaoDsc, &Length, &StringDsc, EndOfFileVbn);
         String[Length] = '\0';
      }
      if (tkptr->FlowControlIsExecuting[tkptr->FlowControlIndex])
      {
         NetWriteBuffered (rqptr, &SsiParse, String, Length);
         tkptr->TraceOutput = true;
      }
      else
         SysDclAst (rqptr, &SsiParse);
      return;
   }

   SsiProblem (rqptr, "!AZ", ErrorSanityCheck, FI_LI);
   SsiEnd (rqptr);
   return;
} 

/*****************************************************************************/
/*
Generate a general error, with explanation about the pre-processor error.
*/ 

SsiProblem
(
REQUEST_STRUCT *rqptr,
...
)
{
   static char  ErrorMessageFao [] =
"\n\
<H2><FONT COLOR=\"#ff0000\"><U>!AZ</U></FONT></H2>\n\
!&@<!!-- module: !AZ line: !UL -->&nbsp;(!AZ !UL)!&@<!!-- !AZ -->\n";

   int  status,
        argcnt;
   unsigned long  FaoVector [32];
   unsigned long  *vecptr;
   va_list  argptr;
   SSI_TASK  *tkptr;

   /*********/
   /* begin */
   /*********/

   va_count (argcnt);

   if (WATCHING(rqptr) && (WATCH_MODULE(WATCH_MOD_SSI)))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SSI, "SsiProblem() !UL", argcnt);

   if (argcnt > 32+1)
   {
      ErrorNoticed (SS$_OVRMAXARG, "SsiProblem()", FI_LI);
      return (SS$_OVRMAXARG);
   }

   tkptr = rqptr->SsiTaskPtr;

   tkptr->StopProcessing = true;

   vecptr = FaoVector;
   if (tkptr->ErrMsgPtr && tkptr->ErrMsgPtr[0])
      *vecptr++ = tkptr->ErrMsgPtr;
   else
      *vecptr++ = MsgFor(rqptr,MSG_SSI_ERROR);
   va_start (argptr, rqptr);
   for (argcnt -= 1; argcnt; argcnt--)
      *vecptr++ = va_arg (argptr, unsigned long);
   va_end (argptr);
   *vecptr++ = MsgFor(rqptr,MSG_SSI_LINE);
   *vecptr++ = tkptr->StatementLineNumber;
   if (tkptr->StatementBeginPtr &&
       tkptr->StatementBeginPtr[0])
   {
      *vecptr++ = " &nbsp;...&nbsp; <NOBR>\\<TT>!&;AZ</TT>\\</NOBR>";
      *vecptr++ = tkptr->StatementBeginPtr;
   }
   else
      *vecptr++ = "";
   *vecptr++ = tkptr->FileContentPtr->FileName;

   status = NetWriteFaol (rqptr, ErrorMessageFao, &FaoVector);
   if (VMSnok (status)) ErrorNoticed (status, "NetWriteFaol()", FI_LI);
}

/*****************************************************************************/

