/*****************************************************************************/
/*
                              HyperShelf.c

A CGI-compliant script.
Can be supported by any server that supplies the CGI variables listed below.


ABOUT HYPERSHELF
----------------

Emulates the "Bookshelf Navigation Utility" (BNU) of 1997ff on-line
documentation CD releases, and the Bookreader shelf navigation capability of
earlier times. It has some additional capabilities not found in either, the
ability to provide access to PostScript, plain-text documents and PDF
documents.

Page colouration may be specified via the appropriate command-line qualifiers
(or corresponding logical name). Defaults for any not specified.  Specifiy as
/BLAH="" to NOT specify the corresponding colour (i.e. leave it to the
browser).

Server mapping must include rules for all file paths (devices) which have
shelves residing on them.  This script munges VMS file paths into URL-style
equivalents.  For example:

  DISK$AXPDOCSEP971:[DATABASE]D39QAAA8.BKB

would be represented as

  /disk$axpdocsep971/database/d39qaaa8.bkb

Therefore the server must map the "/disk$axpdocsep971/*" into the VMS
equivalent, and of course all file paths that can possibly be returned.
Actual a useful rule for all BNU CDs is (depending on your architecture):

  pass /disk$axpdoc*  /disk$axpdoc*
  pass /disk$vaxdoc*  /disk$vaxdoc*

Thanks to Jeff Goodwin of Fairchild Semiconductor for these VMS Apache mappings

  AliasMatch ^/disk\$axpdoc(.+) /disk$axpdoc$1
  AliasMatch ^/disk\$vaxdoc(.+) /disk$vaxdoc$1

Digital Bookreader supports Bookreader format documents.

Digital BNU (currently) supports Bookreader and HTML documents.

WASD Hypershelf supports both plus the following extensions; PostScript, plain-
text and Adobe PDF documents.  It does not support Bristol HyperHelp.

Note that the Hypershelf extensions are preceded by two comment characters,
either "##" (as in the example) or "!!".  The prevents the BNU from barfing on
them.  If a title is the same as the previous entry (or is missing altogether)
the document title becomes a description of the document type.  For examples of
how they behave do a 

  http:///hypershelf/wasd_root/exercise/library.odl

  http:///hypershelf/wasd_root/exercise/library.decw$bookshelf


HYPERSHELF EXTENSIONS (see examples for examples)
---------------------
##post     PostScript document
##plain    plain-text document
##pdf      Adobe Portable Document Format
##url      relative or full Web URL (e.g. 'http://the.host.name/library/')


EXAMPLE BNU SHELF (extension: .ODL)
-----------------

  Title	VMS Hypertext Services Library (BNU version!)
  book	/wasd_root/doc/htd/htd.decw$book	Technical Overview
  html	/wasd_root/doc/htd/htd_0000.html	Technical Overview
  ##post	/wasd_root/doc/htd/htd.ps		Technical Overview
  ##plain	/wasd_root/doc/htd/htd.txt	Technical Overview
  ##pdf	/wasd_root/doc/htd/htd.pdf	Technical Overview
  book	/wasd_root/doc/env/env.decw$book	Hypertext Environment
  html	/wasd_root/doc/env/env_0000.html	Hypertext Environment
  # don't have to specify the title for the same document but different type
  ##post	/wasd_root/doc/env/env.ps
  ##plain	/wasd_root/doc/env/env.txt
  book	/wasd_root/doc/sdm2htm/sdm2htm.decw$book	SDML to HTML Converter
  html	/wasd_root/doc/sdm2htm/sdm2htm_0000.html	SDML to HTML Converter
  ##post	/wasd_root/doc/sdm2htm/sdm2htm.ps	SDML to HTML Converter
  book	/wasd_root/doc/menu_primer/menu_primer.decw$book	Menu Primer
  html	/wasd_root/doc/menu_primer/menu_primer_0000.html	Menu Primer
  ##url   http://the.host.name/library/  The Host Name Library
  ##url   /docs/  Local Server Documents


EXAMPLE BOOKREADER SHELF (extension: .DECW$BOOKSHELF or .BKS)
------------------------

  title\\VMS Hypertext Services Library (Bookreader version!)
  book\wasd_root:[doc.htd]htd\Technical Overview
  ##html\wasd_root:[doc.htd]htd_0000.html\
  ##ps\wasd_root:[doc.htd]htd.ps\
  ##plain\wasd_root:[doc.htd]htd.txt\
  ##pdf\wasd_root:[doc.htd]htd.pdf\
  book\wasd_root:[doc.env]env\Hypertext Environment
  ##html\wasd_root:[doc.env]env_0000.html\
  ##post\wasd_root:[doc.env]env.ps\
  ##plain\wasd_root:[doc.env]env.txt\
  book\wasd_root:[doc.sdm2htm]sdm2htm\SDML to HTML Converter
  ##html\wasd_root:[doc.sdm2htm]sdm2htm_0000.html\
  ##ps\wasd_root:[doc.sdm2htm]sdm2htm.ps\
  book\wasd_root:[doc.menu_primer]menu_primer\Menu Primer
  ##html\wasd_root:[doc.menu_primer]menu_primer_0000.html\Menu Primer
  ##url\http://the.host.name/library/\The Host Name Library
  ##url\/docs/\Local Server Documents


BNU OR BOOKREADER BEHAVIOUR?
----------------------------

HyperShelf adjusts it's behaviour according to the shelf file it is given in
the path.  If the file has a type of .ODL it is considered to be in BNU shelf
format (see above).  Any other type (e.g. .BKS or .DECW$BOOKSHELF) it
considers to be in Bookreader format (again, see above).

If a path to a shelf is not provided HyperShelf looks into the environment for
the DECW$BOOK and DECW$BOOKSHELF logicals.  If it finds DECW$BOOK it considers
it is a Bookreader environment and BNU is not in use.  It they are not found it
considers it a BNU environment.  To specify a default BNU library for when
none is supplied in the path use the /LIBRARY qualifier in a script support
procedure.

In a Bookreader environment HyperShelf will open any library specified by
DECW$BOOKSHELF (or libraries if it has multiple translations).  If not defined
it searches for a file using DECW$BOOK:LIBRARY.*  The first found will become
the default library. This will select all libraries (Digital and third party)
if DECW$BOOK is defined with multiple translations.  The first library
encountered is expanded to display it's contents.  Subsequent libraries have a
shelf link created for them with information appended to the description
indicating it is a library.  The use of search lists can considerably extend
library access (search) times, particularly when slow CD-ROM readers,
InfoServers or DECnet/FAL is involved.  A faster alternative is to incorporate
references to these in an explicit DECW$BOOKSHELF logical.

The BNU environment does not use logical names or logical search lists. 
Everthing appears to be "hard-wired" into the shelves in a URL-style or
Unix-style file path (understandable when you consider part of the BNU viewing
environment is a browser (Netscape Navigator)).  If HyperShelf is not supplied
with a path to an ODL shelf it will search for one of those listed in
'OdlShelfDefaults' storage below.

The qualifiers /BNU and /BOOKREADER can be used to force one behaviour or the
other.


SPECIFYING SHELVES
------------------
Shelves are specified in one of four ways, with the indicated precedence.

  1.  ?file= .......... request query string form field (VMS file spec)
  2.  /path/to/file ... path into VMS file spec via WWW_PATH_TRANSLATED
  3.  /LIBRARY= ....... HYPERSHELF script qualifier (VMS file spec)
  4.  no path ......... DECW$BOOK, DECW$BOOKSHELF, BNU default libraries


OSU ENVIRONMENT
---------------
Script responses are returned in OSU "raw" mode; the script taking care of the
full response header and correctly carriage-controlled data stream, text or
binary!!  Uses the CGILIB.C to engage in the dialog phase generating, storing
and then making available the equivalent of CGI variables.

To use the executable directly (no wrapper procedure), as in

  http://the.host.name/htbin/hypershelf

Define a system-wide logical during server startup

  $ HYPERSHELF_PARAM = -
    "/buttons=""Close$Help=/wasd/runtime/hypershelfhelp.html""" +-
    "/icons=""/wasd/runtime"""
  $ DEFINE /SYSTEM HYPERSHELF$PARAM HYPERSHELF_PARAM


APACHE CGI ENVIRONMENT
----------------------
To use the executable directly (no wrapper procedure), as in

  http://the.host.name/cgi-bin/hypershelf

Define a system-wide logical during server startup

  $ HYPERSHELF_PARAM = -
    "/buttons=""Close$Help=/wasd/runtime/hypershelfhelp.html""" +-
    "/icons=""/wasd/runtime"""
  $ DEFINE /SYSTEM HYPERSHELF$PARAM HYPERSHELF_PARAM


CGI VARIABLES
-------------
Server generated ...

WWW_HTTP_IF_MODIFIED_SINCE      if "304 Not modified" are desired (optional)
WWW_HTTP_PRAGMA         "no-cache" (optional)
WWW_HTTP_REFERER        "close" button becomes active
WWW_PATH_INFO           URL path to shelf
WWW_PATH_TRANSLATED     VMS file specification for shelf
WWW_REQUEST_METHOD      "GET" only is supported
WWW_SCRIPT_NAME         path to script
WWW_SERVER_CHARSET      default character set of server
WWW_SERVER_NAME         host on which the script is executing
WWW_SERVER_SOFTWARE     HTTPd identifying string

HyperShelf generated ...

WWW_FORM_FILE           VMS file name for shelf (when path cannot be supplied)
WWW_FORM_REFERER        overrides the HTTP_REFERER URL
WWW_FORM_TITLE          explicit title of book


QUALIFIERS
----------
/BOOKREADER     Bookreader is the default shelf processor
/BNU            Bookshelf Navigation Utility is the default shelf processor
/CHARSET=       "Content-Type: text/html; charset=...", empty suppresses charset
/DBUG           turns on all "if (Debug)" statements (don't use with OSU)
/HYPERREADER=   name of 'HyperReader' script (defaults to "/hyperreader")
/ICON=          path to icons
/LIBRARY=       specifies the default library (shelf) file (VMS path)
/[NO]ODS5       control extended file specification (basically for testing)
/PRINT=         name of 'Print' script (optional)


LOGICAL NAMES
-------------
HYPERSHELF$DBUG        turns on all "if (Debug)" statements
HYPERSHELF$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)


BUILD DETAILS
-------------
See BUILD_HYPERSHELF.COM procedure.


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

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

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

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


VERSION HISTORY (update SOFTWAREVN as well!)
---------------
04-MAR-2022  MGD  v5.0.2, alert() [Close] for other than origin server
31-AUG-2020  MGD  v5.0.1, bugfix; eliminate &referer= XSS vulnerability
06-OCT-2014  MGD  v5.0.0, although now largely only of historical interest ...
                            ... a nod to the twenty-first century
                            (and happy 90th birthday Dad)
10-MAY-2005  MGD  v4.7.8, SWS 2.0 ignore query string components supplied as
                          command-line parameters differently to CSWS 1.2/3
23-DEC-2003  MGD  v4.7.7, minor conditional mods to support IA64
27-APR-2003  MGD  v4.7.6, change DEFAULT_ICON to all lower-case for CSWS
12-APR-2003  MGD  v4.7.5, link colour changed to 0000cc
20-AUG-2002  MGD  v4.7.4, allow for HTTP/1.1 'Cache-control:' field used
                          by Mozilla variants (instead of HTTP/1.0 'pragma:')
15-AUG-2002  MGD  v4.7.3, make MFD ('000000') suppression WASD-only
30-JUN-2002  MGD  v4.7.2, bugfix; link path in ProcessShelf()
01-JUL-2001  MGD  v4.7.1, add 'SkipParameters' for direct OSU support
28-OCT-2000  MGD  v4.7.0, use CGILIB object module
31-AUG-2000  MGD  v4.6.0, add URL as bookshelf entry type (good suggestion
                          Gerry..Czadowski@@nav-international..com)
18-JAN-2000  MGD  v4.5.0, support extended file specifications (ODS-5)
07-AUG-1999  MGD  v4.4.0, use more of the CGILIB functionality
24-APR-1999  MGD  v4.3.0, use CGILIB.C,
                          standard CGI environment (Netscape FastTrack)
02-OCT-1998  MGD  v4.2.0, provide content-type "; charset=..."
27-AUG-1998  MGD  v4.1.1, iteratively translate DECW$BOOK translations
08-AUG-1998  MGD  v4.1.0, refinements in shelf and library parsing,
                          OSU output processing reworked,
                          bugfix; TimeSetTimezone() 'Seconds' unsigned->signed
06-AUG-1998  MGD  v4.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  v4.0.1, suppress table background colours if empty,
                          accomodations for OSU environment
29-APR-1998  MGD  v4.0.0, remove the need for WASD MapUrl() functions
                          (making it a GENERIC CGI script!!),
                          some tidying up and cosmetic changes
26-FEB-1998  MGD  v3.3.5, TimeSetGmt() modified to be in line with HTTPd
06-NOV-1997  MGD  v3.3.4, discovered "hyperhelp" in latest 7.1 ODL
                          added "unknown" document type
19-AUG-1997  MGD  v3.2.2, MapUrl() to MapUrl_Map() for conditional mapping
01-AUG-1997  MGD  v3.2.1, 'HttpHasBeenOutput' not initialize for CGIplus
20-JUL-1997  MGD  v3.2.0, added /BODY= qualifier, added Adobe PDF document type
07-JUL-1997  MGD  v3.1.0, CGIplus capable
30-JUN-1997  MGD  v3.0.0, major changes to support the BNU format shelves,
                          "Pragma: no-cache" now overrides "If-Modified-Since:"
16-MAY-1996  MGD  v2.2.3, change all .XBMs to transparent .GIFs
23-FEB-1996  MGD  v2.2.2, bugfix; after modifying the HTTPD$GMT format for the
                          HTTPd server I had forgotten about some scripts
22-NOV-1995  MGD  v2.2.1, discovered the TITLE keyword in libraries
12-OCT-1995  MGD  v2.2.0, add 'Referer:', 'Last-Modified:'/'If-Modified-Since:'
24-MAY-1995  MGD  v2.1.0, minor changes for AXP compatibility
15-APR-1995  MGD  v2.0.0, complete rewrite
10-JUN-1994  MGD  v1.0.0, initial development
*/
/*****************************************************************************/

