/*****************************************************************************/
/*
                                 FORMwork.c

A scripting utility that validates a POSTed form's fields against requirements
stored in the form and associated with each field.  Any anomalies are reported
to the client.  When validated correctly the form contents can have any or all
of the following processing performed: 

  o  raw request body be placed in an individual time-stamped file 
  o  processed form be placed in an individual time-stamped file 
  o  form field values appended as CSV (comma-separated variable) file record
  o  processed form emailed to a specified address
  o  CLI global symbols created containing form field values

Some FORMwork functionality parallels that of the tMAILer utility.
Generally it provides more form-related processing.
In particular the CSV functionality allows results easily to be imported into
other data processing utilities.

The FORMwork directives are embedded in the HTML <FORM> inside each <INPUT>,
<TEXTAREA> or <SELECT> tag as an associated FORMWORK=".." elements.  This is
ignore by the client agent (browser) and eliminates the need for any sort of
'template' file and keeps the validation data tightly associated with the form
field data.  The HTML file is read directly by FORMwork and these FORMWORK=".."
elements used to validate the submitted form field values.

The FORMWORK=".." directives are as follows:

  AS        as-is (by default CR/LFs are replaced by a space)
  CG        CGI environment variable value
  DE{sss}   descriptive string (for reporting errors)
  EM        email field ('name@host')
  IG        ignore this field (not stored in CSV, etc., appears in CLI symbols)
  LI{sss}   literal string (prefixes any value string)
  MA        mandatory field
  NU        numeric (digit only) field
  OP        optional field (complementary to mandatory)
  SInnn     maximum size (number of characters)
  WA        formwork "watch" mode

FORMwork directives are basically two character upper or lower case keywords,
some with curly-brace delimited strings as parameters, others with trailing
digits.  Any non-alphanumeric characters can be used inbetween directives to
make them more readable.  Each <INPUT>/<TEXTAREA> tag must have a HTML
NAME=".." element identifying it and a FORMWORK=".." element associated with
it.  Where particular form components such as TYPE=RADIO can have multiple
<INPUT>s with the same NAME=".." at least one must have a FORMWORK=".."
associated with it.

Example HTML containing <FORM> tag:

  <form method="POST" enctype="form/url-encoded" action="/cgi-bin/test.com">

  <input type="text" size="60" name="field1"
  formwork="MASI60DE{some example text}">

  <input type="radio" name="field2" value="one">
  <input type="radio" name="field2" value="two">
  <input type="radio" name="field2" value="three"
  formwork="MA DE{some example text}">

  <input type="checkbox" name="field3" value="four"
  formwork="OP">
  <input type="checkbox" name="field4" value="five"
  formwork="OP">
  <input type="checkbox" name="field5" value="six"
  formwork="OP">

  <input type="text" name="field6" formwork="MA NU SI6 DE{Field Six}">

  <select name="field7" formwork="ma de{From this selection}">
  <option value="seven">7
  <option value="eight">8
  <option value="nine">9
  </select>

  <input type="submit">

  </form>

When successfully submitted a small default 'success' response is provided by
default.  To redirect to some other response use the /LOCATION=url qualifier. 
To suppress all FORMwork responses and have the wrapper DCL procedure generate
a response use the /NORESPONSE qualifier.  FORMwork exits with a NORMAL status
when successfully submitted or an inhibited ERROR status when not.

The /SYMBOLS qualifier can be used to generate global symbols from the form
field names and values.  Values that cannot be accomodated by the current VMS
version are truncated without warning (basically 255 earlier than V7.0, 1023
for V7.0 through to V7.3-1, or 8191 for V7.3-2 and later).  The raw and
processed file names are also provided as CLI symbols.  Wrapper procedures can
use these symbols during post-processing.

A <INPUT> type="submit" (submit button) can optionally have a name="..",
value=".." and associated formwork=".." element.  This allows the wrapper
script to detect one of possibly multiple submit buttons.


WATCH MODE
----------
A developer 'debug' mode is available allowing the various stages of
processing, and some configuration error messages where necessary, to be viewed
as plain text.  This is enabled using the 'WA' directive, the /WATCH qualifier,
and the FORMWORK$WATCH logical name.


QUALIFIERS
----------
/CAPACITY=integer     set maximum capacity of field value in Kbytes (default 4)
/CHARSET=string       explicitly specify a "text/" response character set
                      (to suppress just supply an empty string)
/CSV=filename         output CSV file name record to be appended to
/DBUG                 turns on all "if (Debug)" statements
/DOUBLESPACE          double-space the lines of human-readable response
/EMAIL=string         email address for mailing human-readable response
/HTML=filename        input file containing the <form>
/LOCATION=url         successful submission redirection URL
/NORESPONSE           suppress success response, let wrapper procedure do it
/PERSONAL_NAME=string mail message VMS personal name
/PROCESSED=filename   output file name containing human-readable response
/RAW=filename         output file name for copy of URL-form-encoded request
/SEPARATOR=char       specifies CSV separator character (default is TAB)
/SOFTWAREID           display the FORMWORK version
/SUBJECT=string       mail message subject line
/SYMBOLS[=string]     create CLI symbols from form data
/VERSION              display the FORMWORK version
/WATCH                enable "watch" mode (same as 'WA' and FORMWORK$WATCH)


LOGICAL NAMES
-------------
FORMWORK$DBUG       turns on all "if (Debug)" statements
FORMWORK$PARAM      equivalent to (overrides) the command line
                    parameters/qualifiers (define as a system-wide logical)
FORMWORK$WATCH      if this logical value is the same as the /HTML= string
                    then the equivalent of the 'WA' directive is enabled


BUILD DETAILS
-------------
See BUILD_FORMWORK.COM procedure.


COPYRIGHT
---------
Copyright (C) 2004-2022 Mark G.Daniel

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.


VERSION HISTORY (update SOFTWAREVN as well!)
---------------
12-FEB-2006  MGD  v1.1.0, /capacity
04-AUG-2004  MGD  v1.0.0, initial development
*/
/*****************************************************************************/

#define SOFTWAREVN "1.1.0"
#define SOFTWARENM "FORMWORK"
#ifdef __ALPHA
#  define SOFTWAREID SOFTWARENM " AXP-" SOFTWAREVN
#endif
#ifdef __ia64
#  define SOFTWAREID SOFTWARENM " IA64-" SOFTWAREVN
#endif
#ifdef __VAX
#  define SOFTWAREID SOFTWARENM " VAX-" SOFTWAREVN
#endif
#ifdef __x86_64
#  define SOFTWAREID SOFTWARENM " X86-" SOFTWAREVN
#endif

/* standard C header files */
#include <ctype.h>
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unixio.h>

/* VMS-related header files */
#include <descrip.h>
#include <jpidef.h>
#include <lib$routines.h>
#include <libclidef.h>
#include <maildef.h>
#include <ssdef.h>
#include <stsdef.h>
#include <syidef.h>
#include <mail$routines.h>
#include <starlet.h>

#define boolean int
#define BOOL int
#define true 1
#define false 0

/* application related header file */
#include "cgilib.h"

char  CopyrightInfo [] =
"Copyright (C) 2004-2006 Mark G.Daniel.\n\
This software comes with ABSOLUTELY NO WARRANTY.\n\
This is free software, and you are welcome to redistribute it\n\
under the conditions of the GNU GENERAL PUBLIC LICENSE, version 2.";

#ifndef __VAX
#   pragma nomember_alignment
#endif

#define VMSok(x) ((x) & STS$M_SUCCESS)
#define VMSnok(x) (!((x) & STS$M_SUCCESS))
 
#define FI_LI "FORMWORK", __LINE__

#define QUIET_ERROR_EXIT STS$M_INHIB_MSG | STS$K_ERROR

#define FORM_URLENCODED "application/x-www-form-urlencoded"

#define WATCH_DIVIDER "+++++++++++++++"

char  *MonthName [] = { "", "Jan","Feb","Mar","Apr","May","Jun",
                            "Jul","Aug","Sep","Oct","Nov","Dec" };

char  Utility [] = "FORMWORK";

#define MAX_FIELDS 255

typedef struct FieldStruct FIELD_STRUCT;

struct FieldStruct {
   BOOL  AsIsCarCon,
         EmailField,
         IgnoreField,
         MandatoryField,
         NumericField;
   int  MaxSize;
   char  *DescPtr,
         *FormWorkPtr,
         *NamePtr,
         *ValuePtr;
};

FIELD_STRUCT  FieldData [MAX_FIELDS];

BOOL  Debug,
      CliDoubleSpace,
      CliNoResponse,
      FormWorkWatch;

