/*****************************************************************************/
/*
                                 Conan.c


CGI-compliant script providing access to VMS help and text libraries.
Can be used in the WASD CGI, WASD CGI-plus, OSU and vanilla CGI environments.

"Conan The Librarian"

AND REMEMBER ... Conan can be used as a CGIplus script.  This may require the
script to be purged from the server before new startup parameters come into
effect.  Use a command like the following:

  HTTPD /DO=DCL=PURGE


WASD SERVER CONFIGURATION
-------------------------
This script uses URL-style paths for accessing libraries. The HTTP server must
therefore provide a mapping for all directories that libraries will be
accessed. Something like:

  pass /sys$common/syshlp/* /sys$common/syshlp/*

If auto-scripting is available (as with WASD) these should also be a
configuration directive providing this (example from WASD):

  .HLB  application/x-script  /Conan  VMS help library


HELP LIBRARIES
--------------
Help library modules have a reasonably complex internal structure described in 
the "Utility Routines Manual" and the "Command Definition, Librarian, and 
Message Utilities Manual".  The structure will not be described here. 

KeyWord[1]...KeyWord[10] represent the help keywords 1...10 that are used as 
keys into help information.


CGI ENVIRONMENT VARIABLES
-------------------------
?single=1       one module per line (rather than alphabetically grouped)
?extract=1      extract (rather than format) a Help module
                (the final 'e' in 'explode' is a link to this)


LOGICAL NAMES
-------------
CONAN$DBUG             turns on all "if (Debug)" statements
CONAN$PARAM            equivalent to (overrides) the command line
                       parameters/qualifiers (define as a system-wide logical)
HTTPD$GMT              timezone offset from Greenwich Mean Time
                       (e.g. "+09:30" and only needed if DTSS is not in use)


QUALIFIERS
----------
/BUTTONS=       custom button string
/CHARSET=       "Content-Type: text/html; charset=...", empty suppress charset
/DBUG           turns on all "if (Debug)" statements
/[NO]ODS5       control extended file specification (basically for testing)
/SINGLE         one module per line (rather than alphabetically grouped)
                for all requests
/STYLE=         URL for site CSS style sheet


BUILD DETAILS
-------------
See BUILD_CONAN.COM procedure.


COPYRIGHT
---------
Copyright (C) 1996-2020 Mark G.Daniel
This program, comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it under the
conditions of the GNU GENERAL PUBLIC LICENSE, version 3, or any later version.
http://www.gnu.org/licenses/gpl.txt


VERSION HISTORY (update SOFTWAREVN as well!)
---------------
31-AUG-2020  MGD  v4.1.2, bugfix; eliminate &referer= XSS vulnerability
10-FEB-2020  MGD  v4.1.1, put constraints on some parameters
28-JUL-2015  MGD  v4.1.0, why not HEAD as it's just a foreshortened GET?
04-OCT-2014  MGD  v4.0.0, a nod to the twenty-first century
30-DEC-2008  MGD  v3.7.5, bugfix; OTS Bug?  See notes in code below.
10-MAY-2005  MGD  v3.7.4, SWS 2.0 ignore query string components supplied as
                          command-line parameters differently to CSWS 1.2/3
23-DEC-2003  MGD  v3.7.3, minor conditional mods to support IA64
12-APR-2003  MGD  v3.7.2, link colour changed to 0000cc
20-AUG-2002  MGD  v3.7.1, allow for HTTP/1.1 'Cache-control:' field used
                          by Mozilla variants (instead of HTTP/1.0 'pragma:')
17-AUG-2002  MGD  v3.7.0, changed help module formatting that absorbed
                          all leading white-space destroying intentional
                          formatting (surprised someone hasn't complained
                          about it long before this - thanks Dave Pampreen.),
                          extract a help module as a plain-text page
                          (click on the final 'e' in 'explode'),
                          some accomodations for CSWS v1.2,
                          bugfix; allow for strings with space separated
                          keywords as help keywords in OpenHelpModule()
01-JUL-2001  MGD  v3.6.1, add 'SkipParameters' for direct OSU support
28-OCT-2000  MGD  v3.6.0, use CGILIB object module
11-APR-2000  MGD  v3.5.0, support extended file specifications (ODS-5),
                          bugfix; eliminate extraneous comma in keyword list
07-AUG-1999  MGD  v3.4.0, use more of the CGILIB functionality
29-JUN-1999  MGD  v3.3.0, optional module name list of one per line
                          (rather than alphabetically grouped)
24-APR-1999  MGD  v3.2.0, use CGILIB.C,
                          standard CGI environment (Netscape FastTrack)
02-OCT-1998  MGD  v3.1.0, provide content-type "; charset=..."
08-AUG-1998  MGD  v3.0.3, OSU output processing reworked,
                          bugfix; TimeSetTimezone() 'Seconds' unsigned->signed
06-AUG-1998  MGD  v3.0.2, accomodation for OSU ... reduce HTTP response
                          header carriage control from <CR><LF> to <LF> only
                          (OSU/IE4 combination problematic)
01-AUG-1998  MGD  v3.0.1, suppress table background colours if empty,
                          accomodations for OSU environment
29-APR-1998  MGD  v3.0.0, remove need for WASD MapUrl() functions
                          (making it a GENERIC CGI script!!),
                           some tidying up and cosmetic changes
26-FEB-1998  MGD  v2.9.2, added HTML table around title in help library,
                          TimeSetGmt() modified to be in line with HTTPd
19-AUG-1997  MGD  v2.9.1, MapUrl() to MapUrl_Map() for conditional mapping
20-JUL-1997  MGD  v2.9.0, added /BODY= qualifier (changed background colour)
20-JUN-1997  MGD  v2.8.0, compatible with standard and CGIplus environments,
                          "Pragma: no-cache" now overrides "If-Modified-Since:"
14-MAR-1997  MGD  v2.7.1, bugfix; absorbtion of leading white-space
07-AUG-1996  MGD  v2.7.0, allow help topic to be specified in path, for example
                          "/conan/run/process" or "/help/delete/file"
16-MAY-1996  MGD  v2.6.2, changed XBMs to tranparaent GIFs
23-FEB-1996  MGD  v2.6.1, bugfix, after modifying the HTTPD$GMT format for the
                          HTTPd server I had forgotten about some scripts
12-OCT-1995  MGD  v2.6.0, added 'Referer:', 'Last-Modified:'/'If-Modified-Since:'
18-AUG-1995  MGD  v2.5.0, added "search" form at appropriate places
20-JUN-1995  MGD  v2.4.0, added "explode" feature, lists all help levels below
                          the key specified in a single page
24-MAY-1995  MGD  v2.3.0, minor changes for AXP compatibility
31-MAR-1995  MGD  v2.2.0, ongoing maintenance
02-FEB-1995  MGD  v2.1.0, add "Conan the Librarian" icon
13-DEC-1994  MGD  v2.0.0, made CGI compliant
09-JUL-1994  MGD  v1.0.0, initial development
*/
/*****************************************************************************/

#define SOFTWAREVN "4.1.2"
#define SOFTWARENM "CONAN"
#define SOFTWARECR "Copyright (C) 1996-2020 Mark G.Daniel"
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

/* VMS related header files */
#include <descrip.h>
#include <iodef.h>
#include <jpidef.h>
#include <libdef.h>
#include <libdtdef.h>
#include <lbrdef.h>
#include <lhidef.h>
#include <lnmdef.h>
#include <rms.h>
#include <ssdef.h>
#include <stsdef.h>
#include <syidef.h>

/* application related header files */
#include "enamel.h"
#include <cgilib.h>

#ifndef __VAX
#   pragma nomember_alignment
#endif

#ifndef __VAX
#  ifndef NO_ODS_EXTENDED
#     define ODS_EXTENDED 1
      /* this is smaller than the technical maximum, but still quite large! */
#     define ODS_MAX_FILE_NAME_LENGTH 511
#     define ODS_MAX_FILESYS_NAME_LENGTH 264
#  endif
#endif
#define ODS2_MAX_FILE_NAME_LENGTH 255
#ifndef ODS_MAX_FILE_NAME_LENGTH
#  define ODS_MAX_FILE_NAME_LENGTH ODS2_MAX_FILE_NAME_LENGTH
#endif
#if ODS_MAX_FILE_NAME_LENGTH < ODS2_MAX_FILE_NAME_LENGTH
#  define ODS_MAX_FILE_NAME_LENGTH ODS2_MAX_FILE_NAME_LENGTH
#endif

#define DEFAULT_STYLE "\
body { margin:0.5em; background-color:white; color:black; \
font-family:sans-serif; }\n\
a { text-decoration:none; color:black; \
border:1px solid slategray; border-radius:3px; padding:1px 5px 1px 5px; }\n\
.header { padding:0.5em 0.5em 0.5em 1em; background-color: gainsboro; \
border:1px solid lightslategray; border-radius:1px; \
font-size: 140%; font-weight: bold; }\n\
.helpmodules, .textmodules { width:90%; margin:1.0em 0 1.0em 1.4em; }\n\
.helpmodules a, .textmodules a { line-height:1.7em; \
background-color:whitesmoke; \
white-space:nowrap; margin-right:0.1em; }\n\
.helpmodules p, .textmodules p { margin:0.6em 0 0.5em 0; }\n\
.searchform { margin:1em; }\n\
.results { margin:1.5em 2em 1em 2em; }\n\
.libcontent { margin:1em; }\n\
.libcontent a { background-color:whitesmoke; white-space:nowrap; }\n\
.addinfo { margin:1em; max-width:50em; }\n\
.addinfo a { line-height:1.7em; white-space:nowrap; margin-right:0.1em; }\n\
.module { white-space:pre; }\n\
.buttonbar { padding:0.7em 1em 0.7em 1em; \
border:1px solid lightslategray; border-radius:1px; \
background-color: gainsboro; min-height:1.3em; }\n\
.buttonbar a { text-decoration:none; color:inherit; \
border:1px solid slategray; border-radius:3px; padding:1px 5px 2px 5px; }\n\
.explode { padding-left:4em; font-size:70%; }\n\
.explevel { font-weight:bold; font-size:105% }\n\
.explevel1 { font-weight:bold; font-size:120% }\n\
.helpresult { white-space:nowrap; }\n\
.textresult { font-family:monospace; white-space:nowrap; }\n\
.highlight { border: 1px dotted slategray; \
padding:0 2px 0 2px; background-color:yellow; }\n\
.searchstats { font-size:90%; }\n\
"

#define boolean int
#define true 1
#define false 0
 
#define VMSok(x) ((x) & STS$M_SUCCESS)
#define VMSnok(x) !(((x) & STS$M_SUCCESS))

#define FI_LI __FILE__, __LINE__

#define DEFAULT_LIBRARY "SYS$HELP:HELPLIB.HLB"

#define DEFAULT_BUTTONS "&#8630;Back=javascript:parent.history.go(-1)$\
Close$^Help=/conan/-/conanhelp.html"

char  Utility [] = "CONAN";

boolean  Debug,
         CliListSingle,
         DoHeaderInformation,
         DoListLibraries,
         DoLibraryMenu,
         DoLibraryOpen,
         DoSearch,
         DoSearchStatistics,
         ExplodeHelp,
         ExtractHelp,
         MethodGet,
         MethodHead,
         KeyNameWildcard,
         ListSingle,
         OsuEnvironment,
         StdCgiEnvironment,
         TimeAheadOfGmt;
         
int  SearchHitCount,
     KeyWordCount,
     OdsExtended,
     RecordsSearched,
     SearchStringLength;

unsigned long  LibraryIndex,
               IndexNumber;

unsigned long  IfModifiedSinceBinaryTime [2],
               TimeGmtDeltaBinary [2];

char  ContentTypeCharset [64],
      DefaultButtons [] = DEFAULT_BUTTONS,
      DefaultStyle [] = DEFAULT_STYLE,
      HtmlFormTitle [256],
      HtmlLibraryTitle [256],
      HtmlReferer [512],
      HtmlSearchString [512],
      LastModifiedGmDateTime [32],
      LibraryDirPath [ODS_MAX_FILE_NAME_LENGTH+1],
      LibraryFileName [ODS_MAX_FILE_NAME_LENGTH+1],
      LibraryPathInfo [ODS_MAX_FILE_NAME_LENGTH+1],
      LibrarySpec [ODS_MAX_FILE_NAME_LENGTH+1],
      LibraryTitle [256],
      TimeGmtString [32],
      TimeGmtVmsString [32],
      UnixDateTime [64],
      UriFormTitle [256],
      UriReferer [512];

char  *ButtonsPtr = DefaultButtons,
      *CgiHttpIfModifiedSincePtr,
      *CgiHttpPragmaPtr,
      *CgiHttpCacheControlPtr,
      *CgiLibEnvironmentPtr,
      *CgiPathInfoPtr,
      *CgiPathTranslatedPtr,
      *CgiRequestMethodPtr,
      *CgiScriptNamePtr,
      *CgiServerSoftwarePtr,
      *CgiFormDoPtr,
      *CgiFormExplodePtr,
      *CgiFormExtractPtr,
      *CgiFormKeyPtr,
      *CgiFormRefererPtr,
      *CgiFormTitlePtr,
      *CgiFormSearchPtr,
      *CgiFormSinglePtr,
      *CliCharsetPtr,
      *CharsetPtr,
      *StyleSheetPtr = "";

/* element 0 is not used, keywords number from 1 to 10 */
char  KeyWord [11][256],
      KeyWordHtml [11][256],
      KeyName [11][256],
      KeyNameHtml [11][256],
      SoftwareCopy [] = SOFTWARECR,
      SoftwareID [48];
      
struct lhidef  LibraryHeader;

struct {
   unsigned long  lo32;
   unsigned long  hi32;
} ModuleRFA;

/* CGIplus specific */
int  IsCgiPlus,
     CgiPlusUsageCount;

/* required function prototypes */
char* ButtonBarButton (char*, char*);
char* SearchText (char*, char*, boolean);
int ListHelpModule (struct dsc$descriptor_s*, void*);
int ListTextModule (struct dsc$descriptor_s*, void*);
char* MungeUnderScores (char*);
char* FormatRequestKeys (boolean, int);
int ExtractHelpModule (struct dsc$descriptor_s*, void*);
int OpenHelpModule (struct dsc$descriptor_s*, void*);
int OpenTextModule (struct dsc$descriptor_s*, void*);
int SearchHelpModule (struct dsc$descriptor_s*, void*);
int SearchTextModule (struct dsc$descriptor_s*, void*);
struct dsc$descriptor_s* KeyNameDsc ();
char* XSSuspect (char*);