#define SOFTWAREVN "5.0.2"
#define SOFTWARENM "HYPERSHELF"
#define SOFTWARECR "Copyright (C) 1996-2022 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>
#include <unixio.h>

/* VMS-related header files */
#include <descrip.h>
#include <lib$routines.h>
#include <libdef.h>
#include <lnmdef.h>
#include <rms.h>
#include <ssdef.h>
#include <stsdef.h>
#include <syidef.h>
#include <starlet.h>

/* application header file */
#include "enamel.h"
#include "hyperreader.h"
#include "cgilib.h"

#ifndef __VAX
#  pragma nomember_alignment
#endif

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

#define FI_LI __FILE__, __LINE__

#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 VMSok(x) ((x) & STS$M_SUCCESS)
#define VMSnok(x) (!((x) & STS$M_SUCCESS))
 
#define DEFAULT_ICON "/hypershelf/-/"

char  *BookshelfTypes [] = { ".ODL", ".BKS", ".DECW$BOOKSHELF", "" };

char OdlShelfDefaults [] =
"DECW$USER_DEFAULTS:LIBRARY.ODL, \
DECW$USER_DEFAULTS:CONTENTS.ODL, \
DECW$SYSTEM_DEFAULTS:LIBRARY.ODL, \
DECW$SYSTEM_DEFAULTS:CONTENTS.ODL";

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

char  ErrorBookshelfType [] = "Doesn't look like a bookshelf type!";

char  Utility [] = "HYPERSHELF";