int  BodyBufferCount,
     FieldCount,
     FieldValueCapacity,
     FormLength,
     PrintfReportLength,
     PrintfStringLength,
     ScriptPid;

unsigned long  BinTime [2];

unsigned short  NumTime [7];

char  *BodyBufferPtr,
      *CharsetPtr,
      *CgiContentTypePtr,
      *CgiEnvironmentPtr,
      *CgiRequestMethodPtr,
      *CliCharsetPtr,
      *CliCsvFileNamePtr,
      *CliEmailAddressPtr,
      *CliHtmlFileNamePtr,
      *CliLocationPtr,
      *CliPersonalNamePtr,
      *CliProcessedFileNamePtr,
      *CliRawFileNamePtr,
      *CliSeparatorPtr = "\t",
      *CliSubjectPtr,
      *CliSymbolPtr = "FORMWORK_",
      *FormPtr,
      *PrintfReportPtr,
      *PrintfStringPtr;
      
char  CharsetString [64],
      ContentTypeCharset [64],
      SoftwareID [48],
      SourceHost [256];

char  ErrorBufferOverflow [] = "Buffer overflow.",
      ErrorConfig [] =
"FormWork configuration error.&nbsp; Contact the site administrator.",
      ErrorHttpMethod [] = "HTTP method must be \'GET\' or \'POST\'!",
      ErrorReadingFile [] = "reading HTML file.",
      ErrorMemory [] = "allocating memory.",
      ErrorNotUrlEncoded [] = "Must be URL-encoded form data!",
      ErrorWritingFile [] = "writing data file.",
      SuccessMessage [] = "Successfully submitted.&nbsp; Thankyou.";

char  ReportMandatory [] = "\'%s\' must be supplied.",
      ReportNotEmail [] = "\'%s\' must be in the format \'user@host\'.",
      ReportNotNumeric [] = "\'%s\' must contain only a number.",
      ReportTooLarge [] = "\'%s\' must contain a maximum of %d characters.";

char  ReportToClient [] =
"The following information must be corrected before proceding.\n\
<p><ol>\n%s</ol>\n\
<p>Go back to the form, make the changes, then submit again.&nbsp; Thankyou.\n";

/* required function prototypes */

void PrintfReport (char*, ...);
void PrintfString (char*, ...);

/*****************************************************************************/
/*
'argc/argv' are only required to support OSU, in particular CgiLibOsuInit().
*/

main
(
int argc,
char *argv[]
)
{
   /*********/
   /* begin */
   /*********/

   sprintf (SoftwareID, "%s (%s)", SOFTWAREID, CgiLibEnvironmentVersion());

   if (getenv ("FORMWORK$DBUG"))
   {
      Debug = true;
      CgiLibResponseHeader (200, "text/plain");
      CgiLibEnvironmentSetDebug (Debug);
   }

   GetParameters ();

   /* starts off as integer kBytes and ends up as integer bytes */
   if (FieldValueCapacity < 1 || FieldValueCapacity > 1024)
      FieldValueCapacity = 4;
   FieldValueCapacity = 1024 * FieldValueCapacity;

   CgiLibEnvironmentInit (argc, argv, false);

   CgiLibResponseSetCharset (CliCharsetPtr);
   CgiLibResponseSetSoftwareID (SoftwareID);
   CgiLibResponseSetErrorMessage ("Reported by FormWork");

   CgiEnvironmentPtr = CgiLibEnvironmentName ();

   ProcessRequest ();

   exit (SS$_NORMAL);
}

/*****************************************************************************/
/*
Script workhorse.
*/

ProcessRequest ()

{
   static struct
   {
      unsigned short  buf_len;
      unsigned short  item;
      void  *buf_addr;
      void  *ret_len;
   } JpiItems [] =
   {
     { sizeof(ScriptPid), JPI$_PID, &ScriptPid, 0 },
     { 0,0,0,0 }
   };

   int  cnt, status;
   char  *cptr, *sptr;

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

   if (Debug) fprintf (stdout, "ProcessRequest()\n");

   /* for time-stamping file names, etc. */
   sys$gettim (&BinTime);
   sys$numtim (&NumTime, &BinTime);

   /* get the script PID for unique-ifying any output file names */
   status = sys$getjpiw (0, 0, 0, &JpiItems, 0, 0, 0);
   if (VMSnok (status)) exit (status);

   if (cptr = getenv ("FORMWORK$WATCH"))
      if (strsame (cptr, CliHtmlFileNamePtr, -1))
         FormWorkWatch = true;

   /* put together a string trying to identify the source host */
   cptr = CgiLibVar ("REMOTE_ADDR");
   sptr = CgiLibVar ("REMOTE_HOST");
   if (strsame (cptr, sptr, -1))
      cnt = sprintf (SourceHost, "%s", cptr);
   else
      cnt = sprintf (SourceHost, "%s (%s)", sptr, cptr);
   cptr = CgiLibVarNull ("HTTP_X_FORWARDED_FOR");
   if (cptr) cnt += sprintf (SourceHost+cnt, " (%s)", cptr);
   cptr = CgiLibVarNull ("HTTP_FORWARDED");
   if (cptr) cnt += sprintf (SourceHost+cnt, " (%s)", cptr);
   if (cnt > sizeof(SourceHost)) exit (SS$_BUGCHECK);

   status = ReadFileIntoMemory (CliHtmlFileNamePtr, &FormPtr, &FormLength);
   if (VMSnok (status))
   {
      CgiLibResponseError (FI_LI, status, ErrorReadingFile);
      exit (QUIET_ERROR_EXIT);
   }

   if (FormWorkWatch)
   {
      CgiLibResponseHeader (200, "text/plain");
      fprintf (stdout, "%sHTML\n%s%sINPUTS\n",
               WATCH_DIVIDER, FormPtr, WATCH_DIVIDER);
   }

   ProcessHtmlFile ();

   CgiLibReadRequestBody (&BodyBufferPtr, &BodyBufferCount);
   if (!BodyBufferPtr) BodyBufferPtr = "";
   if (Debug) fprintf (stdout, "%d |%s|\n", BodyBufferCount, BodyBufferPtr);

   if (FormWorkWatch)
      fprintf (stdout, "%sBODY\n%s\n%sFIELDS\n",
               WATCH_DIVIDER, BodyBufferPtr, WATCH_DIVIDER);

   ProcessRequestBody ();

   if (PrintfReportPtr)
   {
      if (FormWorkWatch) fprintf (stdout, "%sRESPONSE\n", WATCH_DIVIDER);

      CgiLibResponseHeader (200, "text/html");
      fprintf (stdout, "<html>\n<head></head>\n<body>\n");
      fprintf (stdout, ReportToClient, PrintfReportPtr);
      fprintf (stdout, "</body>\n</head>\n");
      exit (QUIET_ERROR_EXIT);
   }
   else
   {
      if (CliCsvFileNamePtr ||
          CliEmailAddressPtr ||
          CliProcessedFileNamePtr ||
          CliRawFileNamePtr)
      {
         if (FormWorkWatch) fprintf (stdout, "%sACTION\n", WATCH_DIVIDER);

         FormatResponse ();
         if (CliRawFileNamePtr)
         {
            status = WriteRawFile ();
            if (VMSnok (status))
            {
               CgiLibResponseError (FI_LI, status, ErrorWritingFile);
               exit (QUIET_ERROR_EXIT);
            }
         }
         if (CliProcessedFileNamePtr)
         {
            status = WriteProcessedFile ();
            if (VMSnok (status))
            {
               CgiLibResponseError (FI_LI, status, ErrorWritingFile);
               exit (QUIET_ERROR_EXIT);
            }
         }
         if (CliEmailAddressPtr)
         {
            char  MailSubject [256];
            if (!CliSubjectPtr)
            {
               sprintf (MailSubject, "FormWork: %s", CliHtmlFileNamePtr);
               CliSubjectPtr = MailSubject;
            }
            MailMessage (CliPersonalNamePtr, CliEmailAddressPtr,
                         CliSubjectPtr, PrintfStringPtr);
         }
         /* must be done last because it reuses the FprintfString() storage */
         if (CliCsvFileNamePtr)
         {
            status = WriteCsvFile ();
            if (VMSnok (status))
            {
               CgiLibResponseError (FI_LI, status, ErrorWritingFile);
               exit (QUIET_ERROR_EXIT);
            }
         }
      }

      if (FormWorkWatch) fprintf (stdout, "%sRESPONSE\n", WATCH_DIVIDER);

      if (CliLocationPtr)
         CgiLibResponseRedirect (CliLocationPtr);
      else
      if (!CliNoResponse)
      {
         fprintf (stdout, "Status: 200\r\nContent-Type: text/html\r\n\r\n");
         fprintf (stdout, "%s\n", SuccessMessage);
      }

      if (CliSymbolPtr) SetCliSymbols ();
   }

   if (FormWorkWatch) fprintf (stdout, "%sEND\n", WATCH_DIVIDER);
}