/*****************************************************************************/
/*
'argc' and 'argv' are only required to support CgiLibEnvironmentInit(), in
particular the OSU environment.
*/

main
(
int argc,
char *argv[]
)
{
   int  idx,
        status;
   char  *cptr, *sptr;

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

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

   if (getenv ("CONAN$DBUG")) Debug = true;
   CgiLibEnvironmentSetDebug (Debug);
   if (Debug) fputs ("Content-Type: text/plain\n\n", stdout);

   CgiLibEnvironmentInit (argc, argv, false);
   CgiLibEnvironmentPtr = CgiLibEnvironmentName();

   GetParameters ();

   CgiLibResponseSetCharset (CliCharsetPtr);
   CgiLibResponseSetSoftwareID (SoftwareID);
   CgiLibResponseSetErrorMessage ("Reported by Conan The Librarian");

   IsCgiPlus = CgiLibEnvironmentIsCgiPlus ();

#ifdef ODS_EXTENDED
   OdsExtended = (GetVmsVersion() >= 72);
   ENAMEL_NAML_SANITY_CHECK
#endif /* ODS_EXTENDED */

   if (IsCgiPlus)
   {
      for (;;)
      {
         /* block waiting for the next request */
         CgiLibVar ("");
         CgiPlusUsageCount++;
         if (Debug) fputs ("Content-Type: text/plain\n\n", stdout);
         ConanTheLibrarian ();
         CgiLibCgiPlusEOF ();
      }
   }
   else
      ConanTheLibrarian ();

   exit (SS$_NORMAL);
}