boolean  Debug,
         DecwBookDefined,
         DecwBookshelfDefined,
         DoBnu,
         DoBookreader,
         ErrorReported,
         IsCgiPlus,
         StdCgiEnvironment,
         PageBegun,
         TimeAheadOfGmt,
         WasdEnvironment;

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

int  BookshelfCount,
     OdsExtended,
     PrevBookshelfCount;

char  ContentTypeCharset [64],
      DefaultButtons [] = DEFAULT_BUTTONS,
      DefaultStyle [] = DEFAULT_HYPERSHELF_STYLE,
      LastModifiedGmDateTime [32],
      HtmlReferer [512],
      HyperReaderReferer [512],
      PrevTitle [256],
      ShelfPath [ODS_MAX_FILE_NAME_LENGTH+1],
      SoftwareID [48],
      TimeGmtString [32],
      TimeGmtVmsString [32],
      UrlEncReferer [512],
      UrlTitle [256],
      ShelfSpec [ODS_MAX_FILE_NAME_LENGTH+1];

char  *ButtonsPtr = DefaultButtons,
      *CgiEnvironmentPtr,
      *CgiFormFilePtr,
      *CgiFormRefererPtr,
      *CgiFormTitlePtr,
      *CgiHttpCacheControlPtr,
      *CgiHttpIfModifiedSincePtr,
      *CgiHttpPragmaPtr,
      *CgiPathInfoPtr,
      *CgiPathTranslatedPtr,
      *CgiRequestMethodPtr,
      *CgiScriptNamePtr,
      *CgiServerSoftwarePtr,
      *CharsetPtr,
      *CliCharsetPtr,
      *HyperReaderScriptNamePtr,
      *IconLocationPtr = DEFAULT_ICON,
      *LibraryFileNamePtr = "",
      *PrintScriptNamePtr = "", 
      *SoftwareCopy = SOFTWARECR,
      *StyleSheetPtr = "";

/* required function prototypes */
char* VmsToPath (char*, char*);
char* ButtonBarButton (char*, char*);
char* XSSuspect (char*);

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

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

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

   if (getenv ("HYPERSHELF$DBUG")) Debug = true;
   CgiLibEnvironmentSetDebug (Debug);

   CgiLibEnvironmentInit (argc, argv, false);

   WasdEnvironment = CgiLibEnvironmentIsWasd();

   GetParameters ();

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

   if (StyleSheetPtr[0])
   {
      char *cptr = calloc (1, 64+strlen(StyleSheetPtr));
      sprintf (cptr, "<link rel=\"stylesheet\" \
type=\"text/css\" href=\"%s\">\n",
               StyleSheetPtr);
      StyleSheetPtr = cptr;
   }

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

   /* if not CLI set and old bookreader logicals then bookreader */
   DecwBookDefined = (boolean)(getenv ("DECW$BOOK"));
   DecwBookshelfDefined = (boolean)(getenv ("DECW$BOOKSHELF"));
   if (!(DoBnu || DoBookreader) && (DecwBookDefined || DecwBookshelfDefined))
   {
      DoBookreader = true;
      DoBnu = false;
   }
   else
   {
      DoBnu = true;
      DoBookreader = false;
   }
   if (Debug)
      fprintf (stdout,
"DecwBookDefined: %d DecwBookshelfDefined: %d DoBookreader: %d DoBnu: %d\n",
         DecwBookDefined, DecwBookshelfDefined, DoBookreader, DoBnu);

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

   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 ("HYPERSHELF$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, "/DBUG", -1))
      {
         Debug = true;
         continue;
      }
      if (strsame (aptr, "/BNU", -1))
      {
         DoBookreader = false;
         DoBnu = true;
         continue;
      }
      if (strsame (aptr, "/BOOKREADER", 4))
      {
         DoBookreader = true;
         DoBnu = false;
         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, "/HYPERREADER=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         HyperReaderScriptNamePtr = cptr;
         continue;
      }
      if (strsame (aptr, "/ICON=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         IconLocationPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/LIBRARY=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         LibraryFileNamePtr = cptr;
         continue;
      }
      if (strsame (aptr, "/ODS5", 5))
      {
         OdsExtended = true;
         continue;
      }
      if (strsame (aptr, "/NOODS5", 7))
      {
         OdsExtended = false;
         continue;
      }
      if (strsame (aptr, "/PRINT=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         PrintScriptNamePtr = cptr;
         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);
      }
   }
}

/*****************************************************************************/
/*
*/ 
 
ProcessRequest ()