/*****************************************************************************/
/*
Generate a human-readable version of the submitted form.  This in-memory
version is used to generate the processed file and for the email body.
*/

FormatResponse ()

{
   int  idx;
   char  *cptr, *sptr;
   FIELD_STRUCT  *fptr;

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

   if (Debug) fprintf (stdout, "FormatResponse()\n");

   /* reset the string function */
   PrintfString (NULL);

   PrintfString ("\
Date: %02.02d %s %04.04d %02.02d:%02.02d:%02.02d\n\
Server: %s:%s\n\
Host: %s\n\
Agent: %s\n\n",
                 NumTime[2], MonthName[NumTime[1]], NumTime[0],
                 NumTime[3], NumTime[4], NumTime[5],
                 CgiLibVar("SERVER_NAME"), CgiLibVar("SERVER_PORT"),
                 SourceHost, CgiLibVar("HTTP_USER_AGENT"));

   for (idx = 0; idx < FieldCount; idx++)
   {
      fptr = &FieldData[idx];

      if (fptr->IgnoreField) continue;

      for (cptr = fptr->ValuePtr; *cptr && *cptr != '\n'; cptr++);
      if (*cptr)
      {
         PrintfString ("%s:\n\n", fptr->DescPtr, fptr->ValuePtr);
         cptr = fptr->ValuePtr;
         while (*cptr)
         {
            sptr = cptr;
            while (*cptr && *cptr != '\n') cptr++;
            PrintfString ("  %*.*s\n", cptr-sptr, cptr-sptr, sptr);
            if (*cptr) cptr++;
         }
         PrintfString ("\n");
      }
      else
         PrintfString ("%s: %s%s", fptr->DescPtr, fptr->ValuePtr,
                                   CliDoubleSpace ? "\n\n" : "\n");
   }

   PrintfString ("%s: %s\n", SOFTWAREID, CliHtmlFileNamePtr);

   if (Debug) fprintf (stdout, "PrintfStringPtr |%s|\n", PrintfStringPtr);
}

/*****************************************************************************/
/*
Process the in-memory request body, parsing out URL-form-encoded filed names
and the corresponding values checking each against the field directives
previously read from the file of the generating form.  If there are any
discrepancies between the submitted and required daya then note each for later
reporting to the client.
*/

ProcessRequestBody ()

{
   int  idx, len,
        FieldNameLength,
        FieldValueLength,
        FormWorkCount;
   char  *cptr, *sptr, *zptr,
         *FieldValue;
   char  FieldName [256];
   FIELD_STRUCT  *fptr;

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

   if (Debug) fprintf (stdout, "ProcessRequestBody()\n");

   FormWorkCount = 0;

   FieldValue = calloc (1, FieldValueCapacity);
   if (!FieldValue) exit (vaxc$errno);

   cptr = BodyBufferPtr;
   while (*cptr)
   {
      /**************/
      /* field name */
      /**************/

      FieldName[0] = '\0';
      FieldNameLength = 0;
      while (*cptr && *cptr != '=')
      {
         zptr = (sptr = FieldName) + sizeof(FieldName)-1;
         while (*cptr && *cptr != '=' && sptr < zptr)
         {
            if (isprint(*cptr))
               *sptr++ = *cptr++;
            else
               cptr++;
         }
         if (sptr >= zptr)
         {
            CgiLibResponseError (FI_LI, 0, ErrorBufferOverflow);
            exit (QUIET_ERROR_EXIT);
         }
         *sptr = '\0';
         if (Debug) fprintf (stdout, "FieldName: |%s|\n", FieldName);
         FieldNameLength += sptr - FieldName;
      }

      if (*cptr == '=') cptr++;

      /***************/
      /* field value */
      /***************/

      FieldValue[0] = '\0';
      FieldValueLength = 0;
      while (*cptr && *cptr != '&')
      {
         zptr = (sptr = FieldValue) + FieldValueCapacity - 1;
         /* absorb leading white-space */
         while (*cptr && *cptr != '&' && isspace(*cptr)) cptr++;
         /* copy value while stripping carriage-control and non-printables */
         while (*cptr && *cptr != '&' && sptr < zptr)
         {
            if (isprint(*cptr))
               *sptr++ = *cptr++;
            else
               cptr++;
         }
         if (sptr >= zptr)
         {
            CgiLibResponseError (FI_LI, 0, ErrorBufferOverflow);
            exit (QUIET_ERROR_EXIT);
         }
         /* absorb trailing white-space */
         if (sptr > FieldValue && isspace(*(sptr-1)))
         {
            sptr--;
            while (sptr > FieldValue && isspace(*sptr)) sptr++;
            if (!isspace(*sptr)) sptr++;
         }
         *sptr = '\0';
         if (Debug) fprintf (stdout, "FieldValue: |%s|\n", FieldValue);

         CgiLibUrlDecode (FieldValue);
         FieldValueLength = strlen(FieldValue);
      }

      if (*cptr == '&') cptr++;

      /***********************/
      /* find the field data */
      /***********************/

      if (FormWorkWatch)
         fprintf (stdout, "%03.03d %3.d|%s|\n    %3.d|%s|\n",
                  ++FormWorkCount,
                  strlen(FieldName), FieldName,
                  strlen(FieldValue), FieldValue);

      for (idx = 0; idx < FieldCount; idx++)
         if (strsame (FieldData[idx].NamePtr, FieldName, -1)) break;
      if (idx >= FieldCount)
      {
         if (FormWorkWatch)
            fprintf (stdout, "FIELD NAME NOT FOUND %d|%s|\n",
                     strlen(FieldName), FieldName);
         CgiLibResponseError (FI_LI, 0, ErrorConfig);
         exit (QUIET_ERROR_EXIT);
      }

      /*******************/
      /* store the value */
      /*******************/

      if (FieldData[idx].ValuePtr &&
          FieldData[idx].ValuePtr[0])
      {
         if (FieldValue[0])
         {
            len = strlen(FieldData[idx].ValuePtr);
            FieldData[idx].ValuePtr = realloc (FieldData[idx].ValuePtr,
                                               len+1+FieldValueLength+1);
            FieldData[idx].ValuePtr[len] = '+';
            strcpy (FieldData[idx].ValuePtr+len+1, FieldValue);
         }
      }
      else
      {
         FieldData[idx].ValuePtr = calloc (1, FieldValueLength+1);
         strcpy (FieldData[idx].ValuePtr, FieldValue);
      }

      if (FormWorkWatch)
         if (FieldData[idx].ValuePtr)
            fprintf (stdout, "    %3.d|%s|\n",
                     strlen(FieldData[idx].ValuePtr), FieldData[idx].ValuePtr);
         else
            fprintf (stdout, "    %3.d|{null}|\n", 0);
   }

   /****************************/
   /* check/process the fields */
   /****************************/

   for (idx = 0; idx < FieldCount; idx++)
   {
      fptr = &FieldData[idx];

      if (Debug)
         fprintf (stdout, "%03d %d %d %d %d %d |%s|%s|%s|%s|\n",
                  idx+1,
                  fptr->EmailField, fptr->IgnoreField,
                  fptr->MandatoryField, fptr->NumericField,
                  fptr->MaxSize,
                  fptr->NamePtr, fptr->FormWorkPtr,
                  fptr->DescPtr, fptr->ValuePtr);

      if (fptr->MandatoryField && (!fptr->ValuePtr || !fptr->ValuePtr[0]))
      {
         PrintfReport (ReportMandatory, fptr->DescPtr);
         continue;
      }

      if (fptr->EmailField)
      {
         for (cptr = fptr->ValuePtr; *cptr && *cptr != '@'; cptr++);
         if (!*cptr || cptr == fptr->ValuePtr)
            PrintfReport (ReportNotEmail, fptr->DescPtr);
         else
         {
            cptr++;
            sptr = cptr;
            while (*cptr) cptr++;
            if (cptr == sptr) PrintfReport (ReportNotEmail, fptr->DescPtr);
         }
      }

      if (fptr->MaxSize)
      {
         if (fptr->ValuePtr && strlen(fptr->ValuePtr) > fptr->MaxSize)
            PrintfReport (ReportTooLarge, fptr->DescPtr, fptr->MaxSize);
      }

      if (fptr->NumericField)
      {
         for (cptr = fptr->ValuePtr; *cptr && isdigit(*cptr); cptr++);
         if (*cptr) PrintfReport (ReportNotNumeric, fptr->DescPtr);
      }

      if (!fptr->AsIsCarCon && fptr->ValuePtr)
      {
         /* by default carriage-control is replaced by spaces */
         cptr = fptr->ValuePtr;
         while (*cptr && *cptr != '\r' && *cptr != '\n') cptr++;
         if (*cptr)
         {
            sptr = cptr;
            while (*cptr)
            {
              if (*(unsigned short*)cptr == '\r\n')
               {
                  *sptr++ = ' ';
                  cptr += 2;
               }
               else
               if (*cptr == '\r' || *cptr == '\n')
               {
                  *sptr++ = ' ';
                  cptr++;
               }
               else
                  *sptr++ = *cptr++;
            }
            *sptr = '\0';
         }
      }
   }

   free (FieldValue);
}