/*****************************************************************************/
/*
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 [256];
   static unsigned long  Flags = 0;

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

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

   if (!(clptr = getenv ("CONAN$PARAM")))
   {
      /* 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 [C]SWS (VMS Apache) */
   if (CgiLibEnvironmentIsApache())
   {
      /* CSWS 1.2/3 look for something non-space outside of quotes */
      for (cptr = clptr; *cptr; cptr++)
      {
         if (isspace(*cptr)) continue;
         if (*cptr != '\"') break;
         cptr++;
         while (*cptr && *cptr != '\"') cptr++;
         if (*cptr) cptr++;
      }
      /* CSWS 1.2/3 if nothing outside of quotes then ignore command line */
      if (!*cptr) return;
      /* SWS 2.0 doesn't begin with /APACHE from DCL procedure wrapper */
      if (!strsame (cptr, "/APACHE", 7)) return;
   }

   /* if OSU environment then skip P1, P2, P3 */
   if (CgiLibEnvironmentIsOsu())
      SkipParameters = 3;
   else
      SkipParameters = 0;

   aptr = NULL;
   ch = *clptr;
   for (;;)
   {
      if (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, "/APACHE", 4))
      {
         /* just skip this marker for command-line parameters under SWS 2.0 */
         continue;
      }
      if (strsame (aptr, "/BUTTONS=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         if (*cptr == '+')
         {
            /* append buttons to defaults */
            aptr = calloc (1, strlen(ButtonsPtr)+strlen(cptr)+2);
            strcpy (aptr, ButtonsPtr);
            strcat (aptr, "$");
            strcat (aptr, cptr+1);
            ButtonsPtr = aptr;
         }
         else
            ButtonsPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/CHARSET=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         CliCharsetPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/DBUG", -1))
      {
         Debug = true;
         continue;
      }
      if (strsame (aptr, "/ODS5", 5))
      {
         OdsExtended = true;
         continue;
      }
      if (strsame (aptr, "/NOODS5", 7))
      {
         OdsExtended = false;
         continue;
      }
      if (strsame (aptr, "/SINGLE", -1))
      {
         CliListSingle = true;
         continue;
      }
      if (strsame (aptr, "/STYLE=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         StyleSheetPtr = cptr;
         continue;
      }

      if (*aptr != '/')
      {
         fprintf (stdout, "%%%s-E-MAXPARM, too many parameters\n \\%s\\\n",
                  Utility, aptr);
         exit (STS$K_ERROR | STS$M_INHIB_MSG);
      }
      else
      {
         fprintf (stdout, "%%%s-E-IVQUAL, unrecognized qualifier\n \\%s\\\n",
                  Utility, aptr+1);
         exit (STS$K_ERROR | STS$M_INHIB_MSG);
      }
   }
}

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

ButtonBar (int Top1Bottom2)

{
   char  *cptr;

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

   if (Debug) fprintf (stdout, "ButtonBar() %d\n", Top1Bottom2);

   fprintf (stdout, "<div class=\"buttonbar\">");

   cptr = ButtonsPtr;

   /* back */
   cptr = ButtonBarButton (cptr, "");

   /* close */
   if (CgiFormRefererPtr[0])
      cptr = ButtonBarButton (cptr, HtmlReferer);
   else
      cptr = ButtonBarButton (cptr, NULL);

   /* further buttons (starting with "help") */
   while (*cptr) cptr = ButtonBarButton (cptr, NULL);

   fprintf (stdout, "</div>\n");
}

/*****************************************************************************/
/*
Generate a single "button" inside the context created by ButtonBar().
*/

char* ButtonBarButton
(
char *ButtonLabel,
char *ButtonPath
)
{
   char  *cptr, *sptr, *tptr, *uptr;

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

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

   if (!*ButtonLabel)
      for (cptr = ButtonLabel = "*ERROR*"; *cptr; cptr++);
   else
      for (cptr = ButtonLabel; *cptr && *cptr != '=' && *cptr != '$'; cptr++);

   if (*ButtonLabel == '^')
   {
      ButtonLabel++;
      tptr = " target=\"_blank\"";
   }
   else
      tptr = "";

   if (*cptr == '=')
      for (sptr = uptr = cptr+1; *sptr && *sptr != '$'; sptr++);
   else
   if (ButtonPath)
      sptr = (uptr = ButtonPath) + strlen(ButtonPath);
   else
      sptr = (uptr = "*ERROR*") + 8;

   if (!memcmp(uptr,"javascript:", 11))
      fprintf (stdout,
"<script type=\"text/javascript\">\
document.write(\'<a%s href=\"%*.*s\">%*.*s</a>\\n\');\
</script>\n",
               tptr, sptr-uptr, sptr-uptr, uptr,
               cptr-ButtonLabel, cptr-ButtonLabel, ButtonLabel);
      else
         fprintf (stdout, "<a%s href=\"%*.*s\">%*.*s</a>\n",
                  tptr, sptr-uptr, sptr-uptr, uptr,
                  cptr-ButtonLabel, cptr-ButtonLabel, ButtonLabel);

   while (*cptr && *cptr != '$') cptr++;
   while (*cptr && *cptr == '$') cptr++;

   return (cptr);
}

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

ConanTheLibrarian ()

{
   int  idx, status;
   unsigned long  UnixTime;
   char  *cptr, *sptr;
   struct tm  *UnixTmPtr;

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

   time (&UnixTime);
   UnixTmPtr = localtime (&UnixTime);
   if (!strftime (UnixDateTime, sizeof(UnixDateTime),
                  "%a, %d %b %Y %T", UnixTmPtr))
      strcpy (UnixDateTime, "[error]");
   if (Debug) fprintf (stdout, "UnixDateTime |%s|\n", UnixDateTime);

   /* initialize anything specifically required for CGIplus */
   DoHeaderInformation = DoListLibraries = DoLibraryMenu = DoSearch =
      KeyNameWildcard = ExplodeHelp = ExtractHelp = ListSingle = false;
   DoSearchStatistics = true;
   for (idx = 0; idx < 11; idx++)
      KeyWord[idx][0] = KeyWordHtml[idx][0] = 
         KeyName[idx][0] = KeyNameHtml[idx][0] = '\0';

   CgiServerSoftwarePtr = CgiLibVar ("WWW_SERVER_SOFTWARE");
   CgiRequestMethodPtr = CgiLibVar ("WWW_REQUEST_METHOD");
   MethodGet = MethodHead = false;
   if (!(MethodGet = (strcmp (CgiRequestMethodPtr, "GET") == 0)) &&
       !(MethodHead = (strcmp (CgiRequestMethodPtr, "HEAD") == 0)))
   {
      CgiLibResponseHeader (501, "text/html");
      fprintf (stdout, "Not implemented!\n");
      return;
   }

   CgiPathInfoPtr = CgiLibVar ("WWW_PATH_INFO");
   CgiPathTranslatedPtr = CgiLibVar ("WWW_PATH_TRANSLATED");
   CgiScriptNamePtr = CgiLibVar ("WWW_SCRIPT_NAME");
   CgiHttpPragmaPtr = CgiLibVar ("WWW_HTTP_PRAGMA");
   CgiHttpCacheControlPtr = CgiLibVar ("WWW_HTTP_CACHE_CONTROL");
   CgiHttpIfModifiedSincePtr = CgiLibVar ("WWW_HTTP_IF_MODIFIED_SINCE");

   CgiFormDoPtr = CgiLibVar ("WWW_FORM_DO");
   CgiFormExplodePtr = CgiLibVar ("WWW_FORM_EXPLODE");
   CgiFormExtractPtr = CgiLibVar ("WWW_FORM_EXTRACT");
   CgiFormSinglePtr = CgiLibVar ("WWW_FORM_SINGLE");
   CgiFormKeyPtr = CgiLibVar ("WWW_FORM_KEY");
   CgiFormTitlePtr = CgiLibVar ("WWW_FORM_TITLE");

   CgiFormSearchPtr = CgiLibVar ("WWW_FORM_SEARCH");
   if (!CgiFormSearchPtr[0])
      CgiFormSearchPtr = CgiLibVar ("WWW_KEY_1");

   CgiFormRefererPtr = CgiLibVar ("WWW_FORM_REFERER");
   if (!CgiFormRefererPtr[0])
      CgiFormRefererPtr = CgiLibVar ("WWW_HTTP_REFERER");

   /* OWASP ZAP demonstrates unusual parameters can cause spits */
   if (strlen (CgiPathInfoPtr) > 255 ||
       strlen (CgiPathTranslatedPtr) > 255 ||
       strlen (CgiFormKeyPtr) > 255 ||
       strlen (CgiFormRefererPtr) > 255 ||
       strlen (CgiFormSearchPtr) > 255 ||
       strlen (CgiFormTitlePtr) > 255)
   {
      CgiLibResponseError (FI_LI, 0, "Parameter (significantly) out of range.");
      return;
   }

   /* if a bit suspect (XSS) then give it the chop! (q&d) */
   if (cptr = XSSuspect (CgiFormRefererPtr))
   {
      CgiLibResponseError (FI_LI, 0, cptr);
      return;
   }
   if (CgiFormRefererPtr[0])
   {
      /* re-escape the URL-escaped percentages */
      CgiLibUrlEncode (CgiFormRefererPtr, -1, UriReferer, sizeof(UriReferer));
      CgiLibHtmlEscape (CgiFormRefererPtr, -1, HtmlReferer, sizeof(HtmlReferer));
   }
   else
      UriReferer[0] = HtmlReferer[0] = '\0';

   if (VMSnok (status = TimeSetGmt ()))
   {
      if (status != SS$_NOLOGNAM)
      {
         CgiLibResponseError (FI_LI, status, CgiScriptNamePtr);
         exit (SS$_NORMAL);
      }
   }

   if (CgiHttpIfModifiedSincePtr[0])
   {
      if (VMSnok (HttpGmTime (CgiHttpIfModifiedSincePtr, 
                              &IfModifiedSinceBinaryTime)))
      {
         if (Debug) fprintf (stdout, "If-Modified-Since: NBG!\n");
         IfModifiedSinceBinaryTime[0] = IfModifiedSinceBinaryTime[0] = 0;
         CgiHttpIfModifiedSincePtr = "";
      }
   }

   strcpy (LibraryPathInfo, CgiPathInfoPtr);
   if (Debug) fprintf (stdout, "|%s|\n", LibraryPathInfo);

   /**************/
   /* initialize */
   /**************/

   idx = 0;
   if (CgiFormKeyPtr[0])
   {
      /*****************************************/
      /* get keywords from "key=" query string */
      /*****************************************/

      cptr = CgiFormKeyPtr;
      while (*cptr)
      {
         if (idx <= 10) idx++;
         if (*cptr == '~') cptr++;
         sptr = KeyWord[idx];
         while (*cptr && *cptr != '~') *sptr++ = *cptr++;
         *sptr = '\0';
         CgiLibHtmlEscape (KeyWord[idx], -1, KeyWordHtml[idx], -1);
         if (Debug)
            fprintf (stdout, "KeyWord[%d] (form) |%s|%s|\n",
                     idx, KeyWord[idx], KeyWordHtml[idx]);
      }
   }
   else
   if (!strstr (CgiPathTranslatedPtr, ".HLB") &&
       !strstr (CgiPathTranslatedPtr, ".hlb") &&
       !strstr (CgiPathTranslatedPtr, ".TLB") &&
       !strstr (CgiPathTranslatedPtr, ".tlb") &&
       !strchr (CgiPathTranslatedPtr, '*') &&
       !strchr (CgiPathTranslatedPtr, '%'))
   {
      /*******************************/
      /* get keywords from path info */
      /*******************************/

      /* make sure the default help library is used */
      CgiPathTranslatedPtr = "";

      idx = 0;
      /* by comparing, scan past the script component of the path */
      sptr = CgiScriptNamePtr;
      cptr = CgiPathInfoPtr;
      while (toupper(*cptr) == toupper(*sptr)) { cptr++; sptr++; }
      while (*cptr)
      {
         if (idx <= 10) idx++;
         if (*cptr == '/') cptr++;
         sptr = KeyWord[idx];
         if (*cptr == '_' || *cptr == '/')
         {
            /*
               Some other HTML help systems use an underscore for qualifiers.
               This implementation also allows two concurrent forward-slashes
               so that the qualifier may be entered literally!
            */
            *sptr++ = '/';
            cptr++;
         }
         while (*cptr && *cptr != '/') *sptr++ = *cptr++;
         *sptr = '\0';
         CgiLibHtmlEscape (KeyWord[idx], -1, KeyWordHtml[idx], -1);
         if (Debug)
            fprintf (stdout, "KeyWord[%d] (path) |%s|%s|\n",
                     idx, KeyWord[idx], KeyWordHtml[idx]);
      }
      /* finished with the path if we've used it for keywords! */
      CgiPathInfoPtr = "";
      LibraryPathInfo[0] = '\0';
   }            
   KeyWordCount = idx;
   while (idx++ < 10) KeyWord[idx][0] = KeyWordHtml[idx][0] = '\0';

   /* isolate the directory component of the path */
   strcpy (LibraryDirPath, LibraryPathInfo);
   for (cptr = LibraryDirPath; *cptr; cptr++);
   if (cptr > LibraryDirPath) cptr--;
   while (cptr > LibraryDirPath && *cptr != '/') cptr--;
   if (*cptr == '/') cptr++;
   *cptr = '\0';
   if (Debug) fprintf (stdout, "|%s|\n", LibraryDirPath);

   strcpy (LibrarySpec, CgiPathTranslatedPtr);
   for (sptr = LibrarySpec; *sptr && *sptr != '*' && *sptr != '%'; sptr++);
   if (*sptr) DoListLibraries = true;

   switch (tolower(CgiFormDoPtr[0]))
   {
      case 'h' :  DoHeaderInformation = true;  break;
      case 'l' :  DoListLibraries = true;  break;
      case 'm' :  DoLibraryMenu = true;  break;
      case 'o' :  DoLibraryOpen = true;  break;
      case 's' :  DoSearch = true;  break;
      default :
         for (cptr = CgiPathTranslatedPtr; *cptr; cptr++)
            if (*cptr == '*' || *cptr == '%') DoListLibraries = true;
   }

   if (CgiFormExplodePtr[0])
      ExplodeHelp = true;
   else
      ExplodeHelp = false;

   if (CgiFormExtractPtr[0])
      ExtractHelp = true;
   else
      ExtractHelp = false;

   if (CgiFormSinglePtr[0] || CliListSingle)
      ListSingle = true;
   else
      ListSingle = false;

   if (CgiFormSearchPtr[0])
   {
      DoSearch = true;
      SearchStringLength = strlen(CgiFormSearchPtr);
      CgiLibHtmlEscape (CgiFormSearchPtr, -1, HtmlSearchString, -1);
   }
   if (DoSearch && !CgiFormSearchPtr[0])
   {
      CgiLibResponseError (FI_LI, 0, "Search string not supplied.");
      return;
   }

   if (!KeyWord[1][0])
   {
      /* no module keyname supplied, wildcard to list all modules */
      strcpy (KeyWord[1], "*");
      KeyNameWildcard = true;
   }
   else
   {
      /* check for a wildcard in the keyname, list all modules if there is */
      for (sptr = KeyWord[1]; *sptr && *sptr != '*' && *sptr != '%'; sptr++);
      if (*sptr) KeyNameWildcard = true;
   }

   LibraryTitle[0] = '\0';
   if (!CgiPathTranslatedPtr[0]) CgiFormTitlePtr = "VMS Help";
   if (CgiFormTitlePtr[0]) strcpy (LibraryTitle, CgiFormTitlePtr);
   if (LibraryTitle[0])
      CgiLibHtmlEscape (LibraryTitle, -1, HtmlLibraryTitle, -1);
   else
   {
      sprintf (LibraryTitle, "Library %s", LibraryPathInfo);
      CgiLibHtmlEscape (LibraryTitle, -1, HtmlLibraryTitle, -1);
   }
   if (Debug) fprintf (stdout, "LibraryTitle |%s|\n", LibraryTitle);

   CgiLibUrlEncode (LibraryTitle, -1, UriFormTitle, -1);
   CgiLibHtmlEscape (LibraryTitle, -1, HtmlFormTitle, -1);

   /***********/
   /* process */
   /***********/

   if (DoListLibraries)
      ListLibraries ();
   else
   if (DoLibraryMenu)
      LibraryMenu ();
   else
   if (DoLibraryOpen)
      Librarian ();
   else
      Librarian ();
 }

/****************************************************************************/
/*
Search for library files according to the the supplied specification (defaults 
to help libraries; SYS$HELP:*.HLB).  Display the name of each library in lower 
case as a list item.
*/

ListLibraries ()

{
   boolean  SupportedLibraryType;
   int  len, status,
        FileCount = 0;
   char  PrevFirstChar = '\0';
   char  *cptr, *sptr;
   char  ResFileName [ODS_MAX_FILE_NAME_LENGTH+1],
         ExpFileName [ODS_MAX_FILE_NAME_LENGTH+1],
         String [2048],
         UrlEncoded [ODS_MAX_FILE_NAME_LENGTH+64+1];
   struct FAB  FileFab;
#ifdef ODS_EXTENDED
   struct NAML  FileNaml;
#endif /* ODS_EXTENDED */
   struct NAM  FileNam;

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

   /* if no library specifcation provided then default to help libraries */
   if (!LibrarySpec[0]) strcpy (LibrarySpec, "SYS$HELP:*.HLB;0");

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

   FileFab = cc$rms_fab;
   FileFab.fab$l_fop = FAB$V_NAM;
   FileFab.fab$l_nam = &FileNam;

#ifdef ODS_EXTENDED
   if (OdsExtended)
   {
      FileFab.fab$l_fna = (char*)-1;
      FileFab.fab$b_fns = 0;
      FileFab.fab$l_nam = (struct namdef*)&FileNaml;

      ENAMEL_RMS_NAML(FileNaml)
      FileNaml.naml$l_long_filename = LibrarySpec;
      FileNaml.naml$l_long_filename_size = strlen(LibrarySpec);
      FileNaml.naml$l_long_expand = ExpFileName;
      FileNaml.naml$l_long_expand_alloc = sizeof(ExpFileName)-1;
      FileNaml.naml$l_long_result = ResFileName;
      FileNaml.naml$l_long_result_alloc = sizeof(ResFileName)-1;
   }
   else
#endif /* ODS_EXTENDED */
   {
      FileFab.fab$l_fna = LibrarySpec;
      FileFab.fab$b_fns = strlen(LibrarySpec);
      FileFab.fab$l_nam = &FileNam;

      FileNam = cc$rms_nam;
      FileNam.nam$l_esa = ExpFileName;
      FileNam.nam$b_ess = ODS2_MAX_FILE_NAME_LENGTH;
      FileNam.nam$l_rsa = ResFileName;
      FileNam.nam$b_rss = ODS2_MAX_FILE_NAME_LENGTH;
   }

   if (VMSnok (status = sys$parse (&FileFab, 0, 0)))
   {
      CgiLibResponseError (FI_LI, status, HtmlLibraryTitle);
      return (SS$_NORMAL);
   }

   BeginPage (LibraryPathInfo, LibraryPathInfo);

   fprintf (stdout, "<div class=\"results\">\n");

   while (VMSok (status = sys$search (&FileFab, 0, 0)))
   {
      FileCount++;

#ifdef ODS_EXTENDED
      if (OdsExtended)
      {
         if (strsame(FileNaml.naml$l_long_type, ".DIR;", 5)) continue;
      }
      else
#endif /* ODS_EXTENDED */
         if (strsame(FileNam.nam$l_type, ".DIR;", 5)) continue;

#ifdef ODS_EXTENDED
      if (OdsExtended)
      {
         cptr = FileNaml.naml$l_long_name;
         if (strsame (FileNaml.naml$l_long_type, ".HLB;", 5) ||
             strsame (FileNaml.naml$l_long_type, ".TLB;", 5))
            SupportedLibraryType = true;
         else
            SupportedLibraryType = false;
      }
      else
#endif /* ODS_EXTENDED */
      {
         cptr = FileNam.nam$l_type;
         if (strsame (FileNam.nam$l_type, ".HLB;", 5) ||
             strsame (FileNam.nam$l_type, ".TLB;", 5))
            SupportedLibraryType = true;
         else
            SupportedLibraryType = false;
      }

      if (!SupportedLibraryType) continue;

      sptr = String;
      if (PrevFirstChar && tolower(*cptr) != tolower(PrevFirstChar))
         sptr += sprintf (sptr, "<p>");
      PrevFirstChar = *cptr;

      if (SupportedLibraryType)
      {
#ifdef ODS_EXTENDED
         if (OdsExtended)
         {
            CgiLibUrlEncode (LibraryDirPath, -1, UrlEncoded, -1);
            sptr += sprintf (sptr, "<a href=\"%s%s",
                             CgiScriptNamePtr, UrlEncoded);
            for (cptr = FileNaml.naml$l_long_name;
                 cptr < FileNaml.naml$l_long_ver;
                 *sptr++ = *cptr++);
         }
         else
#endif /* ODS_EXTENDED */
         {
            sptr += sprintf (sptr, "<a href=\"%s%s",
                             CgiScriptNamePtr, LibraryDirPath);
            for (cptr = FileNam.nam$l_name;
                 cptr < FileNam.nam$l_ver;
                 *sptr++ = tolower(*cptr++));
         }
         sptr += sprintf (sptr, "?do=menu&title=%s&referer=%s\">",
                          UriFormTitle, UriReferer);
      }

      /* if there was wildcard in the file type then include it */
#ifdef ODS_EXTENDED
      if (OdsExtended)
      {
         if (FileNaml.naml$l_fnb & NAM$M_WILD_TYPE)
            for (cptr = FileNaml.naml$l_long_name;
                 cptr < FileNaml.naml$l_long_ver;
                 *sptr++ = tolower(*cptr++));
         else
            for (cptr = FileNaml.naml$l_long_name;
                 cptr < FileNaml.naml$l_long_type;
                 *sptr++ = tolower(*cptr++));
      }
      else
#endif /* ODS_EXTENDED */
      {
         if (FileNam.nam$l_fnb & NAM$M_WILD_TYPE)
            for (cptr = FileNam.nam$l_name;
                 cptr < FileNam.nam$l_ver;
                 *sptr++ = tolower(*cptr++));
         else
            for (cptr = FileNam.nam$l_name;
                 cptr < FileNam.nam$l_type;
                 *sptr++ = tolower(*cptr++));
      }

      if (SupportedLibraryType) sptr += sprintf (sptr, "</a>");
      *sptr++ = '\n';
      *sptr = '\0';

      fputs (String, stdout);
   }

   if (!FileCount) fprintf (stdout, "<p>No files found.\n");

   if (status == RMS$_FNF || status == RMS$_NMF) status = SS$_NORMAL;
   if (VMSnok (status))
   {
      CgiLibResponseError (FI_LI, status, HtmlLibraryTitle);
      return (SS$_NORMAL);
   }

   fprintf (stdout, "</div>\n</div>\n");
   ButtonBar (2);
   fprintf (stdout, "</body>\n</html>\n");
   return (status);
}

/*****************************************************************************/
/*
For the specified library file provide a menu of four services; librarian, 
searching, library header and help.
*/ 
 
LibraryMenu ()
 
{
   char  *cptr, *sptr;
   char  UrlEncoded [ODS_MAX_FILE_NAME_LENGTH+64+1];

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

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

   strcpy (LibraryTitle, LibraryPathInfo);
   CgiLibHtmlEscape (LibraryPathInfo, -1, HtmlLibraryTitle, -1);
   CgiLibUrlEncode (LibraryTitle, -1, UriFormTitle, -1);
   CgiLibHtmlEscape (LibraryTitle, -1, HtmlFormTitle, -1);

   BeginPage (LibraryTitle, HtmlLibraryTitle);

   CgiLibUrlEncode (LibraryPathInfo, -1, UrlEncoded, -1);

   fprintf (stdout,
"<div class=\"results\">\n\
<p><a href=\"%s%s?title=%s&referer=%s\">Open Library</a>\n\
<p><a href=\"%s%s?do=header&title=%s&referer=%s\">Library Header</a>\n\
<div class=\"searchform\">\n\
<form action=\"%s%s\">\n\
<input type=\"hidden\" name=\"referer\" value=\"%s\">\n\
<input type=\"hidden\" name=\"title\" value=\"%s\">\n\
<input type=\"hidden\" name=\"do\" value=\"search\">\n\
<input type=\"submit\" value=\"Search for: \">\n\
<input type=\"text\" name=\"Search\">\n\
<input type=\"reset\" value=\"Reset\">\n\
</form>\n\
</div>\n\
</div>\n",
   CgiScriptNamePtr, UrlEncoded, UriFormTitle, UriReferer,
   CgiScriptNamePtr, UrlEncoded, UriFormTitle, UriReferer,
   CgiScriptNamePtr, UrlEncoded,
   HtmlReferer, HtmlFormTitle);

   fprintf (stdout, "</div>\n</div>\n");
   ButtonBar (2);
   fprintf (stdout, "</body>\n</html>\n");
}

/*****************************************************************************/
/*
Initialise library control.  Open the library specified by LibrarySpec.  Get 
header information from the library so its internal format can be determined 
(help or text, other are not supported).  Call ProcessLibrary() to perform the 
requested access according to the library type.
*/ 
 
Librarian ()

{
   int  status;
   unsigned long  Function = LBR$C_READ,
                  FileNameLength;
   char  *cptr;
   char  String [1024];
   $DESCRIPTOR (LibrarySpecDsc, "");
   static struct dsc$descriptor_s 
   LibraryDefDsc = { sizeof(DEFAULT_LIBRARY)-1, DSC$K_DTYPE_T,
                     DSC$K_CLASS_S, DEFAULT_LIBRARY },
   LibraryFileNameDsc = { sizeof(LibraryFileName)-1, DSC$K_DTYPE_T,
                          DSC$K_CLASS_S, LibraryFileName };

   /****************************************/
   /* initialize, open library, get header */
   /****************************************/

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

   if (VMSnok (status = lbr$ini_control (&LibraryIndex, &Function, 0, 0)))
   {
      CgiLibResponseError (FI_LI, status, HtmlLibraryTitle);
      return (SS$_NORMAL);
   }
 
   LibrarySpecDsc.dsc$w_length = strlen(LibrarySpec);
   LibrarySpecDsc.dsc$a_pointer = LibrarySpec;
 
   if (VMSnok (status = lbr$open (&LibraryIndex,
                                  &LibrarySpecDsc,
                                  0,
                                  &LibraryDefDsc,
                                  0,
                                  &LibraryFileNameDsc,
                                  &FileNameLength)))
   {
      if (Debug) fprintf (stdout, "lib$open() %%X%08.08X\n", status);
      CgiLibResponseError (FI_LI, status, HtmlLibraryTitle);
      return (SS$_NORMAL);
   }
 
   /* terminate, removing the trailing version number */
   LibraryFileName[FileNameLength] = '\0';
   for (cptr = LibraryFileName; *cptr && *cptr != ';'; cptr++);
   *cptr = '\0';
   if (Debug) fprintf (stdout, "LibraryFileName |%s|\n", LibraryFileName);

   if (VMSnok (status = lbr$get_header (&LibraryIndex, &LibraryHeader)))
   {
      CgiLibResponseError (FI_LI, status, HtmlLibraryTitle);
      return (SS$_NORMAL);
   }

   if (CgiHttpIfModifiedSincePtr[0])
   {
      if (VMSnok (ModifiedSince (&IfModifiedSinceBinaryTime,
                                 &LibraryHeader.lhi$l_updtim)))
      {
         /* library has not been modified since the date/time, don't send */
         status = lbr$close (&LibraryIndex);
         return (SS$_NORMAL);
      }
   }

   HttpGmTimeString (LastModifiedGmDateTime, &LibraryHeader.lhi$l_updtim);

   /*******************/
   /* process library */
   /*******************/

   if (DoHeaderInformation)
      HeaderInformation ();
   else
   if (LibraryHeader.lhi$l_type == LBR$C_TYP_TXT)
   {
      if (DoSearch)
         SearchTextLibraryModules ();
      else
      if (KeyNameWildcard)
         ListTextLibraryModules ();
      else
         OpenTextLibraryModule ();
   }
   else
   if (LibraryHeader.lhi$l_type == LBR$C_TYP_HLP)
   {
      if (DoSearch)
         SearchHelpLibraryModules ();
      else
      if (KeyNameWildcard)
         ListHelpLibraryModules ();
      else
         OpenHelpLibraryModule ();
   }
   else
   {
      sprintf (String,
      "Library type %d not supported. (Only HELP and TEXT libraries are valid)",
      LibraryHeader.lhi$l_type);
      CgiLibResponseError (FI_LI, 0, String);
      return (SS$_NORMAL);
   }
 
   status = lbr$close (&LibraryIndex);

   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
Provide selected library header information.
*/ 
 
HeaderInformation ()
 
{
   static char  *LibraryTypes[] =
          { "Unknown", "VAX Object", "Macro", "Help", "Text",
            "VAX Sharable Image", "NCS",
            "Alpha Object", "Alpha Sharable Image", "?" };
   static  $DESCRIPTOR (CreatedFaoDsc,
           "<tr><th>Created:</th><td>!%D</td></tr>\n");
   static  $DESCRIPTOR (RevisedFaoDsc,
           "<tr><th>Revised:</th><td>!%D</td></tr>\n");

   unsigned short  slen;
   char  String [1024];
   $DESCRIPTOR (StringDsc, String);

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

   BeginPage (LibraryTitle, LibraryTitle);

   fprintf (stdout,
"<div class=\"results\">\n\
<table class=\"tabhead\" border=\"0\" cellpadding=\"3\" cellspacing=\"0\">\n");

   if (LibraryHeader.lhi$l_type > 8) LibraryHeader.lhi$l_type = 9;
   fprintf (stdout, "<tr><th align=\"right\">Type:</th><td>%s</td></tr>\n",
            LibraryTypes[LibraryHeader.lhi$l_type]);

   fprintf (stdout, "<tr><th align=\"right\">Creator:</th><td>%-*s</td></tr>\n",
            *(char*)LibraryHeader.lhi$t_lbrver,
            (char*)LibraryHeader.lhi$t_lbrver+1);

   fprintf (stdout, "<tr><th align=\"right\">Format:</th><td>%d.%d</td></tr>\n",
            LibraryHeader.lhi$l_majorid, LibraryHeader.lhi$l_minorid);

   sys$fao (&CreatedFaoDsc, &slen, &StringDsc, &LibraryHeader.lhi$l_credat);
   String[slen] = '\0';
   fprintf (stdout, "%s", String);

   sys$fao (&RevisedFaoDsc, &slen, &StringDsc, &LibraryHeader.lhi$l_updtim);
   String[slen] = '\0';
   fprintf (stdout, "%s", String);

   if (LibraryHeader.lhi$l_libstatus)
      fprintf (stdout,
"<tr><th align=\"right\">Status:</th><td>OK</td></tr>\n");
   else
      fprintf (stdout,
"<tr><th align=\"right\">Status:</th>\
<td><span style=\"color:red;\">PROBLEM</span></td></tr>\n");

   fprintf (stdout, "</table>\n");

   fprintf (stdout, "</div>\n</div>\n");
   ButtonBar (2);
   fprintf (stdout, "</body>\n</html>\n");
}

/*****************************************************************************/
/*
*/
 
struct dsc$descriptor_s* KeyNameDsc ()

{
   static char  Key [256];
   static $DESCRIPTOR (KeyDsc, Key);
 
   char  *cptr, *sptr, *zptr;

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

   if (Debug) fprintf (stdout, "KeyNameDsc()\n");
 
   /* although spaces *look* better, in fact underscores */
   zptr = (sptr = Key) + sizeof(Key)-1;
   for (cptr = KeyWord[1]; *cptr && sptr < zptr; cptr++)
   {
      if (*cptr == ' ')
         *sptr++ = '_';
      else
         *sptr++ = toupper(*cptr);
   }
   *sptr = '\0';
   KeyDsc.dsc$w_length = sptr - Key;
   if (Debug) fprintf (stdout, "|%s|\n", Key);

   return (&KeyDsc);
}

/*****************************************************************************/
/*
*/
 
int ListHelpLibraryModules ()

{
   int  status;
 
   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ListHelpLibraryModules()\n");
 
   BeginPage (LibraryTitle, HtmlLibraryTitle);

   fprintf (stdout,
"<div class=\"searchform\">\n\
<form action=\"%s%s\">\n\
<input type=\"hidden\" name=\"referer\" value=\"%s\">\n\
<input type=\"hidden\" name=\"title\" value=\"%s\">\n\
<input type=\"hidden\" name=\"do\" value=\"search\">\n\
<input type=\"submit\" value=\"Search for: \">\n\
<input type=\"text\" name=\"Search\">\n\
<input type=\"reset\" value=\"Reset\">\n\
</form>\n\
</div>\n\
<div class=\"helpmodules\">\n",
   CgiScriptNamePtr, CgiPathInfoPtr,
   HtmlReferer, HtmlFormTitle);

   fflush (stdout);

   IndexNumber = 1;

   if (VMSnok (status = lbr$get_index (&LibraryIndex,
                                       &IndexNumber,
                                       &ListHelpModule,
                                       KeyNameDsc())))
   {
      if (Debug) fprintf (stdout, "lib$get_index() %%X%08.08X\n", status);
      CgiLibResponseError (FI_LI, status, HtmlLibraryTitle);
      return (SS$_NORMAL);
   }

   fprintf (stdout, "\n");

   fprintf (stdout, "</div>\n</div>\n");
   ButtonBar (2);
   fprintf (stdout, "</body>\n</html>\n");
}
 
/*****************************************************************************/
/*
*/ 
 
int ListHelpModule
(
struct dsc$descriptor_s  *KeyNameDscPtr,
void  *RFAptr
)
{
   static char  PrevAlpha = 0,
                PrevChar = 0xff;

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

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

   strncpy (KeyWord[1],
            KeyNameDscPtr->dsc$a_pointer,
            KeyNameDscPtr->dsc$w_length);
   KeyWord[1][KeyNameDscPtr->dsc$w_length] = '\0';
   strcpy (KeyName[1], KeyWord[1]);
   CgiLibHtmlEscape (KeyWord[1], -1, KeyNameHtml[1], -1);

   if (Debug) fprintf (stdout, "KeyNameDsc |%s|\n", KeyWord[1]);

   if (ListSingle)
   {
      if ((unsigned char)KeyName[1][0] >= (unsigned char)PrevChar)
         fprintf (stdout, "\n<li>");
      else
         fprintf (stdout, "<li>");
   }
   else
   if (!isalpha(KeyName[1][0]) ||
       toupper(KeyName[1][0]) == PrevAlpha)
   {
      if ((unsigned char)KeyName[1][0] >= (unsigned char)PrevChar)
         fprintf (stdout, "\n");
   }
   else
   {
      if ((unsigned char)KeyName[1][0] >= (unsigned char)PrevChar)
         fprintf (stdout, "\n<p>");
      PrevAlpha = toupper(KeyName[1][0]);
   }

   fprintf (stdout, "<a href=\"%s%s?key=%s&title=%s&referer=%s\">%s</a>\n",
            CgiScriptNamePtr, LibraryPathInfo,
            FormatRequestKeys(true,1),
            UriFormTitle, UriReferer,
            MungeUnderScores(KeyNameHtml[1]));

   PrevChar = KeyName[1][0];

   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
*/
 
int OpenHelpLibraryModule ()

{
   int  status;
 
   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "OpenHelpLibraryModule()\n");
 
   IndexNumber = 1;

   if (ExtractHelp)
   {
      status = lbr$get_index (&LibraryIndex, &IndexNumber,
                              &ExtractHelpModule, KeyNameDsc());
      if (VMSnok (status))
      {
         if (Debug) fprintf (stdout, "lib$get_index() %%X%08.08X\n", status);
         CgiLibResponseError (FI_LI, status, HtmlLibraryTitle);
      }

      return (SS$_NORMAL);
   }

   status = lbr$get_index (&LibraryIndex, &IndexNumber,
                           &OpenHelpModule, KeyNameDsc());
   if (VMSnok (status))
   {
      if (Debug) fprintf (stdout, "lib$get_index() %%X%08.08X\n", status);
      CgiLibResponseError (FI_LI, status, HtmlLibraryTitle);
      return (SS$_NORMAL);
   }

   fprintf (stdout, "</div>\n");
   ButtonBar (2);
   fprintf (stdout, "</body>\n</html>\n");
   return (SS$_NORMAL);
}
 
/*****************************************************************************/
/*
This is a reasonably complex function, and reflects the complexities internal 
to help library modules!
*/ 
 
int OpenHelpModule
(
struct dsc$descriptor_s  *KeyNameDscPtr,
void  *RFAptr
)
{
   static int  MatchedCount = 0,
               PreviousCgiPlusUsageCount = 0;

   boolean  AdditionalInformation = true,
            ExplodedHeading = false,
            OutputModuleTitle = true;
   int  status,
        AdditionalInformationCount = 0,
        BlankLineCount = 0,
        Count,
        ExplodeLevel = 0,
        HelpLevel = 0,
        Length,
        MatchedLevel = 0,
        MatchesToLevel = 0,
        PreviousHelpLevel = 0,
        TextLineCount = 0;
   int  KeyCount [11];
   char  PrevFirstBufferChar = '\0';
   char  *cptr, *sptr;
   char  Buffer [1024],
         PrevAdditionalChar [2] = "\0\0",
         String [1024];
   $DESCRIPTOR (InBufferDsc, Buffer);
   $DESCRIPTOR (OutBufferDsc, Buffer);
 
   /*********/
   /* begin */
   /*********/

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

   if (CgiPlusUsageCount != PreviousCgiPlusUsageCount)
   {
      MatchedCount = 0;
      PreviousCgiPlusUsageCount = CgiPlusUsageCount;
   }

   strncpy (KeyName[1],
            KeyNameDscPtr->dsc$a_pointer,
            KeyNameDscPtr->dsc$w_length);
   KeyName[1][KeyNameDscPtr->dsc$w_length] = '\0';

   if (Debug) fprintf (stdout, "KeyNameDsc |%s|\n", KeyName[1]);

   CgiLibHtmlEscape (KeyName[1], -1, KeyWordHtml[1], -1);
   if (!KeyWordCount)
   {
      strcpy (KeyWord[1], KeyName[1]);
      KeyWordCount = 1;
   }

   if (ExplodeHelp)
      for (Count = 0; Count < 11; KeyCount[Count++] = 0);

   if (VMSnok (status = lbr$lookup_key (&LibraryIndex,
                                        KeyNameDscPtr,
                                        &ModuleRFA)))
   {
      if (Debug) fprintf (stdout, "lib$lookup_key() %%X%08.08X\n", status);
      CgiLibResponseError (FI_LI, status, HtmlLibraryTitle);
      return (SS$_NORMAL);
   }

   /**************************************/
   /* loop through all records in module */
   /**************************************/

   for (;;)
   {
      status = lbr$get_record (&LibraryIndex,
                               &InBufferDsc,
                               &OutBufferDsc);
      if (Debug) fprintf (stdout, "lbr$get_record() %%X%08.08X\n", status);
      if (VMSnok (status)) break;
 
      /* terminate output buffer, removing any trailing white space */
      cptr = Buffer+OutBufferDsc.dsc$w_length-1;
      while (cptr >= Buffer && isspace(*cptr)) cptr--;
      *++cptr = '\0';
 
      /* comment record (line) in module */
      if (Buffer[0] == '!') continue;

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

      /*
          30-DEC-2008 OTS Bug?  (V7.3-1 and V8.3 at least)
          Use of isdigit() of copyright char 0xa9 resulted in ACCVIO
          LIBOTS          0 00000000000217B0 FFFFFFFF809117B0
          DECC$SHR_EV56   0 00000000000CD0E4 FFFFFFFF80B650E4
          DECC$SHR_EV56   0 000000000014504C FFFFFFFF80BDD04C
          DECC$SHR_EV56   0 0000000000144FAC FFFFFFFF80BDCFAC
          Explicit test of digit range worked around issue.
      */
      if ((Buffer[0] >= '0' && Buffer[0] <= '9' && Buffer[1] == ' ') ||
          Buffer[0] == '/')
      {
         /***************************/
         /* help level or qualifier */
         /***************************/
 
         PreviousHelpLevel = HelpLevel;
         if (Buffer[0] == '/')
         {
            /* if no previous qualifier encountered at this help level */
            /* then the qualifier effectively creates a new help level */
            if (PrevFirstBufferChar != '/') HelpLevel++;
            cptr = Buffer;
            for (sptr = cptr; *sptr && !isspace(*sptr); sptr++);
         }
         else
         {
            /* help level topic (line begins with a digit then a space) */
            HelpLevel = Buffer[0] - '0';
            for (cptr = Buffer + 1; isspace(*cptr); cptr++);
            for (sptr = cptr; *sptr && !isspace(*sptr); sptr++);
         }

         strncpy (KeyName[HelpLevel], cptr, sptr-cptr);
         KeyName[HelpLevel][sptr-cptr] = '\0';
         CgiLibHtmlEscape (cptr, -1, KeyNameHtml[HelpLevel], -1);
         PrevFirstBufferChar = Buffer[0];

         if (MatchesToLevel >= HelpLevel)
         {
            /* if the topic's been matched and output, finish up */
            if (MatchedLevel) break;

            MatchesToLevel = ExplodeLevel = 0;
            for (Count = 1; Count <= HelpLevel; Count++)
            {
               if (strsame (KeyWord[Count], KeyName[Count], -1))
                  MatchesToLevel = Count;
               else
                  break;
            }
         }
         else
         {
            for (Count = MatchesToLevel + 1; Count <= HelpLevel; Count++)
            {
               if (strsame (KeyWord[Count], KeyName[Count], -1))
                  MatchesToLevel = Count;
               else
                  break;
            }
         }

         if (MatchesToLevel == KeyWordCount && MatchesToLevel == HelpLevel)
         {
            /****************/
            /* module title */
            /****************/

            MatchedCount++;

            sptr = String;
            sptr += sprintf (sptr, "%s", LibraryTitle);
            for (Count = 1; Count <= HelpLevel; Count++)
            {
               if (Count > 1)
                  for (cptr = KeyNameHtml[Count-1];
                       *cptr && *cptr != '_' && *cptr != ' ';
                       cptr++);

               if (Count > 1 && *cptr)
                  sptr += sprintf (sptr, ",&thinsp;%s",
                                   MungeUnderScores(KeyNameHtml[Count]));
               else
               if (Count > 1)
                  sptr += sprintf (sptr, "&thinsp;&nbsp;%s",
                                   MungeUnderScores(KeyNameHtml[Count]));
               else
                  sptr += sprintf (sptr, " &nbsp;&mdash;&nbsp; %s",
                                   MungeUnderScores(KeyNameHtml[Count]));
            }

            BeginPage (LibraryTitle, String);

            if (ExplodeHelp)
            {
               ExplodedHeading = true;
               ExplodeLevel = HelpLevel + 1;
            }

            MatchedLevel = HelpLevel;
         }
         else
         if (ExplodeLevel && HelpLevel >= ExplodeLevel)
         {
            /******************/
            /* exploding help */
            /******************/

            if (TextLineCount)
               if (PreviousHelpLevel == ExplodeLevel - 1)
                  fprintf (stdout, "</pre>\n<p>");
               else
                  fprintf (stdout, "</pre>\n");
            TextLineCount = 0;

            ExplodedHeading = true;
            for (Count = HelpLevel+1; Count < 11; KeyCount[Count++] = 0);
            KeyCount[HelpLevel]++;

            fprintf (stdout, "<p><span class=\"explevel%s\">",
                     ExplodeLevel == HelpLevel ? "1" : "");

            for (Count = ExplodeLevel; Count <= HelpLevel; Count++)
               if (Count < HelpLevel)
                  fprintf (stdout, "%d.", KeyCount[Count]);
               else
                  fprintf (stdout, "%d", KeyCount[Count]);

            if (KeyNameHtml[HelpLevel][0] == '/')
               fprintf (stdout, " &nbsp;&nbsp; %s</span>\n",
                        MungeUnderScores(KeyNameHtml[HelpLevel]));
            else
               fprintf (stdout, " &nbsp;&ndash;&nbsp; %s</span>\n",
                        MungeUnderScores(KeyNameHtml[HelpLevel]));
         }
         else
         if (MatchesToLevel == KeyWordCount && HelpLevel == KeyWordCount+1)
         {
            /*************/
            /* subtopics */
            /*************/

            if (AdditionalInformation)
            {
               if (TextLineCount) fprintf (stdout, "</pre>\n");
               if (HelpLevel == 2)
               {
                  fprintf (stdout,
"<div class=\"searchform\">\n\
<form action=\"%s%s\">\n\
<input type=\"hidden\" name=\"referer\" value=\"%s\">\n\
<input type=\"hidden\" name=\"title\" value=\"%s\">\n\
<input type=\"hidden\" name=\"do\" value=\"search\">\n\
<input type=\"hidden\" name=\"key\" value=\"%s\">\n\
<input type=\"submit\" value=\"Search &quot;%s&quot; for: \">\n\
<input type=\"text\" name=\"Search\">\n\
<input type=\"reset\" value=\"Reset\">\n\
</form>\n\
</div>\n",
                           CgiScriptNamePtr, CgiPathInfoPtr,
                           HtmlReferer, HtmlFormTitle,
                           FormatRequestKeys(false,HelpLevel-1),
                           KeyWordHtml[1]);
               }
               fprintf (stdout,
"<b>Additional Information:</b>\n\
<span class=\"explode\">\n\
<a href=\"%s%s?key=%s&explode=yes&title=%s&referer=%s\">explode</a>\n\
<a href=\"%s%s?key=%s&extract=yes\">extract</a>\n\
</span>\n\
<div class=\"addinfo\">\n",
               CgiScriptNamePtr, LibraryPathInfo,
               FormatRequestKeys(true,HelpLevel-1),
               UriFormTitle, UriReferer,
               CgiScriptNamePtr, LibraryPathInfo,
               FormatRequestKeys(true,HelpLevel-1));
               AdditionalInformation = false;
            }
            else
            {
               if (TextLineCount) fprintf (stdout, "</pre>\n");
            }

            if (PrevAdditionalChar[0] ||
                !isalpha(KeyNameHtml[HelpLevel][0]) ||
                islower(KeyNameHtml[HelpLevel][0]))
            {
               if (isalpha(KeyNameHtml[HelpLevel][0]))
               {
                  if (!isalpha(PrevAdditionalChar[0]) ||
                      (isalpha(PrevAdditionalChar[0]) &&
                       KeyNameHtml[HelpLevel][0] != PrevAdditionalChar[0]))
                     fprintf (stdout, "<br>");
               }
               else
               {
                  if (isalpha(PrevAdditionalChar[0]) ||
                      KeyNameHtml[HelpLevel][1] != PrevAdditionalChar[1])
                     fprintf (stdout, "<br>");
               }
            }
            if (PrevAdditionalChar[0] ||
                !isalpha(KeyNameHtml[HelpLevel][0]) ||
                islower(KeyNameHtml[HelpLevel][0]))
            {
               PrevAdditionalChar[0] = KeyNameHtml[HelpLevel][0];
               PrevAdditionalChar[1] = KeyNameHtml[HelpLevel][1];
            }

            fprintf (stdout,
                     "<a href=\"%s%s?key=%s&title=%s&referer=%s\">%s</a>\n",
                     CgiScriptNamePtr, LibraryPathInfo,
                     FormatRequestKeys(true,HelpLevel),
                     UriFormTitle, UriReferer,
                     MungeUnderScores(KeyNameHtml[HelpLevel]));
            AdditionalInformationCount++;
            TextLineCount = 0;
         }
      }
      else
      {
         /*************/
         /* help text */
         /*************/
 
         if ((ExplodeLevel && HelpLevel >= ExplodeLevel) || 
             (MatchesToLevel == KeyWordCount && HelpLevel == KeyWordCount))
         {
            /* check to see if this module record has any non-space chars */
            for (cptr = Buffer;
                 *cptr && (*cptr == ' ' || *cptr == '\t');
                 cptr++);
            /* ignore special directive for DECwindows on-line help */
            if (*cptr && *cptr != '=')
            {
               if (!TextLineCount) fprintf (stdout, "<pre class=\"module\">");
               sptr = String;
               if (ExplodedHeading)
               {
                  ExplodedHeading = false;
                  BlankLineCount = 0;
               }
               if (BlankLineCount && TextLineCount) *sptr++ = '\n';
               if (BlankLineCount) BlankLineCount = 0;
               *sptr++ = ' ';
               cptr = Buffer;
               while (*cptr) *sptr++ = *cptr++;
               *sptr++ = '\n';
               *sptr = '\0';
               HtmlOutputString (String, true);
               TextLineCount++;
            }
            else
            if (!*cptr)
               BlankLineCount++;
         }
      }
   }
 
   if (AdditionalInformationCount)
   {
      ListOtherHelp ();
      fprintf (stdout, "</div>\n");
   }
   else
   if (TextLineCount)
      fprintf (stdout, "</pre>\n");
   else
   if (!MatchedCount)
      return (0x00268838);

   return (SS$_NORMAL);
}
 
/****************************************************************************/
/*
Search for library files with names containing the first keyword.  The idea is
that an in-program help file might exist for additional information, so for the
example of TELNET this function searches for SYS$COMMON:[SYSHLP]*TELNET*.HLB
and lists any matching.  Help topic HELP lists all those
SYS$COMMON:[SYSHLP]*HELP*.HLB :-)
*/

ListOtherHelp ()

{
   int  len, status,
        FileCount = 0;
   char  *cptr, *sptr, *zptr;
   char  HelpSpec [ODS_MAX_FILE_NAME_LENGTH+1],
         ResFileName [ODS_MAX_FILE_NAME_LENGTH+1],
         ExpFileName [ODS_MAX_FILE_NAME_LENGTH+1],
         SimplePath [ODS_MAX_FILE_NAME_LENGTH+1],
         String [2048],
         UrlEncoded [ODS_MAX_FILE_NAME_LENGTH+64+1];
   struct FAB  FileFab;
#ifdef ODS_EXTENDED
   struct NAML  FileNaml;
#endif /* ODS_EXTENDED */
   struct NAM  FileNam;

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

   sptr = HelpSpec;
   for (cptr = LibraryFileName;
        *cptr && *(cptr-1) != ']' && *(cptr-2) != '^' && *(cptr-2) != '.';
        *sptr++ = *cptr++);
   *sptr++ = '*';
   for (cptr = KeyWord[1]; *cptr; *sptr++ = *cptr++);
   for (cptr = "*.HLB"; *cptr; *sptr++ = *cptr++);
   *sptr++ = '\0';
   if (Debug) fprintf (stdout, "|%s|\n", HelpSpec);

   FileFab = cc$rms_fab;
   FileFab.fab$l_fop = FAB$V_NAM;
   FileFab.fab$l_nam = &FileNam;

#ifdef ODS_EXTENDED
   if (OdsExtended)
   {
      FileFab.fab$l_fna = (char*)-1;
      FileFab.fab$b_fns = 0;
      FileFab.fab$l_nam = (struct namdef*)&FileNaml;

      ENAMEL_RMS_NAML(FileNaml)
      FileNaml.naml$l_long_filename = HelpSpec;
      FileNaml.naml$l_long_filename_size = sptr - HelpSpec;
      FileNaml.naml$l_long_expand = ExpFileName;
      FileNaml.naml$l_long_expand_alloc = sizeof(ExpFileName)-1;
      FileNaml.naml$l_long_result = ResFileName;
      FileNaml.naml$l_long_result_alloc = sizeof(ResFileName)-1;
   }
   else
#endif /* ODS_EXTENDED */
   {
      FileFab.fab$l_fna = HelpSpec;
      FileFab.fab$b_fns = sptr - HelpSpec;
      FileFab.fab$l_nam = &FileNam;

      FileNam = cc$rms_nam;
      FileNam.nam$l_esa = ExpFileName;
      FileNam.nam$b_ess = ODS2_MAX_FILE_NAME_LENGTH;
      FileNam.nam$l_rsa = ResFileName;
      FileNam.nam$b_rss = ODS2_MAX_FILE_NAME_LENGTH;
   }

   if (VMSnok (status = sys$parse (&FileFab, 0, 0))) return (SS$_NORMAL);

   while (VMSok (status = sys$search (&FileFab, 0, 0)))
   {
#ifdef ODS_EXTENDED
      if (OdsExtended)
      {
         cptr = FileNaml.naml$l_long_dev;
         zptr = FileNaml.naml$l_long_ver;
      }
      else
#endif /* ODS_EXTENDED */
      {
         cptr = FileNam.nam$l_dev;
         zptr = FileNam.nam$l_ver;
      }

      /* q&d conversion to URL format */
      sptr = SimplePath;
      while (cptr < zptr)
      {
         if (*cptr == ':' && *(cptr+1) == '[')
         {
            *sptr++ = '/';
            cptr += 2;
         }
         else
         if (*cptr == '.' && *(cptr+1) == ']' && *(cptr+1) == '[')
         {
            *sptr++ = '/';
            cptr += 3;
         }
         else
         if (*cptr == ']')
         {
            *sptr++ = '/';
            cptr++;
         }
         else
            *sptr++ = tolower(*cptr++);
      }
      *sptr = '\0';
      CgiLibUrlEncode (SimplePath, -1, UrlEncoded, -1);

      sptr = String;
      if (!FileCount++) sptr += sprintf (sptr, "<p>");

      sptr += sprintf (sptr, "<a href=\"%s/%s?do=open&referer=%s&title=",
                       CgiScriptNamePtr, UrlEncoded, UriReferer);

#ifdef ODS_EXTENDED
      if (OdsExtended)
      {
         for (cptr = FileNaml.naml$l_long_name;
              cptr < FileNaml.naml$l_long_ver;
              *sptr++ = *cptr++);
         sptr += sprintf (sptr, "\">");
         for (cptr = FileNaml.naml$l_long_name;
              cptr < FileNaml.naml$l_long_ver;
              *sptr++ =*cptr++);
      }
      else
#endif /* ODS_EXTENDED */
      {
         for (cptr = FileNam.nam$l_name;
              cptr < FileNam.nam$l_ver;
              *sptr++ = *cptr++);
         sptr += sprintf (sptr, "\">");
         for (cptr = FileNam.nam$l_name;
              cptr < FileNam.nam$l_ver;
              *sptr++ = *cptr++);
      }

      sptr += sprintf (sptr, "</a>\n");

      fputs (String, stdout);
   }

   if (status == RMS$_FNF || status == RMS$_NMF) status = SS$_NORMAL;

   return (status);
}

/*****************************************************************************/
/*
Just suck the brains out of the module as a plain-text document.
*/ 
 
int ExtractHelpModule
(
struct dsc$descriptor_s  *KeyNameDscPtr,
void  *RFAptr
)
{
   int  status;
   char  Buffer [1024];
   $DESCRIPTOR (InBufferDsc, Buffer);
   $DESCRIPTOR (OutBufferDsc, Buffer);
 
   /*********/
   /* begin */
   /*********/

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

   if (VMSnok (status = lbr$lookup_key (&LibraryIndex,
                                        KeyNameDscPtr,
                                        &ModuleRFA)))
   {
      if (Debug) fprintf (stdout, "lib$lookup_key() %%X%08.08X\n", status);
      CgiLibResponseError (FI_LI, status, HtmlLibraryTitle);
      return (SS$_NORMAL);
   }

   /**************************************/
   /* loop through all records in module */
   /**************************************/

   CgiLibResponseHeader (200, "text/plain",
                         "Last-Modified: %s\n", LastModifiedGmDateTime);
   if (MethodHead) return (SS$_NORMAL);

   for (;;)
   {
      status = lbr$get_record (&LibraryIndex,
                               &InBufferDsc,
                               &OutBufferDsc);
      if (Debug) fprintf (stdout, "lbr$get_record() %%X%08.08X\n", status);
      if (VMSnok (status)) break;
 
      Buffer[OutBufferDsc.dsc$w_length] = '\n';
      Buffer[OutBufferDsc.dsc$w_length+1] = '\0';
      fputs (Buffer, stdout);
   }
 
   return (SS$_NORMAL);
}
 
/*****************************************************************************/
/*
*/
 
int SearchHelpLibraryModules ()

{
   int  status;
   char  *cptr, *sptr;
   char  String [1024];
 
   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "SearchHelpLibraryModules()\n");
 
   sprintf (String, "Search &quot;%s&quot; for &quot;%s&quot;",
            HtmlLibraryTitle, HtmlSearchString);

   BeginPage (String, String);

   fprintf (stdout, "<div class=\"results\">\n");

   RecordsSearched = SearchHitCount = 0;
   IndexNumber = 1;

   if (DoSearchStatistics) lib$init_timer (0);

   if (VMSnok (status = lbr$get_index (&LibraryIndex,
                                       &IndexNumber,
                                       &SearchHelpModule,
                                       KeyNameDsc())))
   {
      if (Debug) fprintf (stdout, "lib$get_index() %%X%08.08X\n", status);
      CgiLibResponseError (FI_LI, status, HtmlLibraryTitle);
      return (SS$_NORMAL);
   }

   if (!SearchHitCount) fprintf (stdout, "<b>Not found!</b>\n<p>");

   if (DoSearchStatistics) SearchStatistics ();

   fprintf (stdout, "</div>\n</div>\n");
   ButtonBar (2);
   fprintf (stdout, "</body>\n</html>\n");
}

/*****************************************************************************/
/*
This is a reasonably complex function, and reflects the complexities internal 
to help library modules!
*/ 
 
int SearchHelpModule
(
struct dsc$descriptor_s  *KeyNameDscPtr,
void  *RFAptr
)
{
   static boolean  HitThisTopic = false;
   static char  PrevFirstBufferChar = '\0';

   int  status,
        Count,
        HelpLevel = 0,
        Length;
   char  ch;
   char  *cptr, *hptr, *sptr;
   char  Buffer [1024],
         String [8192];
   $DESCRIPTOR (InBufferDsc, Buffer);
   $DESCRIPTOR (OutBufferDsc, Buffer);
 
   /*********/
   /* begin */
   /*********/

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

   strncpy (KeyWord[1],
            KeyNameDscPtr->dsc$a_pointer,
            KeyNameDscPtr->dsc$w_length);
   KeyWord[1][KeyNameDscPtr->dsc$w_length] = '\0';
   strcpy (KeyName[1], KeyWord[1]);
   CgiLibHtmlEscape (KeyWord[1], -1, KeyWordHtml[1], -1);
   if (!KeyWordCount) KeyWordCount = 1;

   if (Debug) fprintf (stdout, "KeyNameDsc |%s|\n", KeyWord[1]);

   if (VMSnok (status = lbr$lookup_key (&LibraryIndex,
                                        KeyNameDscPtr,
                                        &ModuleRFA)))
   {
      if (Debug) fprintf (stdout, "lib$lookup_key() %%X%08.08X\n", status);
      CgiLibResponseError (FI_LI, status, HtmlLibraryTitle);
      return (SS$_NORMAL);
   }

   /**************************************/
   /* loop through all records in module */
   /**************************************/

   for (;;)
   {
      status = lbr$get_record (&LibraryIndex,
                               &InBufferDsc,
                               &OutBufferDsc);
      if (Debug) fprintf (stdout, "lbr$get_record() %%X%08.08X\n", status);
      if (VMSnok (status)) break;
 
      /* terminate output buffer, removing any trailing white space */
      cptr = Buffer+OutBufferDsc.dsc$w_length-1;
      while (cptr >= Buffer && isspace(*cptr)) cptr--;
      *++cptr = '\0';
 
      /* comment record (line) in module */
      if (Buffer[0] == '!') continue;

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

      /*
          30-DEC-2008 OTS Bug?  (V7.3-1 and V8.3 at least)
          Use of isdigit() of copyright char 0xa9 resulted in ACCVIO
          LIBOTS          0 00000000000217B0 FFFFFFFF809117B0
          DECC$SHR_EV56   0 00000000000CD0E4 FFFFFFFF80B650E4
          DECC$SHR_EV56   0 000000000014504C FFFFFFFF80BDD04C
          DECC$SHR_EV56   0 0000000000144FAC FFFFFFFF80BDCFAC
          Explicit test of digit range worked around issue.
      */
      if ((Buffer[0] >= '0' && Buffer[0] <= '9' && Buffer[1] == ' ') ||
          Buffer[0] == '/')
      {
         /***************************/
         /* help level or qualifier */
         /***************************/
 
         if (Buffer[0] == '/')
         {
            /* if no previous qualifier encountered at this help level */
            /* then the qualifier effectively creates a new help level */
            if (PrevFirstBufferChar != '/') HelpLevel++;
            cptr = Buffer;
         }
         else
         {
            /* help level topic (line begins with a digit then a space) */
            HelpLevel = Buffer[0] - '0';
            for (cptr = Buffer + 1; isspace(*cptr); cptr++);
         }

         PrevFirstBufferChar = Buffer[0];
         strcpy (KeyName[HelpLevel], cptr);
         CgiLibHtmlEscape (cptr, -1, KeyNameHtml[HelpLevel], -1);

         /* if previous topic had a hit then end the list of the hit(s) */
         if (HitThisTopic)
         {
            fprintf (stdout, "</ul>\n");
            fflush (stdout);
         }
         HitThisTopic = false;
      }
      else
      {
         /*************/
         /* help text */
         /*************/
 
         /* check to see if this module record has any non-space chars */
         for (cptr = Buffer; *cptr && isspace(*cptr); cptr++);
         if (*cptr)
         {
            if (*(hptr = SearchText (cptr, CgiFormSearchPtr, false)))
            {
               /********/
               /* hit! */
               /********/

               if (Debug) fprintf (stdout, "Hit |%s|\n", hptr);
               SearchHitCount++;
               sptr = String;

               if (!HitThisTopic)
               {
                  for (Count = 1; Count <= HelpLevel; Count++)
                  {
                     if (Count > 1) *sptr++ = ' ';
                     sptr += sprintf (sptr,
"<a href=\"%s%s?key=%s&title=%s&referer=%s\">%s</a>\n",
                                      CgiScriptNamePtr, LibraryPathInfo,
                                      FormatRequestKeys(true,Count),
                                      UriFormTitle, UriReferer,
                                      MungeUnderScores(KeyNameHtml[Count]));
                  }
                  sptr += sprintf (sptr, "<ul>\n<li class=\"helpresult\">");
                  HitThisTopic = true;
               }
               else
               {
                  /* same topic, new hit */
                  sptr += sprintf (sptr, "<li class=\"helpresult\">");
               }

               ch = cptr[hptr-cptr];
               cptr[hptr-cptr] = '\0';
               sptr += CgiLibHtmlEscape (cptr, -1, sptr, -1);
               cptr[hptr-cptr] = ch;
               sptr += sprintf (sptr, "<span class=\"highlight\">");
               strncpy (sptr, hptr, SearchStringLength);
               sptr += SearchStringLength;
               sptr += sprintf (sptr, "</span>");
               sptr += CgiLibHtmlEscape (hptr+SearchStringLength, -1, sptr, -1);
               sprintf (sptr, "</li>\n");

               fputs (String, stdout);
            }
         }
      }
   }

   if (HitThisTopic)
   {
      fprintf (stdout, "</ul>\n");
      fflush (stdout);
      HitThisTopic = false;
   }

   return (SS$_NORMAL);
}
 
/*****************************************************************************/
/*
String together the keys associated with this help item.
*/ 
 
char* FormatRequestKeys
(
boolean ForUri,
int ToLevel
)
{
   static char  RequestKeys [256];

   int  idx;
   char  *sptr;

   sptr = RequestKeys;
   for (idx = 1; idx <= ToLevel; idx++)
   {
      if (ForUri)
         sptr += CgiLibUrlEncode (KeyName[idx], -1, sptr, -1);
      else
         sptr += CgiLibHtmlEscape (KeyName[idx], -1, sptr, -1);
      *sptr++ = '~';
   }
   /* if needed, get rid of that last '~'! */
   if (idx > 1)
      sptr[-1] = '\0';
   else
      *sptr = '\0';

   return (RequestKeys);
}
 
/*****************************************************************************/
/*
*/
 
int ListTextLibraryModules ()

{
   int  status;
 
   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ListTextLibraryModules()\n");
 
   BeginPage (LibraryTitle, HtmlLibraryTitle);

   fprintf (stdout,
"<div class=\"searchform\">\n\
<form action=\"%s%s\">\n\
<input type=\"hidden\" name=\"referer\" value=\"%s\">\n\
<input type=\"hidden\" name=\"title\" value=\"%s\">\n\
<input type=\"hidden\" name=\"do\" value=\"search\">\n\
<input type=\"submit\" value=\"Search for: \">\n\
<input type=\"text\" name=\"Search\">\n\
<input type=\"reset\" value=\"Reset\">\n\
</form>\n\
</div>\n\
<div class=\"textmodules\">\n",
      CgiScriptNamePtr, CgiPathInfoPtr,
      HtmlReferer, HtmlFormTitle);

   IndexNumber = 1;

   if (VMSnok (status = lbr$get_index (&LibraryIndex,
                                       &IndexNumber,
                                       &ListTextModule,
                                       KeyNameDsc())))
   {
      if (Debug) fprintf (stdout, "lib$get_index() %%X%08.08X\n", status);
      CgiLibResponseError (FI_LI, status, HtmlLibraryTitle);
      return (SS$_NORMAL);
   }

   fprintf (stdout, "</div>\n</div>\n");
   ButtonBar (2);
   fprintf (stdout, "</body>\n</html>\n");
}
 
/*****************************************************************************/
/*
*/ 
 
int ListTextModule
(
struct dsc$descriptor_s  *KeyNameDscPtr,
void  *RFAptr
)
{
   static int  PreviousCgiPlusUsageCount = 0;
   static char  PrevAlpha = '\0';

   char  String [1024],
         UriKeyName [256];
 
   /*********/
   /* begin */
   /*********/

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

   strncpy (KeyWord[1],
            KeyNameDscPtr->dsc$a_pointer,
            KeyNameDscPtr->dsc$w_length);
   KeyWord[1][KeyNameDscPtr->dsc$w_length] = '\0';
   strcpy (KeyName[1], KeyWord[1]);
   CgiLibUrlEncode (KeyWord[1], -1, UriKeyName, -1);
   CgiLibHtmlEscape (KeyWord[1], -1, KeyNameHtml[1], -1);

   if (Debug) fprintf (stdout, "KeyNameDsc |%s|\n", KeyWord[1]);

   if (ListSingle)
   {
      if (CgiPlusUsageCount == PreviousCgiPlusUsageCount)
         fprintf (stdout, "\n<p>");
      else
         fprintf (stdout, "<p>");
   }
   else
   if (!isalpha(KeyName[1][0]) ||
        KeyName[1][0] == PrevAlpha)
   {
      if (CgiPlusUsageCount == PreviousCgiPlusUsageCount)
         fprintf (stdout, "\n");
   }
   else
   {
      if (CgiPlusUsageCount == PreviousCgiPlusUsageCount)
         fprintf (stdout, "\n<p>");
      PrevAlpha = KeyName[1][0];
   }
   fprintf (stdout, "<a href=\"%s%s?key=%s&title=%s&referer=%s\">%s</a>\n",
            CgiScriptNamePtr, LibraryPathInfo, UriKeyName,
            UriFormTitle, UriReferer, KeyNameHtml[1]);

   PreviousCgiPlusUsageCount = CgiPlusUsageCount;
   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
*/
 
int OpenTextLibraryModule ()

{
   int  status;
 
   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "OpenTextLibraryModule()\n");
 
   CgiLibResponseHeader (200, "text/plain",
                         "Last-Modified: %s\n", LastModifiedGmDateTime);
   if (MethodHead) return (SS$_NORMAL);

   IndexNumber = 1;

   if (VMSnok (status = lbr$get_index (&LibraryIndex,
                                       &IndexNumber,
                                       &OpenTextModule,
                                       KeyNameDsc())))
   {
      if (Debug) fprintf (stdout, "lib$get_index() %%X%08.08X\n", status);
      CgiLibResponseError (FI_LI, status, HtmlLibraryTitle);
      return (SS$_NORMAL);
   }

   return (status);
}
 
/*****************************************************************************/
/*
*/ 
 
int OpenTextModule
(
struct dsc$descriptor_s  *KeyNameDscPtr,
void  *RFAptr
)
{
   int  status;
   char  Buffer [1024];
   $DESCRIPTOR (InBufferDsc, Buffer);
   $DESCRIPTOR (OutBufferDsc, Buffer);
 
   /*********/
   /* begin */
   /*********/

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

   strncpy (KeyWord[1],
            KeyNameDscPtr->dsc$a_pointer,
            KeyNameDscPtr->dsc$w_length);
   KeyWord[1][KeyNameDscPtr->dsc$w_length] = '\0';
   if (Debug) fprintf (stdout, "KeyNameDsc |%s|\n", KeyWord[1]);

   if (VMSnok (status = lbr$lookup_key (&LibraryIndex,
                                        KeyNameDscPtr,
                                        &ModuleRFA)))
   {
      if (Debug) fprintf (stdout, "lib$lookup_key() %%X%08.08X\n", status);
      CgiLibResponseError (FI_LI, status, HtmlLibraryTitle);
      return (SS$_NORMAL);
   }

   /**************************************/
   /* loop through all records in module */
   /**************************************/

   for (;;)
   {
      status = lbr$get_record (&LibraryIndex,
                               &InBufferDsc,
                               &OutBufferDsc);
      if (Debug) fprintf (stdout, "lbr$get_record() %%X%08.08X\n", status);
      if (VMSnok (status)) break;
 
      /* add HTTP required carriage-control */
      Buffer[OutBufferDsc.dsc$w_length] = '\n';
      Buffer[OutBufferDsc.dsc$w_length+1] = '\0';
      fputs (Buffer, stdout);
  }
 
   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
*/
 
int SearchTextLibraryModules ()

{
   int  status;
   char  String [1024];
 
   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "SearchTextLibraryModules()\n");
 
   sprintf (String, "Search &quot;%s&quot; for &quot;%s&quot;\n",
            HtmlLibraryTitle, HtmlSearchString);

   BeginPage (String, String);

   fprintf (stdout, "<div class=\"results\">\n");

   RecordsSearched = SearchHitCount = 0;
   IndexNumber = 1;

   if (DoSearchStatistics) lib$init_timer (0);

   if (VMSnok (status = lbr$get_index (&LibraryIndex,
                                       &IndexNumber,
                                       &SearchTextModule,
                                       KeyNameDsc())))
   {
      if (Debug) fprintf (stdout, "lib$get_index() %%X%08.08X\n", status);
      CgiLibResponseError (FI_LI, status, HtmlLibraryTitle);
      return (SS$_NORMAL);
   }

   if (!SearchHitCount) fprintf (stdout, "<b>Not found!</b>\n<p>");

   if (DoSearchStatistics) SearchStatistics ();

   fprintf (stdout, "</div>\n</div>\n");
   ButtonBar (2);
   fprintf (stdout, "</body>\n</html>\n");
}

/*****************************************************************************/
/*
*/ 
 
int SearchTextModule
(
struct dsc$descriptor_s  *KeyNameDscPtr,
void  *RFAptr
)
{
   boolean  HitThisModule = false;
   int  status;
   char  ch;
   char  *cptr, *hptr, *sptr;
   char  Buffer [1024],
         String [1024];
   $DESCRIPTOR (InBufferDsc, Buffer);
   $DESCRIPTOR (OutBufferDsc, Buffer);
 
   /*********/
   /* begin */
   /*********/

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

   strncpy (KeyName[1],
            KeyNameDscPtr->dsc$a_pointer,
            KeyNameDscPtr->dsc$w_length);
   KeyName[1][KeyNameDscPtr->dsc$w_length] = '\0';
   CgiLibHtmlEscape (KeyName[1], -1, KeyNameHtml[1], -1);
   if (Debug) fprintf (stdout, "KeyNameDsc |%s|\n", KeyName[1]);

   if (VMSnok (status = lbr$lookup_key (&LibraryIndex,
                                        KeyNameDscPtr,
                                        &ModuleRFA)))
   {
      if (Debug) fprintf (stdout, "lib$get_index() %%X%08.08X\n", status);
      CgiLibResponseError (FI_LI, status, HtmlLibraryTitle);
      return (SS$_NORMAL);
   }

   /**************************************/
   /* loop through all records in module */
   /**************************************/

   for (;;)
   {
      status = lbr$get_record (&LibraryIndex,
                               &InBufferDsc,
                               &OutBufferDsc);
      if (Debug) fprintf (stdout, "lbr$get_record() %%X%08.08X\n", status);
      if (VMSnok (status)) break;
 
      Buffer[OutBufferDsc.dsc$w_length] = '\0';
      if (Debug) fprintf (stdout, "Buffer |%s|\n", Buffer);

      /* check to see if this module record has any non-space chars */
      for (cptr = Buffer; *cptr && isspace(*cptr); cptr++);
      if (*cptr)
      {
         if (*(hptr = SearchText (cptr, CgiFormSearchPtr, false)))
         {
            /********/
            /* hit! */
            /********/

            if (Debug) fprintf (stdout, "Hit |%s|\n", sptr);
            SearchHitCount++;
            sptr = String;
            if (!HitThisModule)
            {
               sptr += sprintf (sptr,
"<a href=\"%s%s?key=%s&title=%s&referer=%s\"><b>%s</b></a>\n<ul>\n",
                  CgiScriptNamePtr, LibraryPathInfo,
                  KeyName[1], UriFormTitle, UriReferer, KeyNameHtml[1]);
               HitThisModule = true;
            }

            sptr += sprintf (sptr, "<li class=\"textresult\">");
            ch = cptr[hptr-cptr];
            cptr[hptr-cptr] = '\0';
            sptr += CgiLibHtmlEscape (cptr, -1, sptr, -1);
            cptr[hptr-cptr] = ch;
            sptr += sprintf (sptr, "<span class=\"highlight\">");
            strncpy (sptr, hptr, SearchStringLength);
            sptr += SearchStringLength;
            sptr += sprintf (sptr, "</span>");
            sptr += CgiLibHtmlEscape (hptr+SearchStringLength, -1, sptr, -1);
            sprintf (sptr, "</li>\n");

            fputs (String, stdout);
         }
      }
   }

   if (HitThisModule)
   {
      fprintf (stdout, "</ul>\n");
      fflush (stdout);
   }

   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
Case sensistive or case insensistive search of the string pointed to by 'sptr' 
for the string pointed to by 'SearchString'.
*/ 

char* SearchText
( 
char *tptr,
char *SearchString,
boolean CaseSensitive
)
{
   char  *cptr, *sptr;

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

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

   RecordsSearched++;

   sptr = SearchString;
   if (CaseSensitive)
   {
      while (*tptr)
      {
         if (*tptr++ != *sptr) continue;
         /* first character of search string matched record character */
         sptr++;
         cptr = tptr;
         while (*cptr && *sptr)
         {
            if (*cptr != *sptr) break;
            cptr++;
            sptr++;
         }
         if (!*sptr)
         {
            tptr--;
            break;
         }
         sptr = SearchString;
      }
   }
   else
   {
      while (*tptr)
      {
         if (toupper(*tptr++) != toupper(*sptr)) continue;
         /* first character of search string matched record character */
         sptr++;
         cptr = tptr;
         while (*cptr && *sptr)
         {
            if (toupper(*cptr) != toupper(*sptr)) break;
            cptr++;
            sptr++;
         }
         if (!*sptr)
         {
            tptr--;
            break;
         }
         sptr = SearchString;
      }
   }
   return (tptr);
}

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

SearchStatistics ()

{
   static long  LibElapsedTime = 1,
                LibCpuTime = 2;
   static $DESCRIPTOR (ElapsedFaoDsc, "!%T");

   static $DESCRIPTOR (SearchStats1FaoDsc,
"<div class=\"searchstats\">\
<b>Elapsed:</b> !AZ&nbsp; \
<b>CPU:</b> !2ZL:!2ZL.!2ZL&nbsp; \
<b>records (lines):</b> !UL\
</div>\n");

   unsigned long  ElapsedTime [2];
   unsigned long  CpuTime;
   unsigned short  Length;
   char  Elapsed [32],
         String [512];
   $DESCRIPTOR (ElapsedDsc, Elapsed);
   $DESCRIPTOR (StringDsc, String);

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

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

   lib$stat_timer (&LibElapsedTime, &ElapsedTime, 0);
   lib$stat_timer (&LibCpuTime, &CpuTime, 0);

   sys$fao (&ElapsedFaoDsc, &Length, &ElapsedDsc, &ElapsedTime);
   Elapsed[Length] = '\0';
   sys$fao (&SearchStats1FaoDsc, &Length, &StringDsc,
            Elapsed+3,
            CpuTime/6000, CpuTime/100, CpuTime%100,
            RecordsSearched);
   String[Length] = '\0';
   fprintf (stdout, "%s", String);
}

/*****************************************************************************/
/*
Output the HTTP header and page header.
*/
 
BeginPage
(
char *TitleString,
char *HeaderString
)
{
   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "BeginPage()\n");
 
   CgiLibResponseHeader (200, "text/html",
                         "Last-Modified: %s\n", LastModifiedGmDateTime);
   if (MethodHead) return;

   fprintf (stdout,
"<!DOCTYPE html>\n\
<html>\n\
<head>\n\
<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\
<meta name=\"generator\" content=\"%s\">\n\
<meta name=\"copyright\" content=\"%s - GPL licensed\">\n\
<meta name=\"environment\" content=\"%s\">\n\
<meta name=\"date\" content=\"%s\">\n\
<title>%s</title>\n\
<style type=\"text/css\">\n\
%s\
</style>\n\
%s\
</head>\n\
<body>\n",
      SoftwareID, SoftwareCopy,
      CgiLibEnvironmentPtr,
      UnixDateTime, 
      TitleString,
      DefaultStyle,
      StyleSheetPtr);

   fprintf (stdout,
"<div class=\"header\">%s</div>\n\
<div class=\"libcontent\">\n",
            HeaderString);
 
   fflush (stdout);
}
 
/*****************************************************************************/
/*
In a help key string turn underscores into spaces mixed case.
*/

char* MungeUnderScores (char *InString)

{
   static char  OutString [128];

   char  *cptr, *sptr;

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

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

   sptr = OutString;
   if (InString[0] != '/')
   {
      /* not a qualifier, see if it's all the one case! */
      if (isupper(InString[0]))
      {
         for (cptr = InString; *cptr; cptr++)
            if (isalpha(*cptr) && !isupper(*cptr)) break;
      }
      else
      {
         for (cptr = InString; *cptr; cptr++)
            if (isalpha(*cptr) && !islower(*cptr)) break;
      }
   }
   if (InString[0] != '/' && *cptr)
   {                            
      /* probably an underscore-joined topic string */
      for (cptr = InString; *cptr; cptr++)
      {
         if (*cptr == '_')
            *sptr++ = ' ';
         else
            *sptr++ = *cptr;
      }
   }
   else
   {
      /* all upper-case, probably some particular VMS parameter */
      for (cptr = InString; *cptr; cptr++)
         *sptr++ = *cptr;
   }
   *sptr = '\0';
   return (OutString);
}

/*****************************************************************************/
/*
If the object has been modified since the specified date and time then return 
a normal status indicating that the data transfer is to continue.  If not 
modified then send a "not modified" HTTP header and return an error status to 
indicate the object should not be sent.
*/ 
 
int ModifiedSince
(
unsigned long *SinceBinaryTimePtr,
unsigned long *BinaryTimePtr
)
{
   static unsigned long  OneSecondDelta [2] = { -10000000, -1 };

   int  status;
   unsigned long  AdjustedBinTime [2],
                  ScratchBinTime [2];

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

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

   /* if request asks for a "reload" (not cached) then give it regardless */
   if (strsame (CgiHttpPragmaPtr, "no-cache", -1)) return (SS$_NORMAL);
   if (strsame (CgiHttpCacheControlPtr, "no-cache", 8)) return (SS$_NORMAL);
   if (strsame (CgiHttpCacheControlPtr, "no-store", 8)) return (SS$_NORMAL);
   if (strsame (CgiHttpCacheControlPtr, "max-age=0", 9)) return (SS$_NORMAL);

   /*
      Add one second to the modified time.  Ensures a negative time
      for VMS where fractional seconds may result in inconclusive
      results when the target time is being specified in whole seconds.
   */ 
   if (VMSnok (status =
       lib$add_times (SinceBinaryTimePtr, &OneSecondDelta, &AdjustedBinTime)))
   {
      CgiLibResponseError (FI_LI, status, "IfModifiedSince:");
      return (status);
   }
   if (Debug) fprintf (stdout, "sys$add_times() %%X%08.08X\n", status);

   /* if a positive time results the file has been modified */
   if (VMSok (status =
       lib$sub_times (BinaryTimePtr, &AdjustedBinTime, &ScratchBinTime)))
      return (status);

   if (Debug) fprintf (stdout, "sys$sub_times() %%X%08.08X\n", status);
   if (status != LIB$_NEGTIM)
   {
      CgiLibResponseError (FI_LI, status, "IfModifiedSince:");
      return (status);
   }

   /* not modified */
   CgiLibResponseHeader (304, "text/html");
   fprintf (stdout, "Not modified!\n");

   return (LIB$_NEGTIM);
}

/*****************************************************************************/
/*
Create an HTTP format Greenwich Mean Time (UTC) time string in the storage 
pointed at by 'TimeString', e.g. "Fri, 25 Aug 1995 17:32:40 GMT" (RFC 1123). 
This must be at least 30 characters capacity.  If 'BinTimePtr' is null the 
time string represents the current time.  If it points to a quadword, VMS time 
value the string represents that time.  'TimeString' must point to storage 
large enough for 31 characters.
*/

int HttpGmTimeString
(
char *TimeString,
unsigned long *BinTimePtr
)
{
   static char  *DayNames [] =
      { "", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" };
   static char  *MonthName [] =
      { "", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
            "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };

   static $DESCRIPTOR (HttpTimeFaoDsc, "!AZ, !2ZW !AZ !4ZW !2ZW:!2ZW:!2ZW GMT");
   static $DESCRIPTOR (TimeStringDsc, "");

   int  status;
   unsigned long  BinTime [2],
                  GmTime [2];
   unsigned short  Length;
   unsigned short  NumTime [7];
   unsigned long  DayOfWeek;

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

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

   if (!BinTimePtr)
      sys$gettim (&GmTime);
   else
   {
      GmTime[0] = BinTimePtr[0];
      GmTime[1] = BinTimePtr[1];
   }
   if (VMSnok (status = TimeAdjustGMT (true, &GmTime)))
      return (status);

   status = sys$numtim (&NumTime, &GmTime);
   if (Debug)
      fprintf (stdout, "sys$numtim() %%X%08.08X %d %d %d %d %d %d %d\n",
               status, NumTime[0], NumTime[1], NumTime[2],
               NumTime[3], NumTime[4], NumTime[5], NumTime[6]);

   if (VMSnok (status = lib$day_of_week (&GmTime, &DayOfWeek)))
      return (status);
   if (Debug)
      fprintf (stdout, "lib$day_of_week() %%X%08.08X is %d\n",
               status, DayOfWeek);

   /* set the descriptor address and size of the resultant time string */
   TimeStringDsc.dsc$w_length = 30;
   TimeStringDsc.dsc$a_pointer = TimeString;

   if (VMSnok (status =
       sys$fao (&HttpTimeFaoDsc, &Length, &TimeStringDsc,
                DayNames[DayOfWeek], NumTime[2], MonthName[NumTime[1]],
                NumTime[0], NumTime[3], NumTime[4], NumTime[5])))
   {
      if (Debug) fprintf (stdout, "sys$fao() %%X%08.08X\n", status);
      TimeString[0] = '\0';
   }
   else
      TimeString[Length] = '\0';
   if (Debug) fprintf (stdout, "|%s|\n", TimeString);

   return (status);
}

/*****************************************************************************/
/*
Given a string such as "Fri, 25 Aug 1995 17:32:40 GMT" (RFC 1123), or "Friday,
25-Aug-1995 17:32:40 GMT" (RFC 1036), create an internal, local, binary time
with the current GMT offset.  See complementary function HttpGmTimeString().
*/ 

int HttpGmTime
(
char *TimeString,
unsigned long *BinTimePtr
)
{
   static char  *MonthName [] =
      { "", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
            "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };

   int  status;
   char  *tptr;
   unsigned short  Length;
   unsigned short  NumTime [7] = { 0,0,0,0,0,0,0 };
   unsigned long  DayOfWeek;

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

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

   tptr = TimeString;
   /* hunt straight for the comma after the weekday name! */
   while (*tptr && *tptr != ',') tptr++;
   if (*tptr) tptr++;
   /* span white space between weekday name and date */
   while (*tptr && isspace(*tptr)) tptr++;
   if (Debug) fprintf (stdout, "tptr |%s|\n", tptr);
   if (!*tptr) return (STS$K_ERROR);

   /* get the date and then skip to month name */
   if (isdigit(*tptr)) NumTime[2] = atoi (tptr);
   while (*tptr && isdigit(*tptr)) tptr++;
   while (*tptr && (*tptr == '-' || isspace(*tptr))) tptr++;
   if (Debug) fprintf (stdout, "tptr |%s|\n", tptr);
   if (!*tptr) return (STS$K_ERROR);

   /* get the month number from the name and skip to the year */
   for (NumTime[1] = 1; NumTime[1] <= 12; NumTime[1]++)
      if (strsame (tptr, MonthName[NumTime[1]], 3)) break;
   if (NumTime[1] > 12) return (STS$K_ERROR);
   while (*tptr && isalpha(*tptr)) tptr++;
   while (*tptr && (*tptr == '-' || isspace(*tptr))) tptr++;
   if (Debug) fprintf (stdout, "tptr |%s|\n", tptr);
   if (!*tptr) return (STS$K_ERROR);

   /* get the year and then skip to the hour */
   if (isdigit(*tptr))
   {
      NumTime[0] = atoi (tptr);
      if (NumTime[0] < 100) NumTime[0] += 1900;
   }
   while (*tptr && isdigit(*tptr)) tptr++;
   while (*tptr && isspace(*tptr)) tptr++;
   if (Debug) fprintf (stdout, "tptr |%s|\n", tptr);
   if (!*tptr) return (STS$K_ERROR);

   /* get the hour, minute and second */
   if (isdigit(*tptr)) NumTime[3] = atoi (tptr);
   while (*tptr && isdigit(*tptr)) tptr++;
   if (*tptr == ':') tptr++;
   if (isdigit(*tptr)) NumTime[4] = atoi (tptr);
   while (*tptr && isdigit(*tptr)) tptr++;
   if (*tptr == ':') tptr++;
   if (isdigit(*tptr)) NumTime[5] = atoi (tptr);
   while (*tptr && isdigit(*tptr)) tptr++;
   if (*tptr == ':') tptr++;
   if (!*tptr) return (STS$K_ERROR);

   /* the only thing remaining should be the "GMT" */
   while (*tptr && isspace(*tptr)) tptr++;
   if (Debug) fprintf (stdout, "tptr |%s|\n", tptr);
   if (!strsame (tptr, "GMT", 3)) return (STS$K_ERROR);

   /*******************************************/
   /* convert what looks like legitimate GMT! */
   /*******************************************/

   if (Debug)
      fprintf (stdout, "NumTime[] %d %d %d %d %d %d %d\n",
               NumTime[0], NumTime[1], NumTime[2], NumTime[3], 
               NumTime[4], NumTime[5], NumTime[6]); 
   status = lib$cvt_vectim (&NumTime, BinTimePtr);
   if (VMSnok (status)) return (status);
   if (Debug) fprintf (stdout, "lib$cvt_vectim() %%X%08.08X\n", status);

   return (TimeAdjustGMT (false, BinTimePtr));
}

/*****************************************************************************/
/*
Determine the offset from GMT (UTC) using either the HTTPD$GMT or
SYS$TIMEZONE_DIFFERENTIAL logicals. If HTTPD$GMT is not defined time should be
set from the timezone differential logical. Function RequestBegin() calls
this every hour to recheck GMT offset and detect daylight saving or other
timezone changes.
*/

int TimeSetGmt ()

{
   static boolean  UseTimezoneDifferential = false;

   int  status;

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

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

   if (!UseTimezoneDifferential)
   {
      status = TimeSetHttpdGmt();
      /* return if error and that error was not that the name did not exist */
      if (VMSok (status) || status != SS$_NOLOGNAM) return (status);
   }

   UseTimezoneDifferential = true;
   return (TimeSetTimezone());
}

/*****************************************************************************/
/*
The SYS$TIMEZONE_DIFFERENTIAL logical contains the number of seconds offset
from GMT (UTC) as a positive (ahead) or negative (behind) number.  Set the
'TimeGmtString' global storage to a "+hh:mm" or "-hh:mm" string equivalent, and
the 'TimeAheadOfGmt' global boolean and 'TimeGmtVmsString' delta-time global
string.
*/

int TimeSetTimezone ()

{
   static unsigned short  Length;
   static $DESCRIPTOR (TimezoneLogicalNameDsc, "SYS$TIMEZONE_DIFFERENTIAL");
   static $DESCRIPTOR (LnmSystemDsc, "LNM$SYSTEM");
   static $DESCRIPTOR (TimeGmtStringFaoDsc, "!AZ!2ZL:!2ZL");
   static $DESCRIPTOR (TimeGmtVmsStringFaoDsc, "0 !2ZL:!2ZL");
   static $DESCRIPTOR (TimeGmtStringDsc, TimeGmtString);
   static struct {
      short int  buf_len;
      short int  item;
      void  *buf_addr;
      unsigned short  *ret_len;
   } LnmItems [] =
   {
      { sizeof(TimeGmtString)-1, LNM$_STRING, TimeGmtString, &Length },
      { 0,0,0,0 }
   };

   int  status;
   long  Hours,
         Minutes,
         Seconds;
   char  *SignPtr;
   $DESCRIPTOR (TimeGmtVmsStringDsc, TimeGmtVmsString);

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

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

   status = sys$trnlnm (0, &LnmSystemDsc, &TimezoneLogicalNameDsc, 0, &LnmItems);
   if (Debug) fprintf (stdout, "sys$trnlnm() %%X%08.08X\n", status);
   if (VMSnok (status)) return (status);

   TimeGmtString[Length] = '\0';
   if (Debug) fprintf (stdout, "TimeGmtString |%s|\n", TimeGmtString);
   Seconds = atol(TimeGmtString);
   if (Seconds < 0)
   {
      TimeAheadOfGmt = false;
      Seconds = -Seconds;
      SignPtr = "-";
   }
   else
   {
      TimeAheadOfGmt = true;
      SignPtr = "+";
   }
   Hours = Seconds / 3600;
   Minutes = (Seconds - Hours * 3600) / 60;
   if (Debug)
      fprintf (stdout, "%d %s%d:%d\n", Seconds, SignPtr, Hours, Minutes);
   sys$fao (&TimeGmtStringFaoDsc, &Length, &TimeGmtStringDsc,
            SignPtr, Hours, Minutes);
   TimeGmtString[Length] = '\0';
   if (Debug) fprintf (stdout, "TimeGmtString |%s|\n", TimeGmtString);

   sys$fao (&TimeGmtVmsStringFaoDsc, &Length, &TimeGmtVmsStringDsc,
            Hours, Minutes);
   TimeGmtVmsString[Length] = '\0';
   if (Debug) fprintf (stdout, "TimeGmtVmsString |%s|\n", TimeGmtVmsString);

   TimeGmtVmsStringDsc.dsc$w_length = Length;

   if (VMSnok (status =
       sys$bintim (&TimeGmtVmsStringDsc, &TimeGmtDeltaBinary)))
      return (status);
   if (TimeGmtDeltaBinary[0] || TimeGmtDeltaBinary[1])
      return (status);
   /* time must have been zero, make it one, one-hundreth of a second */
   TimeGmtDeltaBinary[0] = -100000;
   TimeGmtDeltaBinary[1] = -1;
   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
Translate the logical HTTPD$GMT (defined to be something like "+10:30" or "-
01:15") and convert it into a delta time structure and store in 
'TimeGmtDeltaBinary'.  Store whether it is in advance or behind GMT in boolean 
'TimeAheadOfGmt'.  Store the logical string in 'TimeGmtString'.
*/

int TimeSetHttpdGmt ()

{
   static unsigned short  Length;
   static $DESCRIPTOR (GmtLogicalNameDsc, "HTTPD$GMT");
   static $DESCRIPTOR (LnmFileDevDsc, "LNM$FILE_DEV");
   static struct {
      short int  buf_len;
      short int  item;
      void  *buf_addr;
      unsigned short  *ret_len;
   } LnmItems [] =
   {
      { sizeof(TimeGmtString)-1, LNM$_STRING, TimeGmtString, &Length },
      { 0,0,0,0 }
   };

   int  status;
   char  *cptr, *sptr;
   $DESCRIPTOR (TimeGmtVmsStringDsc, TimeGmtVmsString);

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

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

   status = sys$trnlnm (0, &LnmFileDevDsc, &GmtLogicalNameDsc, 0, &LnmItems);
   if (Debug) fprintf (stdout, "sys$trnlnm() %%X%08.08X\n", status);
   if (VMSnok (status)) return (status);

   TimeGmtString[Length] = '\0';
   if (Debug) fprintf (stdout, "TimeGmtString |%s|\n", TimeGmtString);

   if (TimeGmtString[0] == '$') return (SS$_NORMAL);

   if (*(cptr = TimeGmtString) == '-')
      TimeAheadOfGmt = false;
   else
      TimeAheadOfGmt = true;
   if (*cptr == '+' || *cptr == '-') cptr++;
   sptr = TimeGmtVmsString;
   *sptr++ = '0';
   *sptr++ = ' ';
   while (*cptr) *sptr++ = *cptr++;
   *sptr = '\0';
   if (Debug) fprintf (stdout, "TimeGmtVmsString |%s|\n", TimeGmtVmsString);

   TimeGmtVmsStringDsc.dsc$w_length = sptr - TimeGmtVmsString;

   if (VMSnok (status =
       sys$bintim (&TimeGmtVmsStringDsc, &TimeGmtDeltaBinary)))
      return (status);
   if (TimeGmtDeltaBinary[0] || TimeGmtDeltaBinary[1])
      return (status);
   /* time must have been zero, make it one, one-hundreth of a second */
   TimeGmtDeltaBinary[0] = -100000;
   TimeGmtDeltaBinary[1] = -1;
   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
The GMT is generated by calculating using an offset using 
'TimeGmtDeltaOffset' and boolean 'TimeAheadOfGmt'.  Adjust either to or from 
GMT.
*/ 

int TimeAdjustGMT
(
boolean ToGmTime,
unsigned long *BinTimePtr
)
{
   int  status;
   unsigned long  AdjustedTime [2];

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

   if (Debug) fprintf (stdout, "TimeAdjustGMT() ToGmTime: %d\n", ToGmTime);

   if ((ToGmTime && TimeAheadOfGmt) || (!ToGmTime && !TimeAheadOfGmt))
   {
      /* to GMT from local and ahead of GMT, or to local from GMT and behind */
      status = lib$sub_times (BinTimePtr, &TimeGmtDeltaBinary, &AdjustedTime);
      if (Debug) fprintf (stdout, "lib$sub_times() %%X%08.08X\n", status);
   }
   else
   {
      /* to GMT from local and behind GMT, or to local from GMT and ahead */
      status = lib$add_times (BinTimePtr, &TimeGmtDeltaBinary, &AdjustedTime);
      if (Debug) fprintf (stdout, "lib$add_times() %%X%08.08X\n", status);
   }

   if (Debug)
   {
      unsigned short  Length;
      char  String [64];
      $DESCRIPTOR (AdjustedTimeFaoDsc, "AdjustedTime: |!%D|\n");
      $DESCRIPTOR (StringDsc, String);

      sys$fao (&AdjustedTimeFaoDsc, &Length, &StringDsc, &AdjustedTime);
      String[Length] = '\0';
      fprintf (stdout, "%s", String);
   }

   BinTimePtr[0] = AdjustedTime[0];
   BinTimePtr[1] = AdjustedTime[1];

   return (status);
}

/*****************************************************************************/
/*
Output an HTML-escaped, URL-anchored, and "$ HELP"-anchored string.
*/

HtmlOutputString
(
char *InString,
boolean AnchorUrls
)
{
   char  OutString [1024];
   char  *cptr;

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

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

   CgiLibAnchorHtmlEscape (InString, -1,
                           OutString, sizeof(OutString),
                           AnchorUrls);
   if (cptr = strstr (OutString, "$ HELP"))
   {
      char  *sptr, *tptr;
      char  Scratch [1024];

      cptr += 2;
      tptr = cptr + 4;
      sptr = Scratch;
      while (*tptr && *tptr != '\n')
      {
         switch (*tptr)
         {
            case ' ' : *sptr++ = '/'; break;
            case '/' : *sptr++ = '/'; *sptr++ = '/'; break;
            default : *sptr++ = *tptr;
         }
         tptr++;
      }
      *sptr = '\0';
      *tptr = '\0';
      fwrite (OutString, cptr-OutString, 1, stdout);
      fprintf (stdout, "<a href=\"%s%s\">%s</a>\n",
               CgiScriptNamePtr, Scratch, cptr);

   }
   else
      fputs (OutString, stdout);
}

/****************************************************************************/
/*
Return an integer reflecting the major and minor version of VMS (e.g. 60, 61,
62, 70, 71, 72, etc.)
*/ 

#ifdef ODS_EXTENDED

int GetVmsVersion ()

{
   static char  SyiVersion [16];

   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,
        version;

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

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

   if (VMSnok (status = sys$getsyiw (0, 0, 0, &SyiItems, 0, 0, 0)))
      exit (status);
   SyiVersion[8] = '\0';
   version = ((SyiVersion[1]-48) * 10) + (SyiVersion[3]-48);
   if (Debug) fprintf (stdout, "|%s| %d\n", SyiVersion, version);
   return (version);
}

#endif /* ODS_EXTENDED */

/*****************************************************************************/
/*
Return non-NULL pointer if looks a bit suspect!
*/

char* XSSuspect (char *cptr)

{
   static char  XSSredacted [] = "XSS redacted! (tsk-tsk)";

   char  *sptr, *zptr;
   char buf [1024];

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

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

   if (!cptr || !*cptr) return (NULL);
   if (strstr (cptr, "</")) return (XSSredacted);
   zptr = (sptr = buf) + sizeof(buf)-1;
   while (*cptr && *cptr && sptr < zptr) *sptr++ = tolower(*cptr++);
   *sptr = '\0';
   if (sptr >= zptr || strstr (buf, "<script>")) return (XSSredacted);
   return (NULL);
}

/****************************************************************************/
/*
Does a case-insensitive, character-by-character string compare and returns 
true if two strings are the same, or false if not.  If a maximum number of 
characters are specified only those will be compared, if the entire strings 
should be compared then specify the number of characters as 0.
*/ 

boolean strsame
(
char *sptr1,
char *sptr2,
int  count
)
{
   while (*sptr1 && *sptr2)
   {
      if (toupper (*sptr1++) != toupper (*sptr2++)) return (false);
      if (count)
         if (!--count) return (true);
   }
   if (*sptr1 || *sptr2)
      return (false);
   else
      return (true);
}

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