{
   int  status,
        idx;
   char  c;
   char  *cptr, *sptr, *zptr,
         *ShelfFilePtr;
   char  ExpFileName [ODS_MAX_FILE_NAME_LENGTH+1],
         ResFileName [ODS_MAX_FILE_NAME_LENGTH+1],
         Scratch [256],
         SearchDefault [ODS_MAX_FILE_NAME_LENGTH+1];
   struct FAB  SearchFab;
#ifdef ODS_EXTENDED
   struct NAML  SearchNaml;
#endif /* ODS_EXTENDED */
   struct NAM  SearchNam;

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

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

   CgiEnvironmentPtr = CgiLibEnvironmentName ();

   ErrorReported = false;

   CgiServerSoftwarePtr = CgiLibVar ("WWW_SERVER_SOFTWARE");
   CgiRequestMethodPtr = CgiLibVar ("WWW_REQUEST_METHOD");
   if (strcmp (CgiRequestMethodPtr, "GET"))
   {
      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");
   CgiHttpCacheControlPtr = CgiLibVar ("WWW_HTTP_CACHE_CONTROL");
   CgiHttpPragmaPtr = CgiLibVar ("WWW_HTTP_PRAGMA");

   CgiFormTitlePtr = CgiLibVar ("WWW_FORM_TITLE");
   CgiFormFilePtr = CgiLibVar ("WWW_FORM_FILE");
   if (!CgiFormFilePtr[0]) CgiFormFilePtr = CgiLibVar ("WWW_FORM_SHELF");

   if (VMSnok (status = TimeSetGmt ()))
   {
      if (status != SS$_NOLOGNAM)
      {
         CgiLibResponseError (FI_LI, status, "GMT offset");
         return;
      }
   }
   CgiHttpIfModifiedSincePtr = CgiLibVar ("WWW_HTTP_IF_MODIFIED_SINCE");
   if (CgiHttpIfModifiedSincePtr[0])
   {
      if (VMSnok (HttpGmTime (CgiHttpIfModifiedSincePtr, 
                              &IfModifiedSinceBinaryTime)))
      {
         if (Debug) fprintf (stdout, "If-Modified-Since: NBG!\n");
         IfModifiedSinceBinaryTime[0] = IfModifiedSinceBinaryTime[0] = 0;
         CgiHttpIfModifiedSincePtr = "";
      }
   }

   if (!HyperReaderScriptNamePtr)
   {
      if (CgiLibEnvironmentIsWasd() ||
          CgiLibEnvironmentIsCgiPlus())
         HyperReaderScriptNamePtr = DEFAULT_WASD_HYPERREADER;
      else
      if (CgiLibEnvironmentIsOsu())
         HyperReaderScriptNamePtr = DEFAULT_OSU_HYPERREADER;
      else
         HyperReaderScriptNamePtr = DEFAULT_CGI_HYPERREADER;
   }

   if (CgiPathInfoPtr[0] == '/' && !CgiPathInfoPtr[1])
      CgiPathInfoPtr = CgiPathTranslatedPtr = "";

   if (!CgiFormTitlePtr[0]) CgiFormTitlePtr = "Library";

   CgiFormRefererPtr = CgiLibVar ("WWW_FORM_REFERER");
   if (!CgiFormRefererPtr[0])
      CgiFormRefererPtr = CgiLibVar ("WWW_HTTP_REFERER");
   if (cptr = XSSuspect (CgiFormRefererPtr))
   {
      CgiLibResponseError (FI_LI, 0, cptr);
      return;
   }

   if (CgiFormRefererPtr[0])
   {
      /* re-escape the URL-escaped percentages */
      CgiLibUrlEncode (CgiFormRefererPtr, -1,
                       UrlEncReferer, sizeof(UrlEncReferer));

      /* check that any referer is for the same server */
      for (cptr = UrlEncReferer; *cptr && *(short*)cptr != '//'; cptr++);
      if (cptr) cptr += 2;
      sptr = CgiLibVar ("WWW_SERVER_NAME");
      while (*cptr && *sptr && tolower(*cptr) == tolower(*sptr))
      {
         cptr++;
         sptr++;
      }
      if ((*cptr && *cptr != '/') || *sptr)
      {
         /* not same server, substitute alert() */
         zptr = (sptr = HtmlReferer) + sizeof(HtmlReferer)-8;
         for (cptr = "javascript:alert(\\\'"; *cptr; *sptr++ = *cptr++);
         for (cptr = UrlEncReferer; *cptr && sptr < zptr; *sptr++ = *cptr++);
         for (cptr = "\\\');"; *cptr; *sptr++ = *cptr++);
         *sptr = '\0';
      }
      else
         CgiLibHtmlEscape (CgiFormRefererPtr, -1,
                           HtmlReferer, sizeof(HtmlReferer));
   }
   else
      UrlEncReferer[0] = HtmlReferer[0] = '\0';

   ShelfSpec[0] = '\0';
   if (!ShelfSpec[0]) strcpy (ShelfSpec, CgiFormFilePtr);
   if (!ShelfSpec[0]) strcpy (ShelfSpec, CgiPathTranslatedPtr);
   if (!ShelfSpec[0]) strcpy (ShelfSpec, LibraryFileNamePtr);
   if (Debug) fprintf (stdout, "ShelfSpec |%s|\n", ShelfSpec);

   if (ShelfSpec[0])
   {
      /* find file type (extension) */
      for (cptr = ShelfSpec; *cptr; cptr++);
      if (*(cptr-1) == ';') *(cptr-1) = '\0';
      while (cptr > ShelfSpec && *cptr != '.' &&
             *cptr != ']' && *cptr != ':') cptr--;
      if (*cptr == '.')
      {
         /* check if looks like a bookshelf file type */
         for (idx = 0; BookshelfTypes[idx][0]; idx++)
         {
            if (Debug) fprintf (stdout, "|%s|%s|\n", BookshelfTypes[idx], cptr);
            if (strsame (BookshelfTypes[idx], cptr, -1)) break;
         }
         if (!BookshelfTypes[idx][0])
         {
            CgiLibResponseError (FI_LI, 0, ErrorBookshelfType);
            return;
         }
      }
   }

   PageBegun = false;
   BookshelfCount = PrevBookshelfCount = 0;

   if (DoBookreader)
   {
      /**************/
      /* Bookreader */
      /**************/

      idx = 0;
      for (;;)
      {
         SearchFab = cc$rms_fab;

         if (ShelfSpec[0])
         {
            SearchFab.fab$l_dna = "DECW$BOOK:.DECW$BOOKSHELF";
            SearchFab.fab$b_dns = 25;
         }
         else
         {
            if (DecwBookshelfDefined)
            {
               SysTrnLnm ("DECW$BOOKSHELF", idx++, SearchDefault);
               if (!SearchDefault[0]) break;
            }
            else
            {
               SysTrnLnm ("DECW$BOOK", idx++, SearchDefault);
               if (!SearchDefault[0]) break;
               for (;;)
               {
                  SysTrnLnm (SearchDefault, 0, Scratch);
                  if (!Scratch[0]) break;
                  strcpy (SearchDefault, Scratch);
               }
               strcat (SearchDefault,"LIBRARY.*;");
            }
            SearchFab.fab$l_dna = SearchDefault;
            SearchFab.fab$b_dns = strlen(SearchDefault);
         }

         SearchFab.fab$b_shr = FAB$M_SHRGET;

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

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

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

         status = sys$parse (&SearchFab, 0, 0);
         if (Debug) fprintf (stdout, "sys$parse() %%X%08.08X\n", status);
         if (VMSnok (status))
         {
            /* subsequent translations ignore directories not being there! */
            if (status == RMS$_DNF && idx > 0) continue;

            if (ShelfSpec[0])
               cptr = ShelfSpec;
            else
               cptr = SearchFab.fab$l_dna;

            CgiLibResponseError (FI_LI, status, cptr);

            return (status);
         }

#ifdef ODS_EXTENDED
         if (OdsExtended)
            SearchNaml.naml$l_long_ver[SearchNaml.naml$l_long_ver_size] = '\0';
         else
#endif /* ODS_EXTENDED */
            SearchNam.nam$l_ver[SearchNam.nam$b_ver] = '\0';
         if (Debug)
            fprintf (stdout, "ExpFileName |%s|\n", ExpFileName);

         status = sys$search (&SearchFab, 0, 0);
         if (Debug) fprintf (stdout, "sys$search() %%X%08.08X\n", status);
         if (VMSok (status))
         {
#ifdef ODS_EXTENDED
            if (OdsExtended)
               *SearchNaml.naml$l_long_ver = '\0';
            else
#endif /* ODS_EXTENDED */
               *SearchNam.nam$l_ver = '\0';
            if (Debug) fprintf (stdout, "ResFileName |%s|\n", ResFileName);

            status = ProcessShelf (ResFileName);
            if (Debug) fprintf (stdout, "ProcessShelf() %%X%08.08X\n", status);
         }

         if (status == RMS$_NMF) status = SS$_NORMAL;
         if (VMSnok (status))
         {
            if (ShelfSpec[0])
               cptr = ShelfSpec;
            else
               cptr = SearchFab.fab$l_dna;
            CgiLibResponseError (FI_LI, status, cptr);
            return (SS$_NORMAL);
         }

         /* continue to go through this rigmoral only if no shelf specified! */
         if (ShelfSpec[0]) break;
      }
   }
   else
   {
      /*******/
      /* BNU */
      /*******/

      ShelfFilePtr = OdlShelfDefaults;
      while (*ShelfFilePtr)
      {
         cptr = ShelfFilePtr;
         while (*cptr == ' ' || *cptr == ',') cptr++;
         if (!*cptr) break;
         if (Debug) fprintf (stdout, "cptr |%s|\n", cptr);
         ShelfFilePtr = cptr;
         while (*cptr && *cptr != ' ' && *cptr != ',') cptr++;
         c = *cptr;

         SearchFab = cc$rms_fab;
         SearchFab.fab$l_dna = ShelfFilePtr;
         SearchFab.fab$b_dns = cptr - ShelfFilePtr;
         SearchFab.fab$b_shr = FAB$M_SHRGET;

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

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

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

         if (VMSnok (status = sys$parse (&SearchFab, 0, 0)))
         {
            if (ShelfSpec[0])
               CgiLibResponseError (FI_LI, status, ShelfSpec);
            else
               CgiLibResponseError (FI_LI, status, ShelfFilePtr);
            *cptr = c;
            return (status);
         }

         *(ShelfFilePtr = cptr) = c;

#ifdef ODS_EXTENDED
         if (OdsExtended)
            SearchNaml.naml$l_long_ver[SearchNaml.naml$l_long_ver_size] = '\0';
         else
#endif /* ODS_EXTENDED */
            SearchNam.nam$l_ver[SearchNam.nam$b_ver] = '\0';
         if (Debug)
            fprintf (stdout, "ExpFileName |%s|\n", ExpFileName);

         status = sys$search (&SearchFab, 0, 0);
         if (Debug) fprintf (stdout, "sys$search() %%X%08.08X\n", status);
         if (VMSok (status))
         {
#ifdef ODS_EXTENDED
            if (OdsExtended)
               *SearchNaml.naml$l_long_ver = '\0';
            else
#endif /* ODS_EXTENDED */
               *SearchNam.nam$l_ver = '\0';
            if (Debug) fprintf (stdout, "ResFileName |%s|\n", ResFileName);

            status = ProcessShelf (ResFileName);
            if (Debug) fprintf (stdout, "ProcessShelf() %%X%08.08X\n", status);
            break;
         }

         /* try with the next combination of defaults (if any) */
         if (status == RMS$_FNF) continue;
         if (status == RMS$_DNF) continue;
         break;
      }

      if (!*ShelfFilePtr)
      {
         status = RMS$_FNF;
         CgiLibResponseError (FI_LI, status, OdlShelfDefaults);
         return (status);
      }
      else
      {
         if (VMSnok (status))
         {
            if (ShelfSpec[0])
               cptr = ShelfSpec;
            else
               cptr = ExpFileName;
            CgiLibResponseError (FI_LI, status, cptr);
            return (status);
         }
      }
   }

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

   if (VMSok (status) && BookshelfCount)
   {
      ButtonBar (2);
      fprintf (stdout, "</body>\n</html>\n");
   }
   else
   if (VMSnok (status))
   {
      if (ShelfSpec[0])
         cptr = ShelfSpec;
      else
      {
#ifdef ODS_EXTENDED
         if (OdsExtended)
            SearchNaml.naml$l_long_ver[SearchNaml.naml$l_long_ver_size] = '\0';
         else
#endif /* ODS_EXTENDED */
            SearchNam.nam$l_ver[SearchNam.nam$b_ver] = '\0';
         cptr = ExpFileName;
      }
      CgiLibResponseError (FI_LI, status, cptr);
   }

   /* release any parse and search internal data structures (for CGIplus) */
   SearchFab.fab$l_fna = "a:[b]c.d;";
   SearchFab.fab$b_fns = 9;
   SearchFab.fab$b_dns = 0;
#ifdef ODS_EXTENDED
   if (OdsExtended)
      SearchNaml.naml$b_nop = NAM$M_SYNCHK;
   else
#endif /* ODS_EXTENDED */
      SearchNam.nam$b_nop = NAM$M_SYNCHK;
   sys$parse (&SearchFab, 0, 0);
}

/*****************************************************************************/
/*
Open the supplied shelf file name.  Read each line in the  file parsing the
entry type (shelf or book), the file specification, and entry  description
(title of shelf or book).  Close the file.
*/ 
 
int ProcessShelf (char* ShelfFileName)

{
   boolean  IsDecwShelf,
            IsOdlShelf,
            IsSameAsPrevTitle;
   int  status,
        idx,
        LineCount,
        LineOutputCount;
   char  ExpFileName [ODS_MAX_FILE_NAME_LENGTH+1],
         FileName [ODS_MAX_FILE_NAME_LENGTH+1],
         HtmlTitle [256],
         Line [256],
         LinkPath [ODS_MAX_FILE_NAME_LENGTH+1],
         Scratch [ODS_MAX_FILE_NAME_LENGTH*2],
         ShelfDirectory [ODS_MAX_FILE_NAME_LENGTH+1],
         Title [256],
         Type [256],
         VmsLinkPath [ODS_MAX_FILE_NAME_LENGTH+1];
   char  *cptr, *sptr, *zptr,
         *ExpDevicePtr,
         *ExpNodePtr,
         *ExpNamePtr,
         *ExpTypePtr,
         *PossibleOdlShelfTitlePtr;
   struct FAB  ShelfFab;
#ifdef ODS_EXTENDED
   struct NAML  ShelfNaml;
#endif /* ODS_EXTENDED */
   struct NAM  ShelfNam;
   struct RAB  ShelfRab;
   struct XABDAT  ShelfXabDat;

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

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

   ShelfFab = cc$rms_fab;
   ShelfFab.fab$b_fac = FAB$M_GET;
   ShelfFab.fab$b_shr = FAB$M_SHRGET;
   ShelfFab.fab$l_xab = &ShelfXabDat;
   ShelfXabDat = cc$rms_xabdat;

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

      ENAMEL_RMS_NAML(ShelfNaml)
      ShelfNaml.naml$l_long_filename = ShelfFileName;
      ShelfNaml.naml$l_long_filename_size = strlen(ShelfFileName);
      ShelfNaml.naml$l_long_expand = ExpFileName;
      ShelfNaml.naml$l_long_expand_alloc = sizeof(ExpFileName)-1;
   }
   else
#endif /* ODS_EXTENDED */
   {
      ShelfFab.fab$l_fna = ShelfFileName;  
      ShelfFab.fab$b_fns = strlen(ShelfFileName);
      ShelfFab.fab$l_nam = &ShelfNam;

      ShelfNam = cc$rms_nam;
      ShelfNam.nam$l_esa = ExpFileName;
      ShelfNam.nam$b_ess = ODS2_MAX_FILE_NAME_LENGTH;
   }

   status = sys$open (&ShelfFab, 0, 0);

   if (VMSnok (status))
   {
      if (Debug) fprintf (stdout, "sys$open() %%X%08.08X\n", status);
      if (ShelfSpec[0])
         cptr = ShelfSpec;
      else
         cptr = ShelfFileName;
      CgiLibResponseError (FI_LI, status, cptr);
      return (status);
   }

#ifdef ODS_EXTENDED
   if (OdsExtended)
   {
      ExpNodePtr = ShelfNaml.naml$l_long_node;
      ExpDevicePtr = ShelfNaml.naml$l_long_dev;
      ExpNamePtr = ShelfNaml.naml$l_long_name;
      ExpTypePtr = ShelfNaml.naml$l_long_type;
      *ShelfNaml.naml$l_long_ver = '\0';
   }
   else
#endif /* ODS_EXTENDED */
   {
      ExpNodePtr = ShelfNam.nam$l_node;
      ExpDevicePtr = ShelfNam.nam$l_dev;
      ExpNamePtr = ShelfNam.nam$l_name;
      ExpTypePtr = ShelfNam.nam$l_type;
      *ShelfNam.nam$l_ver = '\0';
   }
   if (Debug) fprintf (stdout, "|%s|\n", ExpFileName);

   /* don't last-modified check if using DECW$BOOK to search for libraries */
   if ((ShelfSpec[0] ||
        DoBnu) &&
       !DecwBookshelfDefined)
   {
      if (CgiHttpIfModifiedSincePtr[0])
      {
         if (VMSnok (ModifiedSince (&IfModifiedSinceBinaryTime,
                                    &ShelfXabDat.xab$q_rdt)))
         {
            /* book has not been modified since the date/time, don't send */
            sys$close (&ShelfFab, 0, 0);
            return (STS$K_ERROR);
         }
      }
   }

   HttpGmTimeString (LastModifiedGmDateTime, &ShelfXabDat.xab$q_rdt);

   ShelfRab = cc$rms_rab;
   ShelfRab.rab$l_fab = &ShelfFab;
   /* 2 buffers and read ahead performance option */
   ShelfRab.rab$b_mbf = 2;
   ShelfRab.rab$l_rop = RAB$M_RAH;
   ShelfRab.rab$l_ubf = Line;
   ShelfRab.rab$w_usz = sizeof(Line)-1;

   if (VMSnok (status = sys$connect (&ShelfRab, 0, 0)))
   {
      if (Debug) fprintf (stdout, "sys$connect() %%X%08.08X\n", status);
      sys$close (&ShelfFab, 0, 0);
      if (ShelfSpec[0])
         cptr = ShelfSpec;
      else
         cptr = ShelfFileName;
      CgiLibResponseError (FI_LI, status, cptr);
      return (status);
   }

   /* convert the VMS file path into a slash-separated, sort-of equivalent */
   VmsToPath (ShelfPath, ExpFileName);

   if (Debug) fprintf (stdout, "|%s|\n", ExpTypePtr);
   IsDecwShelf = IsOdlShelf = false;
   if (!strcmp (ExpTypePtr, ".ODL"))
      IsOdlShelf = true;
   else
      IsDecwShelf = true;

   if (++BookshelfCount == 1)
   {
      /********************/
      /* first/only shelf */
      /********************/

      sptr = HyperReaderReferer;
      sptr += CgiLibUrlEncode (CgiScriptNamePtr, -1, sptr, -1);
      if (CgiPathInfoPtr[0] != '/') *sptr++ = '/';
      sptr += CgiLibUrlEncode (CgiPathInfoPtr, -1, sptr, -1);
      sptr += CgiLibUrlEncode ("?title=", -1, sptr, -1);
      /*
         This escapes the escape percentages leaving the URI still escaped
         when the script (HyperReader) receives it at the other end :^)
      */
      CgiLibUrlEncode (CgiFormTitlePtr, -1, Scratch, -1);
      sptr += CgiLibUrlEncode (Scratch, -1, sptr, -1);
      sptr += CgiLibUrlEncode ("&referer=", -1, sptr, -1);
      sptr += CgiLibUrlEncode (UrlEncReferer, -1, sptr, -1);
      *sptr = '\0';
      if (Debug)
         fprintf (stdout, "HyperReaderReferer |%s|\n", HyperReaderReferer);

      /***************/
      /* HTTP header */
      /***************/

      CgiLibResponseHeader (200, "text/html",
                            "Last-Modified: %s\n", LastModifiedGmDateTime);
   }

   /****************/
   /* file records */
   /****************/

   LineCount = LineOutputCount = 0;
   HtmlTitle[0] = PrevTitle[0] = UrlTitle[0] = '\0';

   while (VMSok (status = sys$get (&ShelfRab, 0, 0)))
   {
      LineCount++;
      Line[ShelfRab.rab$w_rsz] = '\0';
      if (Debug) fprintf (stdout, "Line |%s|\n", Line);
      if (!Line[0]) continue;

      /********************/
      /* parse components */
      /********************/

      cptr = Line;
      if ((cptr[0] == '#' && cptr[1] == '#' && isalpha(cptr[2])) ||
          (cptr[0] == '!' && cptr[1] == '!' && isalpha(cptr[2])))
         cptr += 2;

      /* ignore unknown line types */
      if (!isalpha(*cptr)) continue;

      Type[0] = FileName[0] = Title[0] = '\0';

      sptr = Type;
      while (*cptr && isspace(*cptr)) cptr++;
      if (IsOdlShelf)
      {
         while (*cptr && !isspace(*cptr)) *sptr++ = toupper(*cptr++);
         *sptr = '\0';
         while (*cptr && isspace(*cptr)) cptr++;
      }
      else
      {
         while (*cptr && *cptr != '\\' && !isspace(*cptr))
            *sptr++ = toupper(*cptr++);
         *sptr = '\0';
         while (*cptr && isspace(*cptr)) cptr++;
         if (*cptr == '\\') cptr++;
         while (*cptr && isspace(*cptr)) cptr++;
      }

      PossibleOdlShelfTitlePtr = cptr;
      if (IsOdlShelf)
      {
         sptr = FileName;
         while (*cptr && !isspace(*cptr))
         {
            *sptr++ = *cptr++;
            if (*cptr == '%')
            {
               *sptr++ = '2';
               *sptr++ = '5';
            }
         }
         *sptr = '\0';
         while (*cptr && isspace(*cptr)) cptr++;
      }
      else
      {
         /* check if this specification has device and directory components */
         for (zptr = cptr;
              *zptr && *zptr != ':' && *zptr != '[' && *zptr != '/' &&
                 *zptr != '\\' && !isspace(*zptr);
              zptr++);
         if (*zptr == ':' || *zptr == '[' || *zptr == '/')
         {
            /* includes directory components, straight-up copy */
            sptr = FileName;
            while (*cptr && *cptr != '\\' && !isspace(*cptr))
               *sptr++ = tolower(*cptr++);
            *sptr = '\0';
         }
         else
         {
            /* use the device and directory of the current shelf */
            sptr = FileName;
            for (zptr = ExpNodePtr;
                 zptr < ExpNamePtr;
                 *sptr++ = tolower(*zptr++));
            while (*cptr && *cptr != '\\' && !isspace(*cptr))
               *sptr++ = tolower(*cptr++);
            *sptr = '\0';
         }
         while (*cptr && isspace(*cptr)) cptr++;
         if (*cptr == '\\') cptr++;
         while (*cptr && isspace(*cptr)) cptr++;
      }

      sptr = Title;
      while (*cptr) *sptr++ = *cptr++;
      *sptr = '\0';

      /***************/
      /* shelf title */
      /***************/

      if (Type[0] == 'T' && strsame (Type, "TITLE", -1))
      {
         /* title of this shelf */
         if (IsOdlShelf)
         {
            sptr = Title;
            for (cptr = PossibleOdlShelfTitlePtr; *cptr; *sptr++ = *cptr++);
            *sptr = '\0';
            cptr = "ODL";
         }
         else
            cptr = "DECW$BOOKSHELF";
         if (!Title[0])
            sprintf (Title, "[Unable to parse %s shelf title (line %d)]",
                     cptr, LineCount);
         
         if (Debug) fprintf (stdout, "TITLE |%s|\n", Title);

         if (!HtmlTitle[0])
         {
            CgiLibHtmlEscape (Title, -1, HtmlTitle, -1);
            CgiLibUrlEncode (Title, -1, UrlTitle, -1);
         }
      }

      if (BookshelfCount == 1)
      {
         /***********************************/
         /* first/only shelf ... page title */
         /***********************************/

         if (!PageBegun)
         {
            BeginPage (HtmlTitle);
            PageBegun = true;
         }
      }

      if (BookshelfCount == 1 && BookshelfCount != PrevBookshelfCount)
      {
         /* note that we are now working on a new bookshelf file */
         PrevBookshelfCount = BookshelfCount;
         fprintf (stdout, "<!-- %s -->\n", ExpFileName);
      }

      if (BookshelfCount > 1)
      {
         /* Bookreader type search list through libraries */
         if (Type[0] == 'T' && strsame (Type, "TITLE", -1)) break;
         continue;
      }

      if (Type[0] == 'T' && strsame (Type, "TITLE", -1)) continue;


      /*********************/
      /* shelf or document */
      /*********************/

      CgiLibHtmlEscape (Title, -1, HtmlTitle, -1);
      CgiLibUrlEncode (Title, -1, UrlTitle, -1);

      if (strsame (Title, PrevTitle, -1) || strsame (Title, "ditto", 5))
         IsSameAsPrevTitle = true;
      else
         IsSameAsPrevTitle = false;

      LinkPath[0] = VmsLinkPath[0] = '\0';
      if (FileName[0])
      {
         if (IsOdlShelf)
         {
            /*************/
            /* ODL shelf */
            /*************/

            if (FileName[0] == '%')
            {
               /* full path was specified */
               sptr = LinkPath;
               for (cptr = "/disk$"; *cptr; *sptr++ = *cptr++);
               for (cptr = FileName+1; *cptr; *sptr++ = tolower(*cptr++));
               *sptr = '\0';
            }
            else
            if (FileName[0] == '/')
            {
               /* full path was specified */
               sptr = LinkPath;
               for (cptr = FileName; *cptr; *sptr++ = tolower(*cptr++));
               *sptr = '\0';
            }
            else
            {
               sptr = LinkPath;
               for (cptr = CgiPathInfoPtr; *cptr; *sptr++ = *cptr++);
               *sptr = '\0';
               while (*sptr != '/' && sptr > LinkPath) sptr--;
               if (*sptr == '/') sptr++;
               for (cptr = FileName; *cptr; *sptr++ = tolower(*cptr++));
               *sptr = '\0';
            }
            if (Debug) fprintf (stdout, "LinkPath |%s|\n", LinkPath);
         }
         else
         {
            /************************/
            /* DECW$BOOKSHELF shelf */
            /************************/

            strcpy (VmsLinkPath, FileName);
         }
      }

      if (Debug)
         fprintf (stdout, "|%s|%s|%s|\n", FileName, LinkPath, VmsLinkPath);

      /*******************/
      /* output the HTML */
      /*******************/

      LineOutputCount++;

      fprintf (stdout, "<tr><td>\n");

      if (!Type[0] || !FileName[0])
         fprintf (stdout, "ERROR-IN-SHELF-ENTRY: Line %d", LineCount);
      else
      if (Type[0] == 'S' && strsame (Type, "SHELF", -1))
      {
         /*********/
         /* shelf */
         /*********/

         if (IsOdlShelf)
            ItemLink (UrlTitle, CgiScriptNamePtr, LinkPath, ".odl",
                      "shelf.gif", " _|\\_");
         else
            ItemLink (UrlTitle, CgiScriptNamePtr, VmsLinkPath,
                      ".decw$bookshelf", "shelf.gif", "_|\\_");
      }
      else
      if (Type[0] == 'B' && strsame (Type, "BOOK", -1))
      {
         /********/
         /* book */
         /********/

         if (!Title[0] || IsSameAsPrevTitle)
            strcpy (HtmlTitle, "<FONT SIZE=-1><I>(Bookreader version)</I></FONT>");
                 
         if (IsOdlShelf)
            ItemLink (UrlTitle, HyperReaderScriptNamePtr, LinkPath,
                      ".bkb", "book.gif", "[}{]");
         else
            ItemLink (UrlTitle, HyperReaderScriptNamePtr, VmsLinkPath,
                      ".decw$book", "book.gif", "[}{]");
      }
      else
      if (Type[0] == 'H' && strsame (Type, "HTML", -1))
      {
         /****************/
         /* HTML version */
         /****************/

         if (!Title[0] || IsSameAsPrevTitle)
            strcpy (HtmlTitle, "<font size=-1><i>(html version)</i></font>");

         /* convert the VMS file path into a sort-of URL equivalent */
         if (VmsLinkPath[0]) VmsToPath (LinkPath, VmsLinkPath);

         if (IsOdlShelf) cptr = ".htm"; else cptr = ".html";
         ItemLink (NULL, "", LinkPath, cptr, "html.gif", "[htm]");
      }
      else
      if (Type[0] == 'P'  && strsame (Type, "PDF", -1))
      {
         /*********************************************/
         /* Adobe PDF version (HyperReader extension) */
         /*********************************************/

         if (!Title[0] || IsSameAsPrevTitle)
            strcpy (HtmlTitle, "<font size=-1><i>(pdf version)</i></font>");

         /* convert the VMS file path into a sort-of URL equivalent */
         if (VmsLinkPath[0]) VmsToPath (LinkPath, VmsLinkPath);

         ItemLink (NULL, "", LinkPath, ".pdf", "pdf.gif", "[pdf]");
      }
      else
      if (Type[0] == 'P'  && strsame (Type, "PLAIN", -1))
      {
         /**********************************************/
         /* plain-text version (HyperReader extension) */
         /**********************************************/

         if (!Title[0] || IsSameAsPrevTitle)
            strcpy (HtmlTitle,
                    "<font size=-1><i>(plain-text version)</i></font>");

         /* convert the VMS file path into a sort-of URL equivalent */
         if (VmsLinkPath[0]) VmsToPath (LinkPath, VmsLinkPath);

         ItemLink (NULL, "", LinkPath, ".txt", "plain.gif", "[txt]");

         if (PrintScriptNamePtr[0])
            ItemLink (NULL, PrintScriptNamePtr, LinkPath, ".txt",
                      "print.gif", "[prn]");
      }
      else
      if ((Type[0] == 'P' && strsame (Type, "POST", 4)) ||
          (Type[0] == 'P' && strsame (Type, "PS", -1)))
      {
         /**********************************************/
         /* PostScript version (HyperReader extension) */
         /**********************************************/

         if (!Title[0] || IsSameAsPrevTitle)
            strcpy (HtmlTitle, "<font size=-1><i>(PostScript version)</i></font>");

         /* convert the VMS file path into a sort-of URL equivalent */
         if (VmsLinkPath[0]) VmsToPath (LinkPath, VmsLinkPath);

         ItemLink (NULL, "", LinkPath, ".ps", "ps.gif", "[.ps]");

         if (PrintScriptNamePtr[0])
            ItemLink (NULL, PrintScriptNamePtr, LinkPath, ".ps",
                      "print.gif", "[prn]");
      }
      else
      if (Type[0] == 'U'  && strsame (Type, "URL", -1))
      {
         /*******************************************/
         /* URL (HTML link - HyperReader extension) */
         /*******************************************/

         if (!Title[0]) strcpy (HtmlTitle, FileName);

         ItemLink (NULL, "", FileName, NULL, "url.gif", "[url]");
      }
      else
      {
         /*****************/
         /* unknown agent */
         /*****************/

         if (!Title[0] || IsSameAsPrevTitle)
            strcpy (HtmlTitle, "<font size=-1><i>(ditto)</i></font>");

         ItemLink (UrlTitle, "", LinkPath, "", "unknown.gif", "[???]");
      }

      fprintf (stdout, "</td><td>%s</td></tr>\n", HtmlTitle);

      strcpy (PrevTitle, Title);
   }

   /****************/
   /* end of shelf */
   /****************/

   sys$close (&ShelfFab, 0, 0);

   if (status == RMS$_EOF) status = SS$_NORMAL;
   if (VMSnok (status))
   {
      CgiLibHtmlEscape (CgiFormTitlePtr, -1, HtmlTitle, -1);
      if (HtmlTitle[0])
         cptr = HtmlTitle;
      else
         cptr = CgiPathInfoPtr;
      CgiLibResponseError (FI_LI, status, cptr);
      return (status);
   }

   if (BookshelfCount > 1)
   {
      /************************************/
      /* bookreader search list libraries */
      /************************************/

      if (BookshelfCount != PrevBookshelfCount)
      {
         /* note that we are now working on a new bookshelf file */
         PrevBookshelfCount = BookshelfCount;
         fprintf (stdout, "<!-- %s -->\n", ExpFileName);
      }

      if (LineCount)
      {
         fprintf (stdout, "<tr><td>\n");
         if (IsOdlShelf)
            ItemLink (UrlTitle, CgiScriptNamePtr,
                      VmsToPath(NULL, ShelfFileName), ".odl",
                      "shelf.gif", " _|\\_");
         else
         {
            for (cptr = ShelfFileName; *cptr; cptr++) *cptr = tolower(*cptr);
            ItemLink (UrlTitle, CgiScriptNamePtr, ShelfFileName,
                      ".decw$bookshelf", "shelf.gif", "_|\\_");
         }
         if (HtmlTitle[0])
            fprintf (stdout,
"</td><td>&nbsp;%s\
 &nbsp;&nbsp;<font size=-1>[library]</font></td></tr>\n",
                     HtmlTitle);
         else
            fprintf (stdout,
"</td><td>&nbsp;library %d\
 &nbsp;&nbsp;<font size=-1>[library]</font></td></tr>\n",
                     BookshelfCount);
      }
      else
         fprintf (stdout,
            "<tr><td></td><td>&nbsp;Empty bookshelf: <tt>%s</tt></td></tr>\n",
            VmsToPath (NULL, ShelfFileName));
   }
   else
   if (!LineOutputCount)
   {
      if (!PageBegun)
      {
         BeginPage (HtmlTitle);
         PageBegun = true;
      }

      if (BookshelfCount != PrevBookshelfCount)
      {
         /* note that we are now working on a new bookshelf file */
         PrevBookshelfCount = BookshelfCount;
         fprintf (stdout, "<!-- %s -->\n", ExpFileName);
      }

      fprintf (stdout,
         "<tr><td></td><td>&nbsp;Empty bookshelf: <tt>%s</tt></td></tr>\n",
         VmsToPath (NULL, ShelfFileName));
   }

   return (SS$_NORMAL);
}

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