/*****************************************************************************/
/*
Some small functions used by ProcessHtmlFile().
*/

char* FindStartOfTag (char *cptr)
{
   while (*cptr && *cptr != '<') cptr++;
   return (cptr);
}

char* FindEndOfTag (char *cptr)
{
   while (*cptr) {
      while (*cptr && *cptr != '>' && *cptr != '\"') cptr++;
      if (!*cptr || *cptr == '>') break;
      cptr++;
      while (*cptr && *cptr != '\"') cptr++;
      if (*cptr) cptr++;
   }
   return (cptr);
}

/* find something like 'name="content"' */
char* FindElement (char *cptr, char *eptr, int elen)
{
   while (*cptr) {
      while (*cptr && *cptr != '\"' && *cptr != '>' &&
             !strsame(cptr, eptr, elen)) cptr++;
      if (*cptr != '\"') break;
      cptr++;
      while (*cptr && *cptr != '\"') cptr++;
      if (*cptr) cptr++;
   }
   return (cptr);
}

/* get the content from something like 'name="content"' */
char* GetElement (char *cptr, char *bptr, int blen)
{
   char  *sptr, *zptr;
   zptr = (sptr = bptr) + blen-1;
   if (*cptr == '\"')
   {
      cptr++;
      while (*cptr && *cptr != '\"' && sptr < zptr) *sptr++ = *cptr++;
      if (*cptr) cptr++;
   }
   else
      while (*cptr && *cptr != '>' && !isspace(*cptr) && sptr < zptr)
         *sptr++ = *cptr++;
   *sptr = '\0';
   return (cptr);
}

/*****************************************************************************/
/*
Read the contents of the /HTML= specified HTML file containing the form and
the formwork="" directives into memory.  Parse that file populating the
'FormData' array with the form data.  While processing check the directives for
anomalies and report as necessary.
*/

ProcessHtmlFile ()