BeginPage (char *HtmlTitle)

{
   char  *cptr, *ShelfModePtr;
   char  UnixDateTime [32];
   unsigned long  UnixTime;
   struct tm  *UnixTmPtr;

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

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

   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);

   if (!HtmlTitle[0]) CgiLibHtmlEscape (CgiFormTitlePtr, -1, HtmlTitle, -1);

   if (DoBnu) ShelfModePtr = "BNU"; else ShelfModePtr = "Bookreader";

   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 (%s)\">\n\
<meta name=\"date\" content=\"%s\">\n\
<title>HyperShelf ... %s</title>\n\
<style type=\"text/css\">\n\
%s\
</style>\n\
%s\
</head>\n\
<body>\n",
      SoftwareID, SoftwareCopy,
      CgiEnvironmentPtr, ShelfModePtr, UnixDateTime,
      HtmlTitle,
      DefaultStyle,
      StyleSheetPtr);

   fprintf (stdout, "<div class=\"header\">%s</div>\n", HtmlTitle);
 
   ButtonBar (1);

   fflush (stdout);

   fputs ("<table class=\"shelves\" border=\"0\" \
cellpadding=\"0\" cellspacing=\"2\">\n", stdout);
}

/*****************************************************************************/
/*
Create a link for a single shelf item.
*/ 

ItemLink
(
char *UrlTitle,
char *ScriptNamePtr,
char *LinkPath,
char *DefaultType,
char *IconName,
char *AltText
)
{
   char  *cptr;
   char EncodedLinkPath [ODS_MAX_FILE_NAME_LENGTH+64+1];

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

   if (Debug)
      fprintf (stdout, "ItemLink() |%s|%s|%s|%s|%s|%s|\n",
               UrlTitle, ScriptNamePtr, LinkPath, DefaultType,
               IconName, AltText);

   if (!DefaultType)
   {
      /* default file type is specified as NULL, must be a raw URL */
      fprintf (stdout,
"<a href=\"%s\"><img src=\"%s%s\" align=\"top\" alt=\"%s\"></a>\n",
         LinkPath, IconLocationPtr, IconName, AltText);
      return;
   }

   /* look for file type in specification */
   for (cptr = LinkPath; *cptr; cptr++);
   while (cptr > LinkPath && *cptr != '.'
          && *cptr != '/' && *cptr != ']' && *cptr != ':')
      cptr--;
   if (*cptr != '.')
   {
      /* no file type supplied, default to one! */
      while (*cptr) cptr++;
      strcat (LinkPath, DefaultType);
   }
   /* now scan backwards to check for likely VMS specification */
   while (cptr > LinkPath && *cptr != '/' && *cptr != ']' && *cptr != ':')
      cptr--;

   if (*cptr == ']' || *cptr == ':')
   {
      /* link has VMS file specification */
      CgiLibUrlEncodeFileName (LinkPath,
                               EncodedLinkPath, sizeof(EncodedLinkPath),
                               true, true);

      if (!UrlTitle)
      {
         fprintf (stdout,
"<a href=\"%s?file=%s\">\
<img src=\"%s%s\" align=\"top\" alt=\"%s\" border=\"0\"></a>\n",
            ScriptNamePtr, EncodedLinkPath,
            IconLocationPtr, IconName, AltText);
      }
      else
      {
         fprintf (stdout,
"<a href=\"%s?file=%s&title=%s&referer=%s\">\
<img src=\"%s%s\" align=\"top\" alt=\"%s\" border=\"0\"></a>\n",
            ScriptNamePtr, EncodedLinkPath, UrlTitle, UrlEncReferer,
            IconLocationPtr, IconName, AltText);
      }
   }
   else
   {
      /* link has URL-style path */
      CgiLibUrlEncode (LinkPath, -1,
                       EncodedLinkPath, sizeof(EncodedLinkPath));

      if (!UrlTitle)
      {
         fprintf (stdout,
"<a href=\"%s%s\">\
<img src=\"%s%s\" align=\"top\" alt=\"%s\" border=\"0\"></a>\n",
            ScriptNamePtr, EncodedLinkPath,
            IconLocationPtr, IconName, AltText);
      }
      else
      {
         fprintf (stdout,
"<a href=\"%s%s?title=%s&referer=%s\">\
<img src=\"%s%s\" align=\"top\" alt=\"%s\" border=\"0\"></a>\n",
            ScriptNamePtr, EncodedLinkPath, UrlTitle, UrlEncReferer,
            IconLocationPtr, IconName, AltText);
      }
   }
}