{
   int  idx, status,
        FormWorkCount,
        FormWorkDataLength,
        InputDescLength,
        InputLiteralLength,
        InputNameLength;
   char  *cptr, *sptr, *zptr;
   char  FormWorkData [256],
         FormName [256],
         InputDesc [256],
         InputLiteral [256],
         InputName [256],
         InputTag [1024],
         InputType [256],
         Scratch [256];
   FIELD_STRUCT  *fptr;

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

   if (Debug) fprintf (stdout, "ProcessHtmlFile()\n");

   FormWorkCount = 0;

   cptr = FormPtr;
   while (*cptr)
   {
      /********************************/
      /* find the (next) <form..> tag */
      /********************************/

      cptr = FindStartOfTag (cptr);
      if (!*cptr) break;

      if (!strsame (cptr, "<form ", 6))
      {
         cptr = FindEndOfTag (cptr);
         continue;
      }

      /*************************/
      /* look for 'input' tags */
      /*************************/

      while (*cptr)
      {
         while (*cptr)
         {
            cptr = FindStartOfTag (cptr);
            if (!*cptr) break;

            /* if end of this form */
            if (strsame (cptr, "</form", 6)) break;

            /* if this is a tag we're looking for */
            if (strsame (cptr, "<input ", 7)) break;
            if (strsame (cptr, "<textarea ", 10)) break;
            if (strsame (cptr, "<select ", 8)) break;

            cptr = FindEndOfTag (cptr);
         }
         if (!*cptr) break;
         if (strsame (cptr, "</form", 6)) break;

         /***************/
         /* 'input' tag */
         /***************/

         if (FormWorkWatch)
         {
            sptr = FindEndOfTag (cptr);
            memset (InputTag, 0, sizeof(InputTag));
            memcpy (InputTag, cptr, sptr-cptr+1 > sizeof(InputTag)-1 ?
                                       sizeof(InputTag)-1 : sptr-cptr+1); 
            for (sptr = InputTag; *sptr; sptr++)
               if (*sptr == '\r' || *sptr == '\n') *sptr = ' ';
         }

         InputName[0] = FormWorkData[0] = '\0';

         sptr = cptr;
         if (strsame (cptr, "<input ", 7))
         {
            /* <textarea> and <select> tasg do not use type=".." */
            cptr = FindElement (cptr, "type=", 5);
            /* if there is no 'type=' element! */
            if (!*cptr || *cptr == '>')
            {
               cptr = FindEndOfTag (sptr);
               if (*cptr) cptr++;
               *cptr = '\0';
               if (FormWorkWatch)
                  fprintf (stdout, "NO \'type=\' %d|%s|\n",
                           strlen(sptr), sptr);
               CgiLibResponseError (FI_LI, 0, ErrorConfig);
               exit (QUIET_ERROR_EXIT);
            }

            cptr += 5;
            cptr = GetElement (cptr, InputType, sizeof(InputType));
            if (Debug) fprintf (stdout, "InputType |%s|\n", InputType);
   
            if (strsame (InputType, "reset", -1))
            {
               cptr = FindEndOfTag (sptr);
               continue;
            }
         }

         cptr = FindElement (sptr, "name=", 5);
         /* if there is no 'name=' element! */
         if (!*cptr || *cptr == '>')
         {
            cptr = FindEndOfTag (sptr);
            /* only worry about submit buttons with name=".." elements */
            if (strsame (InputType, "submit", -1)) continue;
            if (*cptr) cptr++;
            *cptr = '\0';
            if (FormWorkWatch)
               fprintf (stdout, "NO \'name=\' %d|%s|\n",
                        strlen(sptr), sptr);
            CgiLibResponseError (FI_LI, 0, ErrorConfig);
            exit (QUIET_ERROR_EXIT);
         }

         cptr += 5;
         cptr = GetElement (cptr, InputName, sizeof(InputName));
         if (Debug) fprintf (stdout, "InputName |%s|\n", InputName);
         InputNameLength = strlen(InputName);

         cptr = FindElement (sptr, "formwork=", 9);
         if (!*cptr || *cptr == '>')
         {
            /* there is no 'formwork=' element! */
            FormWorkData[0] = '\0';
            FormWorkDataLength = 0;
         }
         else
         {
            cptr += 9;
            cptr = GetElement (cptr, FormWorkData, sizeof(FormWorkData));
            if (Debug) fprintf (stdout, "FormWorkData |%s|\n", FormWorkData);
            FormWorkDataLength = strlen(FormWorkData);
         }

         /********************************************/
         /* process tag name and (any) formwork data */
         /********************************************/

         if (FormWorkWatch)
            fprintf (stdout, "%03.03d %3.d|%s|\n    %3.d|%s|\n",
                     ++FormWorkCount,
                     strlen(InputTag), InputTag,
                     strlen(FormWorkData), FormWorkData);

         for (idx = 0; idx < FieldCount; idx++)
            if (strsame (FieldData[idx].NamePtr, InputName, -1)) break;
         fptr = &FieldData[idx];
         if (idx >= FieldCount)
         {
            /* new input field */
            if (++FieldCount >= MAX_FIELDS)
            {
               if (FormWorkWatch)
                  fprintf (stdout, "MAX_FIELDS %d EXCEEDED\n", MAX_FIELDS);
               CgiLibResponseError (FI_LI, 0, ErrorConfig);
               exit (QUIET_ERROR_EXIT);
            }
            fptr->NamePtr = calloc (1, InputNameLength+1);
            strcpy (fptr->NamePtr, InputName);
         }

         if (FormWorkData[0])
         {
            char  *cptr, *sptr, *zptr;

            fptr->FormWorkPtr = calloc (1, FormWorkDataLength+1);
            strcpy (fptr->FormWorkPtr, FormWorkData);

            cptr = sptr = FormWorkData;
            while (*cptr)
            {
               if (Debug) fprintf (stdout, "cptr |%s|\n", cptr);
               while (*cptr && !isalpha(*cptr)) cptr++;
               switch (*(unsigned short*)cptr)
               {
                  case 'as' :
                  case 'AS' :

                     cptr += 2; fptr->AsIsCarCon = true; break;

                  case 'cg' :
                  case 'CG' :

                     sptr = cptr;
                     cptr += 2;
                     if (*cptr != '{')
                     {
                        if (FormWorkWatch)
                           fprintf (stdout, "NO CGI \'{\' |%s|\n", sptr);
                        CgiLibResponseError (FI_LI, 0, ErrorConfig);
                        exit (QUIET_ERROR_EXIT);
                     }
                     cptr++;
                     zptr = (sptr = Scratch) + sizeof(Scratch)-1;
                     while (*cptr && *cptr != '}' && sptr < zptr)
                     {
                        if (*cptr == '\\') cptr++;
                        if (*cptr) *sptr++ = *cptr++;
                     }
                     *sptr = '\0';
                     if (*cptr == '}') cptr++;
                     sptr = CgiLibVarNull (Scratch);
                     if (!sptr) sptr = "{none}";
                     fptr->ValuePtr = calloc (1, strlen(sptr)+1);
                     strcpy (fptr->ValuePtr, sptr);
                     break;

                  case 'de' :
                  case 'DE' :

                     sptr = cptr;
                     cptr += 2;
                     if (*cptr != '{')
                     {
                        if (FormWorkWatch)
                           fprintf (stdout, "NO DE \'{\' |%s|\n",
                                    sptr);
                        CgiLibResponseError (FI_LI, 0, ErrorConfig);
                        exit (QUIET_ERROR_EXIT);
                     }
                     cptr++;
                     zptr = (sptr = InputDesc) + sizeof(InputDesc)-1;
                     while (*cptr && *cptr != '}' && sptr < zptr)
                     {
                        if (*cptr == '\\') cptr++;
                        if (*cptr) *sptr++ = *cptr++;
                     }
                     *sptr = '\0';
                     if (*cptr == '}') cptr++;
                     InputDescLength = sptr - InputDesc;
                     fptr->DescPtr = calloc (1, InputDescLength+1);
                     strcpy (fptr->DescPtr, InputDesc);
                     break;

                  case 'em' :
                  case 'EM' :

                     cptr += 2; fptr->EmailField = true; break;

                  case 'ig' :
                  case 'IG' :

                     cptr += 2; fptr->IgnoreField = true; break;

                  case 'li' :
                  case 'LI' :

                     sptr = cptr;
                     cptr += 2;
                     if (*cptr != '{')
                     {
                        if (FormWorkWatch)
                           fprintf (stdout, "NO LI \'{\' |%s|\n",
                                    sptr);
                        CgiLibResponseError (FI_LI, 0, ErrorConfig);
                        exit (QUIET_ERROR_EXIT);
                     }
                     cptr++;
                     zptr = (sptr = InputLiteral) + sizeof(InputLiteral)-1;
                     while (*cptr && *cptr != '}' && sptr < zptr)
                     {
                        if (*cptr == '\\') cptr++;
                        if (*cptr) *sptr++ = *cptr++;
                     }
                     *sptr = '\0';
                     if (*cptr == '}') cptr++;
                     InputLiteralLength = sptr - InputLiteral;
                     fptr->ValuePtr = calloc (1, InputLiteralLength+1);
                     strcpy (fptr->ValuePtr, InputLiteral);
                     break;

                  case 'ma' :
                  case 'MA' :

                     cptr += 2; fptr->MandatoryField = true; break;

                  case 'nu' :
                  case 'NU' :

                     cptr += 2; fptr->NumericField = true; break;

                  case 'op' :
                  case 'OP' :

                     /* just for documentation purposes really */
                     cptr += 2; break;

                  case 'si' :
                  case 'SI' :

                     sptr = cptr;
                     cptr += 2;
                     fptr->MaxSize = atoi(cptr);
                     if (!isdigit(*cptr) || !fptr->MaxSize)
                     {
                        if (FormWorkWatch)
                           fprintf (stdout, "NO SIZE |%s|\n", sptr);
                        CgiLibResponseError (FI_LI, 0, ErrorConfig);
                        exit (QUIET_ERROR_EXIT);
                     }
                     while (*cptr && isdigit(*cptr)) cptr++;
                     break;

                  case 'wa' :
                  case 'WA' :

                     cptr += 2;
                     if (!FormWorkWatch)
                     {
                        CgiLibResponseHeader (200, "text/plain");
                        fprintf (stdout, "%sHTML\n%s%sINPUTS\n",
                                 WATCH_DIVIDER, FormPtr, WATCH_DIVIDER);
                        FormWorkWatch = true;
                     }
                     break;

                  default :
                     if (FormWorkWatch)
                        fprintf (stdout, "UNKNOWN FORMWORK |%s|\n", cptr);
                     CgiLibResponseError (FI_LI, 0, ErrorConfig);
                     exit (QUIET_ERROR_EXIT);
               }
            }

            if (Debug)
               fprintf (stdout, "%03d %d %d %d %d %d |%s|%s|%s|%s|\n",
                        FieldCount,
                        fptr->EmailField, fptr->IgnoreField,
                        fptr->MandatoryField, fptr->NumericField,
                        fptr->MaxSize,
                        fptr->NamePtr, fptr->FormWorkPtr,
                        fptr->DescPtr, fptr->ValuePtr);
         }
      }
   }

   /***********************************/
   /* check all input have essentials */
   /***********************************/

   for (idx = 0; idx < FieldCount; idx++)
   {
      fptr = &FieldData[idx];
      if (!fptr->FormWorkPtr)
      {
         if (FormWorkWatch)
            fprintf (stdout, "NO \'formwork=\' %d|%s|\n",
                     strlen(fptr->NamePtr), fptr->NamePtr);
         CgiLibResponseError (FI_LI, 0, ErrorConfig);
         exit (QUIET_ERROR_EXIT);
      }
      if (fptr->IgnoreField) continue;
      if (!fptr->DescPtr)
      {
         if (FormWorkWatch)
            fprintf (stdout, "NO DE{..} %d|%s|\n",
                     strlen(fptr->NamePtr), fptr->NamePtr);
         CgiLibResponseError (FI_LI, 0, ErrorConfig);
         exit (QUIET_ERROR_EXIT);
      }
   }

}

/*****************************************************************************/
/*
Write the URL-form-encoded (not decoded) request body into a time-stamped file. 
Prepend some relevant CGI source host information in URL-form-encoded format.
*/

WriteRawFile ()

{
   int  cnt, status;
   char  FileName [256];
   FILE  *fp;

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

   if (Debug) fprintf (stdout, "WriteRawFile()\n");

   cnt = sprintf (FileName,
"%s%04.04d%02.02d%02.02d%02.02d%02.02d%02.02d%02.02d_%08.08X_RAW.TXT",
            CliRawFileNamePtr, NumTime[0], NumTime[1], NumTime[2], 
            NumTime[3], NumTime[4], NumTime[5], NumTime[6], ScriptPid);
   if (FormWorkWatch)
      fprintf (stdout, "WRITE RAW %d|%s|\n", cnt, FileName);

   fp = fopen (FileName, "w", "shr=nil");
   if (!fp) return (vaxc$errno);
   cnt = fprintf (fp,
"REMOTE_ADDR=%s&REMOTE_HOST=%s&HTTP_FORWARDED=%s&HTTP_X_FORWARDED_FOR=%s&%s",
                  CgiLibVar("REMOTE_ADDR"),
                  CgiLibVar("REMOTE_HOST"),
                  CgiLibVar("HTTP_FORWARDED"),
                  CgiLibVar("HTTP_X_FORWARDED_FOR"),
                  BodyBufferPtr);
   if (cnt)
      status = SS$_NORMAL;
   else
      status = vaxc$errno;
   fclose (fp);

   if (CliSymbolPtr)
   {
      char  SymbolName [256];
      char  *cptr, *sptr, *zptr;
      zptr = (sptr = SymbolName) + sizeof(SymbolName)-1;
      /* prefix the symbol name */
      for (cptr = CliSymbolPtr; *cptr && sptr < zptr; *sptr++ = *cptr++);
      for (cptr = "_FILENAME__RAW"; *cptr && sptr < zptr; *sptr++ = *cptr++);
      *sptr = '\0';
      SetGlobalSymbol (SymbolName, FileName, 0);
   }

   return (status);
}

/*****************************************************************************/
/*
Write the in-memory, human-readable submitted form into a time-stamped file.
*/

WriteProcessedFile ()

{
   int  cnt, status;
   char  FileName [256];
   FILE  *fp;

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

   if (Debug) fprintf (stdout, "WriteProcessedFile()\n");

   cnt = sprintf (FileName,
"%s%04.04d%02.02d%02.02d%02.02d%02.02d%02.02d%02.02d_%08.08X_PRO.TXT",
            CliProcessedFileNamePtr, NumTime[0], NumTime[1], NumTime[2], 
            NumTime[3], NumTime[4], NumTime[5], NumTime[6], ScriptPid);
   if (FormWorkWatch)
      fprintf (stdout, "WRITE PROCESSED %d|%s|\n", cnt, FileName);

   fp = fopen (FileName, "w", "shr=nil");
   if (!fp) return (vaxc$errno);
   cnt = fprintf (fp, "%s", PrintfStringPtr);
   if (cnt)
      status = SS$_NORMAL;
   else
      status = vaxc$errno;
   fclose (fp);

   if (CliSymbolPtr) 
   {
      char  SymbolName [256];
      char  *cptr, *sptr, *zptr;
      zptr = (sptr = SymbolName) + sizeof(SymbolName)-1;
      /* prefix the symbol name */
      for (cptr = CliSymbolPtr; *cptr && sptr < zptr; *sptr++ = *cptr++);
      for (cptr = "_FILENAME__PRO"; *cptr && sptr < zptr; *sptr++ = *cptr++);
      *sptr = '\0';
      SetGlobalSymbol (SymbolName, FileName, 0);
   }

   return (status);
}

/*****************************************************************************/
/*
Write the Comma-Separated Value (even if separated by some other character)
entry in the specified file.  If it's the first entry when write a leading
header record containing the column descriptions.
*/

WriteCsvFile ()

{
   int  cnt, idx, status;
   char  *cptr;
   FIELD_STRUCT  *fptr;
   FILE  *fp;
   stat_t  FstatBuffer;

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

   if (Debug) fprintf (stdout, "WriteCsvFile()\n");

   /* reset the string function */
   PrintfString (NULL);

   if (FormWorkWatch)
      fprintf (stdout, "WRITE CSV %d|%s|\n",
               strlen(CliCsvFileNamePtr), CliCsvFileNamePtr);

   for (cnt = 0; cnt < 10; cnt++)
   {
      fp = fopen (CliCsvFileNamePtr, "a", "shr=put");
      if (fp) break;
      sleep (1);
   }
   if (!fp) return (vaxc$errno);

   if (fstat (fileno(fp), &FstatBuffer) < 0)
   {
      status = vaxc$errno;
      if (Debug) fprintf (stdout, "fstat() %%X%08.08X\n", status);
      fclose (fp);
      return (status);
   }

   if (!FstatBuffer.st_size)
   {
      /* first entry, provide headings */
      PrintfString ("Date%cHost%c", *CliSeparatorPtr, *CliSeparatorPtr);
      cnt = 0;
      for (idx = 0; idx < FieldCount; idx++)
      {
         fptr = &FieldData[idx];
         if (fptr->IgnoreField) continue;
         if (cnt++)
            PrintfString ("%c%s", *CliSeparatorPtr, fptr->DescPtr);
         else
            PrintfString ("%s", fptr->DescPtr);
      }
      PrintfString ("\n");
   }

   PrintfString ("%02.02d %s %04.04d %02.02d:%02.02d:%02.02d%c%s%c",
                 NumTime[2], MonthName[NumTime[1]], NumTime[0],
                 NumTime[3], NumTime[4], NumTime[5],
                 *CliSeparatorPtr, SourceHost, *CliSeparatorPtr);

   cnt = 0;
   for (idx = 0; idx < FieldCount; idx++)
   {
      fptr = &FieldData[idx];
      if (fptr->IgnoreField) continue;
      /* ensure any embedded carriage control or separator become spaces */
      for (cptr = fptr->ValuePtr; *cptr; cptr++)
         if (*cptr == '\r' || *cptr == '\n' || *cptr == *CliSeparatorPtr)
            *cptr = ' ';
      if (cnt++)
         PrintfString ("%c%s", *CliSeparatorPtr, fptr->ValuePtr);
      else
         PrintfString ("%s", fptr->ValuePtr);
   }
   PrintfString ("\n");

   if (Debug) fprintf (stdout, "PrintfStringPtr |%s|\n", PrintfStringPtr);

   cnt = fprintf (fp, "%s", PrintfStringPtr);
   if (cnt)
      status = SS$_NORMAL;
   else
      status = vaxc$errno;
   fclose (fp);
   return (status);
}

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

SetCliSymbols ()

{
   int  idx;
   char  *cptr, *sptr, *zptr;
   char  SymbolName [256];
   FIELD_STRUCT  *fptr;

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

   if (Debug) fprintf (stdout, "SetCliSymbols()\n");

   for (idx = 0; idx < FieldCount; idx++)
   {
      fptr = &FieldData[idx];
      zptr = (sptr = SymbolName) + sizeof(SymbolName)-1;
      /* prefix the symbol name */
      for (cptr = CliSymbolPtr; *cptr && sptr < zptr; *sptr++ = *cptr++);
      /* convert any unacceptable characters in the name into underscores */
      for (cptr = fptr->NamePtr; *cptr && sptr < zptr; cptr++)
         if (isalpha(*cptr)) *sptr++ = *cptr; else *sptr++ = '_';
      *sptr = '\0';
      SetGlobalSymbol (SymbolName, fptr->ValuePtr, 0);
   }
}

/*****************************************************************************/
/*
Read the request body (if POST) or the request's query string (if GET) into a
single array of char (doesn't matter whether it's text or binary).
*/

ReadRequestDataIntoMemory
(
char **BufferPtrPtr,
int *DataSizePtr
)
{
   static int  BufferChunk = 1024;

   static int  BufferCount;
   static char  *BufferPtr;

   int  BufferSize,
        ReadCount;
   char  *cptr;

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

   if (Debug) fprintf (stdout, "ReadRequestDataIntoMemory()\n");

   /* if the body has already been read then just return */
   if (BufferPtr != NULL)
   {
      if (BufferPtrPtr != NULL) *BufferPtrPtr = BufferPtr;
      if (DataSizePtr != NULL) *DataSizePtr = BufferCount;
      return;
   }

   CgiRequestMethodPtr = CgiLibVar("WWW_REQUEST_METHOD");
   if (strsame (CgiRequestMethodPtr, "GET", -1))
   {
      BufferPtr = CgiLibVar("WWW_QUERY_STRING");
      BufferCount = strlen (BufferPtr);
      if (BufferPtrPtr != NULL) *BufferPtrPtr = BufferPtr;
      if (DataSizePtr != NULL) *DataSizePtr = BufferCount;
      return;
   }
   else
   if (strsame (CgiRequestMethodPtr, "POST", -1))
   {
      CgiContentTypePtr = CgiLibVar("WWW_CONTENT_TYPE");
      if (!strsame (CgiContentTypePtr, FORM_URLENCODED, -1))
      {
         CgiLibResponseError (FI_LI, 0, ErrorNotUrlEncoded);
         exit (QUIET_ERROR_EXIT);
      }
   }
   else
   {
      CgiLibResponseError (FI_LI, 0, ErrorHttpMethod);
      exit (QUIET_ERROR_EXIT);
   }

   CgiLibReadRequestBody (&BufferPtr, &BufferCount);

   if (BufferPtrPtr != NULL) *BufferPtrPtr = BufferPtr;
   if (DataSizePtr != NULL) *DataSizePtr = BufferCount;
}

/****************************************************************************/
/*
Read the file contents specified by 'FileName' into memory, set the pointer
at 'FileTextPtr' to the contents and the file size at 'FileSizePtr'.  Returns a
VMS status value that should be checked.
*/ 