/*****************************************************************************/
/*
Create the navigation buttons.
*/ 
ButtonBar (int Top1Bottom2)

{
   int  idx;
   char  *cptr, *sptr;

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

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

   fprintf (stdout, "<div class=\"buttonbar%s\">\n",
            Top1Bottom2 == 1 ? " butbartop" : "");

   cptr = ButtonsPtr;

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

   /* close */
   if (HtmlReferer[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);
}

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

int SysTrnLnm
(
char *LogicalName,
int IndexNumber,
char *LogicalValue
)
{
   static unsigned short  Length;
   static $DESCRIPTOR (LnmFileDevDsc, "LNM$FILE_DEV");
   static $DESCRIPTOR (LogicalNameDsc, "");
   static struct {
      short int  buf_len;
      short int  item;
      void   *buf_addr;
      unsigned short  *ret_len;
   } LnmItems [] =
   {
      { sizeof(int), LNM$_INDEX, 0, 0, },
      { 255, LNM$_STRING, 0, &Length },
      { 0,0,0,0 }
   };

   int  status;
   char  *SignPtr;

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

   if (Debug)
      fprintf (stdout, "SysTrnLnm() |%s| %d\n", LogicalName, IndexNumber);

   LogicalValue[0] = '\0';
   LogicalNameDsc.dsc$a_pointer = LogicalName;
   LogicalNameDsc.dsc$w_length = strlen(LogicalName);
   LnmItems[0].buf_addr = &IndexNumber;
   LnmItems[1].buf_addr = LogicalValue;
   status = sys$trnlnm (0, &LnmFileDevDsc, &LogicalNameDsc, 0, &LnmItems);
   if (Debug) fprintf (stdout, "sys$trnlnm() %%X%08.08X\n", status);
   if (VMSnok (status)) return (status);
   LogicalValue[Length] = '\0';
   if (Debug) fprintf (stdout, "LogicalValue |%s|\n", LogicalValue);
   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
Convert a VMS file specification into a URL-style specification.  For example:
"DEVICE:[DIR1.DIR2]FILE.TXT" into "/device/dir1/dir2/file.txt".  OSU and Apache
environments require a MFD (i.e. 000000) to remain in a Unix-stlye
specification, whereas with WASD it's optional (and usually not present).
*/ 
 
char* VmsToPath
(
char *PathPtr,
char *VmsPtr
)
{
   static char  Path [ODS_MAX_FILE_NAME_LENGTH+1];

   char  *pptr, *vptr;

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

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

   vptr = VmsPtr;
   if (!(pptr = PathPtr)) pptr = PathPtr = Path;
   *pptr++ = '/';
   /* copy the device and directory components */
   while (*vptr)
   {
      if (*vptr == ':' && *(vptr+1) == '[')
      {
         vptr++;
         vptr++;
         /* remove any reference to a Master File Directory */
         if (WasdEnvironment && strncmp (vptr, "000000", 6) == 0)
            vptr += 6;
         else
            *pptr++ = '/';
      }
      if (*vptr == '.')
      {
         if (vptr[1] == '.' && vptr[2] == '.')
         {
            *pptr++ = '/';
            *pptr++ = *vptr++;
            *pptr++ = *vptr++;
            *pptr++ = *vptr++;
         }
         else
         {
            vptr++;
            *pptr++ = '/';
         }
      }
      else
      if (*vptr == ']')
      {
         vptr++;
         *pptr++ = '/';
         break;
      }
      else
         *pptr++ = tolower(*vptr++);
   }
   /* copy the file component */
   while (*vptr) *pptr++ = tolower(*vptr++);
   *pptr++ = '\0';
   *pptr = '\0';

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

   return (PathPtr);
}

/*****************************************************************************/
/*
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, "sys$add_times()");
      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, "sys$sub_times()");
      return (status);
   }

   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;
   unsigned short  Length;
   unsigned short  NumTime [7] = { 0,0,0,0,0,0,0 };
   unsigned long  DayOfWeek;
   char  *tptr;

   /*********/
   /* 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';
      fputs (String, stdout);
   }

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

   return (status);
}

/****************************************************************************/
/*
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 unsigned 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);
}

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