int ReadFileIntoMemory
(
char *Source,
char **FileTextPtr,
int *FileSizePtr
)
{
   static int  BytesRemaining;

   int  status,
        Bytes,
        BufferCount,
        Length;
   char  *BufferPtr,
         *LinePtr;
   FILE  *fp;
   stat_t  FstatBuffer;

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

   if (Debug) fprintf (stdout, "ReadFileIntoMemory() |%s|\n", Source);

   if ((fp = fopen (Source, "r", "shr=get", "shr=put")) == NULL)
   {
      status = vaxc$errno;
      if (Debug) fprintf (stdout, "fopen() %%X%08.08X\n", status);
      return (status);
   }

   if (fstat (fileno(fp), &FstatBuffer) < 0)
   {
      status = vaxc$errno;
      if (Debug) fprintf (stdout, "fstat() %%X%08.08X\n", status);
      fclose (fp);
      return (status);
   }

   Bytes = FstatBuffer.st_size;
   if (Debug) fprintf (stdout, "%d bytes\n", Bytes);
   /* a little margin for error ;^) */
   Bytes += 32;

   BufferPtr = calloc (Bytes, 1);
   if (!BufferPtr)
   {
      status = vaxc$errno;
      CgiLibResponseError (FI_LI, status, "calloc()");
      exit (status);
   }
   BytesRemaining = Bytes;
   LinePtr = BufferPtr;

   BufferCount = 0;
   while (fgets (LinePtr, BytesRemaining, fp) != NULL)
   {
      /** if (Debug) fprintf (stdout, "|%s|\n", LinePtr); **/
      Length = strlen(LinePtr);
      LinePtr += Length;
      BufferCount += Length;
      BytesRemaining -= Length;
   }
   fclose (fp);

   if (Debug) fprintf (stdout, "%d |%s|\n", BufferCount, BufferPtr);

   if (FileTextPtr != NULL) *FileTextPtr = BufferPtr;
   if (FileSizePtr != NULL) *FileSizePtr = BufferCount;

   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
Use the VMS callable mail interface to create and send a VMS mail message.  
'To' can be  a list of comma-separated addresses.  'Subject' is a
null-terminated string. 'Body' is a  null-terminated string of '\n'-separated
lines of plain text.  Just truncates anything longer than 255 characters (body
excluded, body records included)!
*/ 

int MailMessage
(
char *PersonalName,
char *To,
char *Subject,
char *Body
)
{
   int  status;
   unsigned long  SendContext = 0;
   char  *cptr, *sptr;

   struct {
      short int  buf_len;
      short int  item;
      void  *buf_addr;
      unsigned short  *ret_len;
   }
   BodyPartItem [] =
   {
      { 0, MAIL$_SEND_RECORD, 0, 0 },
      { 0, MAIL$_NOSIGNAL, 0, 0 },
      {0,0,0,0}
   },
   PersonalNameItem [] =
   {
      { 0, MAIL$_SEND_PERS_NAME, PersonalName, 0 },
      { 0, MAIL$_NOSIGNAL, 0, 0 },
      {0,0,0,0}
   },
   SendUserNameItem [] =
   {
      { 0, MAIL$_SEND_USERNAME, 0, 0 },
      { 0, MAIL$_NOSIGNAL, 0, 0 },
      {0,0,0,0}
   },
   SubjectItem [] =
   {
      { 0, MAIL$_SEND_SUBJECT, Subject, 0 },
      { 0, MAIL$_NOSIGNAL, 0, 0 },
      {0,0,0,0}
   },
   NoSignalItem [] =
   {
      { 0, MAIL$_NOSIGNAL, 0, 0 },
      {0,0,0,0}
   },
   NullItem = {0,0,0,0};

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

   if (Debug)
     fprintf (stdout, "MailMessage() |%s|\n|%s|\n|%s|\n",
              PersonalName, To, Subject);

   if (PersonalName != NULL && PersonalName[0])
   {
      PersonalNameItem[0].buf_len = strlen(PersonalName);
      if (PersonalNameItem[0].buf_len > 255) PersonalNameItem[0].buf_len = 255;
      status = mail$send_begin (&SendContext, &PersonalNameItem, &NullItem);
   }
   else
      status = mail$send_begin (&SendContext, &NoSignalItem, &NullItem);

   if (VMSnok (status))
   {
      CgiLibResponseError (FI_LI, status, "beginning message");
      exit (QUIET_ERROR_EXIT);
   }

   /* a single, or multiple comma-separated addresses */
   cptr = To;
   while (*cptr)
   {
      sptr = cptr;
      while (*cptr && *cptr != ',') cptr++;
      if (*cptr) *cptr++ = '\0';

      SendUserNameItem[0].buf_addr = sptr;
      SendUserNameItem[0].buf_len = strlen(sptr);
      if (SendUserNameItem[0].buf_len > 255) SendUserNameItem[0].buf_len = 255;

      if (Debug)
         fprintf (stdout, "address |%s|\n",
                 (char*)SendUserNameItem[0].buf_addr);
      status = mail$send_add_address (&SendContext, &SendUserNameItem,
                                      &NullItem);
      if (VMSnok (status))
      {
         CgiLibResponseError (FI_LI, status, sptr);
         exit (QUIET_ERROR_EXIT);
      }
   }

   SubjectItem[0].buf_len = strlen(Subject);
   if (SubjectItem[0].buf_len > 255) SubjectItem[0].buf_len = 255;
   status = mail$send_add_attribute (&SendContext, &SubjectItem, &NullItem);
   if (VMSnok (status))
   {
      CgiLibResponseError (FI_LI, status, "adding subject");
      exit (QUIET_ERROR_EXIT);
   }

   cptr = Body;
   while (*cptr)
   {
      BodyPartItem[0].buf_addr = cptr;
      while (*cptr && *cptr != '\n') cptr++;
      BodyPartItem[0].buf_len = cptr - (char*)BodyPartItem[0].buf_addr;
      if (BodyPartItem[0].buf_len > 255) BodyPartItem[0].buf_len = 255;
      if (*cptr) cptr++;
      status = mail$send_add_bodypart (&SendContext, &BodyPartItem, &NullItem);
      if (VMSnok (status))
      {
         CgiLibResponseError (FI_LI, status, "adding body");
         exit (QUIET_ERROR_EXIT);
      }
   }

   status = mail$send_message (&SendContext, &NoSignalItem, &NoSignalItem);
   if (VMSnok (status))
   {
       CgiLibResponseError (FI_LI, status, "sending message");
       exit (QUIET_ERROR_EXIT);
   }

   mail$send_end (&SendContext, &NullItem, &NullItem);

   return (SS$_NORMAL);
}

/****************************************************************************/
/*
Create a dynamic null-terminated string using 'printf' format strings.
*/

void PrintfString
(
char *FormatString,
...
)
{
   int  argcnt,
        BufferLength;
   char  *cptr;
   char  Buffer [4096];
   va_list  argptr;

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

   va_count (argcnt);

   if (Debug)
      fprintf (stdout, "PrintfString() %d |%s|\n", argcnt, FormatString);

   if (!FormatString)
   {
      if (PrintfStringPtr) free (PrintfStringPtr);
      PrintfStringPtr = NULL,
      PrintfStringLength = 0;
      return;
   }

   va_start (argptr, FormatString);
   BufferLength = vsprintf (Buffer, FormatString, argptr);
   if (BufferLength > sizeof(Buffer)-1) _exit (SS$_BUGCHECK);
   va_end (argptr);

   PrintfStringPtr = realloc (PrintfStringPtr,
                              PrintfStringLength+BufferLength+16);
   if (!PrintfStringPtr)
   {
      CgiLibResponseError (FI_LI, vaxc$errno, ErrorMemory);
      exit (QUIET_ERROR_EXIT);
   }
   strcpy (PrintfStringPtr+PrintfStringLength, Buffer);
   PrintfStringLength += BufferLength;
   PrintfStringPtr[PrintfStringLength] = '\0';
}

/****************************************************************************/
/*
Create a dynamic null-terminated string using 'printf' format strings in HTML
list entry format of each anomaly reported via this function.
*/

void PrintfReport
(
char *FormatString,
...
)
{
   int  argcnt,
        BufferLength;
   char  *cptr;
   char  Buffer [1024];
   va_list  argptr;

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

   va_count (argcnt);

   if (Debug)
      fprintf (stdout, "PrintfReport() %d |%s|\n", argcnt, FormatString);

   va_start (argptr, FormatString);
   BufferLength = vsprintf (Buffer, FormatString, argptr);
   if (BufferLength > sizeof(Buffer)-1) _exit (676);
   va_end (argptr);

   PrintfReportPtr = realloc (PrintfReportPtr,
                              PrintfReportLength+BufferLength+16);
   if (!PrintfReportPtr)
   {
      CgiLibResponseError (FI_LI, vaxc$errno, ErrorMemory);
      exit (QUIET_ERROR_EXIT);
   }
   strcpy (PrintfReportPtr+PrintfReportLength, "<li>");
   PrintfReportLength += 4;
   strcpy (PrintfReportPtr+PrintfReportLength, Buffer);
   PrintfReportLength += BufferLength;
   PrintfReportPtr[PrintfReportLength++] = '\n';
   PrintfReportPtr[PrintfReportLength] = '\0';
}

/*****************************************************************************/
/*
Get "command-line" parameters, whether from the command-line or from a
configuration symbol or logical containing the equivalent.  OSU scripts have
the 'method', 'url' and 'protocol' supplied as P1, P2, P3 (these being detected
and used by CGILIB), and are of no interest to this function.
*/

GetParameters ()

{
   static char  CommandLine [1024];
   static unsigned long  Flags = 0;

   int  status,
        SkipParameters;
   unsigned short  Length;
   char  ch;
   char  *aptr, *cptr, *clptr, *sptr;
   $DESCRIPTOR (CommandLineDsc, CommandLine);

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

   if (Debug) fprintf (stdout, "GetParameters()\n");

   if ((clptr = getenv ("FORMWORK$PARAM")) == NULL)
   {
      /* get the entire command line following the verb */
      if (VMSnok (status =
          lib$get_foreign (&CommandLineDsc, 0, &Length, &Flags)))
         exit (status);
      (clptr = CommandLine)[Length] = '\0';
   }

   /* if OSU environment then skip P1, P2, P3 */
   if (getenv ("WWWEXEC_RUNDOWN_STRING") != NULL)
      SkipParameters = 3;
   else
      SkipParameters = 0;

   aptr = NULL;
   ch = *clptr;
   for (;;)
   {
      if (aptr != NULL && *aptr == '/') *aptr = '\0';
      if (!ch) break;

      *clptr = ch;
      if (Debug) fprintf (stdout, "clptr |%s|\n", clptr);
      while (*clptr && isspace(*clptr)) *clptr++ = '\0';
      aptr = clptr;
      if (*clptr == '/') clptr++;
      while (*clptr && !isspace (*clptr) && *clptr != '/')
      {
         if (*clptr != '\"')
         {
            clptr++;
            continue;
         }
         cptr = clptr;
         clptr++;

         while (*clptr)
         {
            if (*clptr == '\"')
               if (*(clptr+1) == '\"')
                  clptr++;
               else
                  break;
            *cptr++ = *clptr++;
         }
         *cptr = '\0';
         if (*clptr) clptr++;
      }
      ch = *clptr;
      if (*clptr) *clptr = '\0';
      if (Debug) fprintf (stdout, "aptr |%s|\n", aptr);
      if (!*aptr) continue;

      if (SkipParameters)
      {
         SkipParameters--;
         continue;
      }

      if (strsame (aptr, "/CAPACITY=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         FieldValueCapacity = atoi(cptr);
         continue;
      }
      if (strsame (aptr, "/CHARSET=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         CliCharsetPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/CSV=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         CliCsvFileNamePtr = cptr;
         continue;
      }
      if (strsame (aptr, "/DBUG", -1))
      {
         Debug = true;
         continue;
      }
      if (strsame (aptr, "/DOUBLESPACE", 4))
      {
         CliDoubleSpace = true;
         continue;
      }
      if (strsame (aptr, "/EMAIL=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         CliEmailAddressPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/HTML=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         CliHtmlFileNamePtr = cptr;
         continue;
      }
      if (strsame (aptr, "/LOCATION=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         CliLocationPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/NORESPONSE", 6))
      {
         CliNoResponse = true;
         continue;
      }
      if (strsame (aptr, "/PERSONAL_NAME=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         CliPersonalNamePtr = cptr;
         continue;
      }
      if (strsame (aptr, "/PROCESSED=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         CliProcessedFileNamePtr = cptr;
         continue;
      }
      if (strsame (aptr, "/RAW=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         CliRawFileNamePtr = cptr;
         continue;
      }
      if (strsame (aptr, "/SEPARATOR=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         CliSeparatorPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/SOFTWAREID", 4) ||
          strsame (aptr, "/VERSION", 4))
      {
         fprintf (stdout, "%%%s-I-SOFTWAREID, %s\n%s\n",
                  Utility, SoftwareID, CopyrightInfo);
         exit (SS$_NORMAL);
      }
      if (strsame (aptr, "/SUBJECT=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         CliSubjectPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/SYMBOLS=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         if (*cptr) CliSymbolPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/WATCH", 4))
      {
         FormWorkWatch = true;
         continue;
      }

      if (*aptr == '/')
      {
         fprintf (stdout, "%%%s-E-IVQUAL, unrecognized qualifier\n \\%s\\\n",
                  Utility, aptr+1);
         exit (QUIET_ERROR_EXIT);
      }

      fprintf (stdout, "%%%s-E-MAXPARM, too many parameters\n \\%s\\\n",
               Utility, aptr);
      exit (QUIET_ERROR_EXIT);
   }
}

/****************************************************************************/
/*
Assign a global symbol.  If the string pointer is null the numeric value is
used.  Symbol lengths are adjusted according to the maximum allowed for the
version of the operating system.
*/ 

SetGlobalSymbol
(
char *Name,
char *String,
int Value
)
{
   static int  SymbolType = LIB$K_CLI_GLOBAL_SYM;
   static char  ValueString [32];
   static $DESCRIPTOR (NameDsc, "");
   static $DESCRIPTOR (ValueDsc, "");
   static $DESCRIPTOR (ValueFaoDsc, "!UL");
   static $DESCRIPTOR (ValueStringDsc, ValueString);

   int  status;

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

   if (Debug)
      fprintf (stdout, "SetGlobalSymbol() |%s|%s| %d\n", Name, String, Value);

   NameDsc.dsc$a_pointer = Name;
   NameDsc.dsc$w_length = strlen(Name);
   if (!String)
   {
      ValueDsc.dsc$a_pointer = ValueString;
      sys$fao (&ValueFaoDsc, &ValueDsc.dsc$w_length, &ValueStringDsc, Value);
      ValueString[ValueDsc.dsc$w_length] = '\0';
   }
   else
   {
      ValueDsc.dsc$a_pointer = String;
      ValueDsc.dsc$w_length = strlen(String);
      if (ValueDsc.dsc$w_length > 255 && VmsVersion() < 700)
         ValueDsc.dsc$w_length = 255;
      else
      if (ValueDsc.dsc$w_length > 1023 && VmsVersion() < 732)
         ValueDsc.dsc$w_length = 1023;
      else
      if (ValueDsc.dsc$w_length > 8191)
         ValueDsc.dsc$w_length = 8191;
   }

   if (VMSnok (status = lib$set_symbol (&NameDsc, &ValueDsc, &SymbolType)))
      exit (status);
}

/****************************************************************************/
/*
Return an integer reflecting the major, minor and other VMS version number.
For example, return 600 for "V6.0", 730 for "V7.3" and 732 for "V7.3-2".
*/ 

int VmsVersion ()

{
   static int  VersionInteger;
   static char  SyiVersion [8+1];

   static struct {
      short int  buf_len;
      short int  item;
      void  *buf_addr;
      unsigned short  *ret_len;
   }
   SyiItems [] =
   {
      { 8, SYI$_VERSION, &SyiVersion, 0 },
      { 0,0,0,0 }
   };

   int  status;
   char  *cptr;

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

   if (Debug) fprintf (stdout, "VmsVersion() %d\n", VersionInteger);

   if (VersionInteger) return (VersionInteger);

   if (VMSnok (status = sys$getsyiw (0, 0, 0, &SyiItems, 0, 0, 0)))
      exit (status);

   if (cptr = getenv("WASD_VMS_VERSION"))
      strncpy (SyiVersion, cptr, sizeof(SyiVersion));

   SyiVersion[sizeof(SyiVersion)-1] = '\0';
   if (Debug) fprintf (stdout, "SyiVersion |%s|\n", SyiVersion);

   if (SyiVersion[0] == 'V' &&
       isdigit(SyiVersion[1]) &&
       SyiVersion[2] == '.' &&
       isdigit(SyiVersion[3]))
   {
      /* e.g. "V7.3" */
      VersionInteger = ((SyiVersion[1]-48) * 100) + ((SyiVersion[3]-48) * 10);
      /* if something like "V7.3-2" */
      if (SyiVersion[4] == '-') VersionInteger += SyiVersion[5]-48;
   }
   else
   {
      fprintf (stdout,
"%%%s-E-VMS, cannot understand VMS version string \"%s\"\n\
-%s-I-KLUDGE, continue by using WASD_VMS_VERSION environment variable\n",
               Utility, SyiVersion, Utility);
      exit (SS$_BUGCHECK);
   }

   if (Debug) fprintf (stdout, "%d\n", VersionInteger);
   return (VersionInteger);
}

/****************************************************************************/
/*
Does a case-insensitive, character-by-character string compare and returns
true if two strings are the same, or false if not.
*/

boolean strsame
(
char *sptr1,
char *sptr2,
int  count
)
{
   /*********/
   /* begin */
   /*********/

   if (count > 0)
      return (strncasecmp (sptr1, sptr2, count) == 0);
   else
      return (strcasecmp (sptr1, sptr2) == 0);
}

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

