/*****************************************************************************/
/*
                                  msg.c

Provide a site-configurable message database.  This is particularly directed
towards non-English language sites.  No attempt has been made to completely
internationalize the server, only messages the average client is likely to be
exposed to are currently provided.

The only way of reloading the database is to restart the server. This is not
of great concern for once customized the messages should remain completely
stable between releases.

When multiple languages are configured the message language processing may be
observed using the WATCH response-header item.


MULTIPLE LANGUAGE SPECIFICATIONS
--------------------------------
Multiple language messages can be specified in two ways:

  1) within the one file
  2) in multiple files specified by a multivalued logical name

Within The One File
~~~~~~~~~~~~~~~~~~~
Language availability is specified through the use of [Language] directives. 
These must be numbered from 1 to the count of those supplied.  The highest
numbered language must have the complete set of messages for this is the
fallback when obtaining any message (this would normally be 'en').  The
[Language] may be specified as a comma-separated list of equivalent or similar
specifications, which during request processing will be matched against a
client specified list of accepted-languages one at a time in specified order. 
A wildcard may be specified which matches all fitting the template.  In this
manner a single language can be used also to match minor variants or language
specification synonyms.

  [Version]  9.0
  [Language]  1  es,es-ES
  [Language]  2  de,de-*
  [Language]  3  en

In the above (rather contrived) example a client request with

  Accept-Language: es-ES,de;q=0.6,en;q=0.3

would have language 1 selected, a client with

  Accept-Language: de-ch,es;q=0.6,en;q=0.3

language 2 selected, with

  Accept-Language: pt-br,de;q=0.6,en;q=0.3

also language 2 selected, with

  Accept-Language: pt

language 3 (the default) selected, etc.

Note that the messages for each language must use the *first* language
specification provided in the [Language] list.  In the example above all
messages for language 1 would be introduced using 'es', for language 2 with
'de' and for language 3 with 'en'.

Multiple Files - Multivalued Logical Name
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
With this approach a logical name containing multiple file names is defined
(more commonly described as a logical search list).  The final file specified
must contain the full message set.  Files specified prior to this, can contain
as many or as few of the full set as is desired.  A [Language] number does not
need to be specified as they are processed in the order the logical name
specifies them in.  Other language file directives are required.

The following is an example of a logical name providing the same three
languages in the examples above.

  $ DEFINE /SYSTEM WASD_CONFIG_MSG WASD_ROOT:[LOCAL]HTTPD$MSG_ES.CONF, -
                                   WASD_ROOT:[LOCAL]HTTPD$MSG_DE.CONF, -
                                   WASD_ROOT:[LOCAL]HTTPD$MSG.CONF

The file contents would be as follows (very contrived examples :-)

  # HTTPD$MSG_ES.CONF
  [Version]  9.0
  [Language]  0  es,es-ES
  [auth]
  es 01  Habla Espanol
  es 02  Habla Inglesi
  [dir]
  es 03  Habla Espanol
  es 04  Habla Inglesi

  # HTTPD$MSG_DE.CONF
  [Version]  9.0
  [Language]  0  de,de-*
  [auth]
  de 01  Sprechen Sie Deutsches
  de 02  Sprechen Sie Englisch
  [dir]
  de 03  Sprechen Sie Deutsches
  de 04  Sprechen Sie Englisch

  # HTTPD$MSG.CONF
  [Version]  9.0
  [Language]  0  en
  [auth]
  .
  .
  .(full set of messages)

The major ADVANTAGE of maintaining multiple files in this way is there is no
need to MERGE files when a new revision is required.  Just update the version
number and add any new required messages to the existing secondary file.
messages to the existing


ADDITIONAL [LANGUAGE] PARAMETERS
--------------------------------
Two optional parameters can follow the language number and language
specification synonym.  The first is a character set to be associated with the
message set.  This is specified using the keyword 'charset=' followed by the
character set specification.  For example

  [Language]  2  charset=ISO-8859-5

The second optional parameter is a comma-separated list of host domain
specifications that the message set can be associated with.  If there is no
"Accept-Language:" list associated with the request then this information will
be used to select the message set.  For example

  [Language]  2  charset=ISO-8859-5  hosts=cz,cz-*


CURRENT MESSAGE GROUPINGS
-------------------------

  [auth]        authentication/authorization
  [dir]         directory listing
  [general]     messages belonging to no particular module or category
  [htadmin]     authentication database administration
  [http]        http status messages
  [mapping]     mapping rules
  [proxy]       proxy module
  [put]         PUT/POST module
  [request]     request acceptance and initial parsing
  [script]      DCL/scripting module
  [ssi]         Server Side Includes
  [status]      error and other status reporting
  [upd]         update module


VERSION HISTORY
---------------
06-JUL-2016  MGD  MsgConfigLoadCallback() make [ismap] optional
10-APR-2016  MGD  [ismap] is now obsolete but retained to not break msg files
10-JUN-2010  MGD  bugfix; allow METACON_TOKEN_INCLUDE for [IncludeFile]
04-MAR-2005  MGD  allow WASD_CONFIG_MSG to be a logical search list to support
                  multiple, independent language files (also see METACON.C)
05-SEP-2003  MGD  optional 'charset=<string>' [language] parameter
15-AUG-2003  MGD  where CDATA constraints make using &#10; entity impossible
                  use a field name of hidden$lf and &#94; substituted for it
04-APR-2003  MGD  wildcard and comma-separated list of languages
                  can be specified (e.g. "[Language] es-ES,es,es-*")
12-OCT-2002  MGD  refine metacon reporting
31-MAR-2002  MGD  MsgForNoRequest() no longer required
22-JAN-2002  MGD  bugfix; MsgFor() Accept-Lang: comparison (jpp@esme.fr)
29-SEP-2001  MGD  instance support
15-SEP-2001  MGD  meta-config
04-AUG-2001  MGD  support module WATCHing
28-FEB-2001  MGD  OdsLoadTextFile(), OdsParseTextFile(), [IncludeFile]
18-JUN-2000  MGD  add MsgRevise()
08-APR-2000  MGD  HTTP status messages, v7.0
04-MAR-2000  MGD  use FaolToBuffer(), et.al.
02-JAN-2000  MGD  config file opened via ODS module
04-DEC-1999  MGD  additional messages for v6.1
05-MAY-1999  MGD  proxy messages, v6.0
06-JUL-1998  MGD  bugfix; MsgUnload(), VmFree() of 'HostListPtr'
14-FEB-1998  MGD  message file format changed from v4.4 to v5.0
25-OCT-1997  MGD  changes around MsgFor() when no request structure available
09-AUG-1997  MGD  initial development (HTTPd v4.4)
*/
/*****************************************************************************/

#ifdef WASD_VMS_V7
#undef _VMS__V6__SOURCE
#define _VMS__V6__SOURCE
#undef __VMS_VER
#define __VMS_VER 70000000
#undef __CRTL_VER
#define __CRTL_VER 70000000
#endif

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

/* VMS related header files */
#include <lnmdef.h>
#include <ssdef.h>
#include <stsdef.h>

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

#define WASD_MODULE "MSG"

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

char  MsgNotSet [] = "INTERNAL ERROR, please notify the administrator.";

char  ErrorMsgDupLangNumber [] = "Duplicate [language] number",
      ErrorMsgDupMsgNumber [] = "Duplicate message number",
      ErrorMsgLangDisabled [] = "[language] disabled",
      ErrorMsgLangName [] = "[language] name not specified",
      ErrorMsgLangNotFound [] = "[language] not found",
      ErrorMsgLangNotSpecified [] = "[language] not specified",
      ErrorMsgLangNumber [] = "[language] number out-of-range",
      ErrorMsgLangTooMany [] = "Too many [language]s",
      ErrorMsgMessageNumber [] = "Message number out-of-range",
      ErrorMsgNoGroup [] = "[group-name] has not been specified",
      ErrorMsgNoLangNumber [] = "No [language] number",
      ErrorMsgNoLangName [] = "No [language] name",
      ErrorMsgNoMessageNumber [] = "No message number",
      ErrorMsgObsoleteGroup [] = "Obsolete group",
      ErrorMsgTooFew [] = "Too few messages",
      ErrorMsgTooMany [] = "Too many messages",
      ErrorMsgUnknownGroup [] = "Unknown [group-name]",
      ErrorMsgVersion [] = "Message file [version] mismatch",
      ErrorMsgVersionNotChecked [] = "No [version] for checking";

MSG_META  MsgMeta;
MSG_META  *MsgMetaPtr;

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

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

extern BOOL  HttpdServerStartup;

extern int  ToLowerCase[],
            ToUpperCase[];

extern char  ErrorSanityCheck[],
             SoftwareID[];

extern CONFIG_STRUCT  Config;
extern META_CONFIG  *MetaGlobalMsgPtr;
extern WATCH_STRUCT  Watch;

/*****************************************************************************/
/*
*/ 
 
int MsgConfigLoad (META_CONFIG **MetaConPtrPtr)

{
   int  status;

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

   if (WATCH_MODULE(WATCH_MOD_MSG))
      WatchThis (WATCHALL, WATCH_MOD_MSG, "MsgConfigLoad()");

   status = MetaConLoad (MetaConPtrPtr,
                         v10orPrev10(CONFIG_MSG_FILE_NAME,-1),
                         &MsgConfigLoadCallback, false, false);
   if (*MetaConPtrPtr == MetaGlobalMsgPtr)
   {
      /* server startup */
      MetaConStartupReport (MetaGlobalMsgPtr, "MSG");
      if (VMSnok (status)) exit (status);
   }
   return (status);
}

/*****************************************************************************/
/*
Called by MetaConUnload() to free resources allocated during msg
configuration.
*/ 
 
MsgConfigUnload (META_CONFIG *mcptr)

{
   int  Count,
        LanguageCount;
   MSG_META  *mmptr;

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

   if (WATCH_MODULE(WATCH_MOD_MSG))
      WatchThis (WATCHALL, WATCH_MOD_MSG, "MsgConfigUnload()");

   mmptr = mcptr->MsgMetaPtr;

   for (LanguageCount = 1;
        LanguageCount <= mmptr->LanguageCount;
        LanguageCount++)
   {
      if (mmptr->Msgs[LanguageCount])
      {
         for (Count = 0; Count <= MSG_RANGE; Count++)
         {
            /* the '\b' backspace is a sentinal, will never occur in a message */
            if (mmptr->Msgs[LanguageCount]->TextPtr[Count] &&
                !SAME2(mmptr->Msgs[LanguageCount]->TextPtr[Count],'\b['))
            {
               /* remember that MsgCreate() returns allocation plus one! */
               VmFree (mmptr->Msgs[LanguageCount]->TextPtr[Count]-1, FI_LI);
            }
         }

         if (mmptr->Msgs[LanguageCount]->CharsetPtr)
            VmFree (mmptr->Msgs[LanguageCount]->CharsetPtr, FI_LI);

         if (mmptr->Msgs[LanguageCount]->HostListPtr)
            VmFree (mmptr->Msgs[LanguageCount]->HostListPtr, FI_LI);

         VmFree (mmptr->Msgs[LanguageCount], FI_LI);
      }
   }

   /* clean it out completely, just in case it's to be reused! */
   memset (mmptr, 0, sizeof(MSG_META));
}

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

BOOL MsgConfigLoadCallback (META_CONFIG *mcptr)

{
   static BOOL  IgnoreIsMap;
   static char  ProblemOverflow [] = "Storage overflow",
                ProblemUsage [] = "Cannot use during message configuration";

   int  status,
        ConfigFileIndex,
        Count,
        MessageNumber;
   char  *cptr, *sptr, *zptr;
   char  LanguageName [128],
         StringBuffer [512];
   MSG_META  *mmptr;
   METACON_LINE  *mclptr;

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

   if (WATCH_MODULE(WATCH_MOD_MSG))
   {
      WatchThis (WATCHALL, WATCH_MOD_MSG,
                 "MsgConfigLoadCallback() !&F !&X",
                 &MsgConfigLoadCallback, mcptr);
      if (WATCH_MODULE(WATCH_MOD__DETAIL))
      {
         mclptr = mcptr->ParsePtr;
         WatchDataFormatted ("!&X !UL !UL !UL !UL !&X !&Z !&Z\n",
            mclptr, mclptr->Size, mclptr->Token, mclptr->Number,
            mclptr->Length, mclptr->LineDataPtr, mclptr->TextPtr,
            mclptr->InlineTextPtr);
      }
   }

   if (mcptr->LnmIndex >= mcptr->LnmCount)
      ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);

   /* get a pointer to the current "line" */
   mclptr = mcptr->ParsePtr;

   /* if this is during server startup then set the global msg pointer */
   if (HttpdServerStartup)
      mmptr = mcptr->MsgMetaPtr = MsgMetaPtr = &MsgMeta;
   else
   /* if a report then conjure one from the singularity */
   if (!mcptr->MsgMetaPtr)
      mmptr = mcptr->MsgMetaPtr = VmGet (sizeof(MSG_META));
   else
      /* not the first time through */
      mmptr = mcptr->MsgMetaPtr;

   if (mclptr->Token == METACON_TOKEN_PRE)
   {
      /******************/
      /* pre-initialize */
      /******************/

      mmptr->CheckedLanguages = mmptr->VersionChecked = false;
      mmptr->LanguageCount = 0;
      mmptr->MessageBase = -1;
      mmptr->MessageCount = 0;

      return (true);
   }

   if (mclptr->Token == METACON_TOKEN_POST)
   {
      /****************/
      /* post-process */
      /****************/

      if (mcptr->LnmIndex+1 == mcptr->LnmCount)
      {
         if (mmptr->MessageCount < MSG_TOTAL)
         {
            if (HttpdServerStartup)
               ErrorExitVmsStatus (0, ErrorMsgTooFew, FI_LI);
            else
               MetaConReport (mcptr, METACON_REPORT_ERROR, ErrorMsgTooFew);
         }
         else
         if (mmptr->MessageCount > MSG_TOTAL)
         {
            /* technically, this shouldn't be possible! */
            if (HttpdServerStartup)
               ErrorExitVmsStatus (0, ErrorMsgTooMany, FI_LI);
            else
               MetaConReport (mcptr, METACON_REPORT_ERROR, ErrorMsgTooMany);
         }
         mmptr->LanguageDefault = mmptr->LanguageCount;
         mmptr->SearchListCount = mcptr->LnmCount;
      }

      strzcpy (mmptr->Msgs[mmptr->LanguageNumber]->FileName,
               mcptr->LoadReport.FileName,
               sizeof(mmptr->Msgs[mmptr->LanguageNumber]->FileName));

      return (true);
   }

   if (mclptr->Token == METACON_TOKEN_INCLUDE) return (true);

   if (mclptr->Token == METACON_TOKEN_VERSION) return (true);

   if (mclptr->Token != METACON_TOKEN_TEXT)
   {
      /* only interested in directive text */
      MetaConReport (mcptr, METACON_REPORT_ERROR, ProblemUsage);
      return (false);
   }

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

   /* buffer the text associated with the current "line" */
   zptr = (sptr = StringBuffer) + sizeof(StringBuffer);
   cptr = mclptr->TextPtr;
   while (*cptr && sptr < zptr) *sptr++ = *cptr++;
   if (sptr >= zptr)
   {
      MetaConReport (mcptr, METACON_REPORT_ERROR, ProblemOverflow);
      return (false);
   }
   if (sptr > StringBuffer)
   {
      sptr--;
      while (sptr > StringBuffer && ISLWS(*sptr)) sptr--;
      sptr++;
   }
   *sptr = '\0';

   cptr = StringBuffer;

   if (*cptr == '[')
   {
      if (strsame (cptr, "[version]", 9))
      {
         /*************/
         /* [version] */
         /*************/

         cptr += 9;
         while (*cptr && ISLWS(*cptr)) cptr++;
         if (!strsame (cptr, MSG_VERSION, -1))
         {
            if (HttpdServerStartup)
               ErrorExitVmsStatus (0, ErrorMsgVersion, FI_LI);
            else
            {
               MetaConReport (mcptr, METACON_REPORT_ERROR, ErrorMsgVersion);
               return (false);
            }
         }
         mmptr->VersionChecked = true;
         return (true);
      }

      if (strsame (cptr, "[language]", 10))
      {
         /**************/
         /* [language] */
         /**************/

         /* allow for empty [Language] generated by configuration revise */
         for (sptr = cptr+10; *sptr && !isalnum(*sptr); sptr++);
         if (!*sptr) return (true);

         /* step over the [language] and any trailing white-space */
         cptr += 10;
         while (*cptr && ISLWS(*cptr)) cptr++;

         /* allow a dangling "charset="/"hosts=" for on-line configuration  */
         if (strsame (cptr, "charset=", 8) || strsame (cptr, "hosts=", 6))
            return (true);

         if (mcptr->LnmCount > 1)
            mmptr->LanguageCount = mcptr->LnmIndex+1;
         else
            mmptr->LanguageCount++;

         if (WATCH_MODULE(WATCH_MOD_MSG))
            WatchThis (WATCHALL, WATCH_MOD_MSG,
                       "LANG COUNT !UL", mmptr->LanguageCount);

         /* check if the number of languages specified exceeds limits */
         if (mmptr->LanguageCount > MAX_LANGUAGES)
         {
            if (HttpdServerStartup)
               ErrorExitVmsStatus (0, ErrorMsgLangTooMany, FI_LI);
            else
            {
               MetaConReport (mcptr, METACON_REPORT_ERROR,
                              ErrorMsgLangTooMany);
               return (false);
            }
         }

         if (mcptr->LnmCount > 1)
         {
            /* with a language list it's from the index of the search list */
            mmptr->LanguageNumber = mcptr->LnmIndex+1;
         }
         else
         {
            if (!isdigit(*cptr))
            {
               /* no language number (order) has been specified */
               if (HttpdServerStartup)
                  ErrorExitVmsStatus (0, ErrorMsgNoLangNumber, FI_LI);
               else
               {
                  MetaConReport (mcptr, METACON_REPORT_ERROR,
                                 ErrorMsgNoLangNumber);
                  return (false);
               }
            }

            /* language number (order) */
            mmptr->LanguageNumber = atol(cptr);
         }

         /* check if the number of languages specified exceeds limits */
         if (mmptr->LanguageNumber < 0 ||
             mmptr->LanguageNumber > MAX_LANGUAGES)
         {
            if (HttpdServerStartup)
               ErrorExitVmsStatus (0, ErrorMsgLangNumber, FI_LI);
            else
            {
               MetaConReport (mcptr, METACON_REPORT_ERROR, ErrorMsgLangNumber);
               return (false);
            }
         }

         for (Count = 1; Count < mmptr->LanguageCount; Count++)
         {
            if (!mmptr->Msgs[Count]) continue;
            if (mmptr->Msgs[Count]->LanguageNumber == mmptr->LanguageNumber)
            {
               if (HttpdServerStartup)
                  ErrorExitVmsStatus (0, ErrorMsgDupLangNumber, FI_LI);
               else
               {
                  MetaConReport (mcptr, METACON_REPORT_ERROR,
                                 ErrorMsgDupLangNumber);
                  return (false);
               }
            }
         }

         /* allocate memory for this new language */
         mmptr->Msgs[mmptr->LanguageNumber] =
            (MSG_STRUCT*)VmGet(sizeof(MSG_STRUCT));

         /* set all the messages in the language to NULL */
         for (Count = 0; Count <= MSG_RANGE; Count++)
            mmptr->Msgs[mmptr->LanguageNumber]->TextPtr[Count] = NULL;

         /* set the language number */
         mmptr->Msgs[mmptr->LanguageNumber]->LanguageNumber =
            mmptr->LanguageNumber;

         /* get language name (start by skipping over the languge number) */
         while (*cptr && isdigit(*cptr)) cptr++;
         while (*cptr && ISLWS(*cptr)) cptr++;
         if (!*cptr)
         {
            if (HttpdServerStartup)
               ErrorExitVmsStatus (0, ErrorMsgNoLangName, FI_LI);
            else
            {
               MetaConReport (mcptr, METACON_REPORT_ERROR, ErrorMsgNoLangName);
               return (false);
            }
         }
         zptr = (sptr = mmptr->Msgs[mmptr->LanguageNumber]->LanguageList) +
                sizeof(mmptr->Msgs[mmptr->LanguageNumber]->LanguageList);
         while (*cptr && !ISLWS(*cptr) && sptr < zptr) *sptr++ = *cptr++;
         if (sptr > zptr) sptr--;
         *sptr = '\0';

         while (*cptr && ISLWS(*cptr)) cptr++;
         if (strsame (cptr, "charset=", 8))
         {
            /* character set for the messages */
            cptr += 8;
            sptr = cptr;
            while (*cptr && !ISLWS(*cptr)) cptr++;
            /* empty charsets are permitted to allow on-line revision */
            if (cptr - sptr)
            {
               zptr = mmptr->Msgs[mmptr->LanguageNumber]->CharsetPtr =
                  VmGet (cptr-sptr+1);
               memcpy (zptr, sptr, cptr-sptr);
               zptr[cptr-sptr] = '\0';
            }
         }

         while (*cptr && ISLWS(*cptr)) cptr++;
         if (*cptr)
         {
            /* trailing host list found */
            if (strsame (cptr, "hosts=", 6)) cptr += 6;
            sptr = cptr;
            while (*cptr) cptr++;
            /* trim trailing white-space */
            if (cptr > sptr) cptr--;
            while (cptr > sptr && ISLWS(*cptr)) cptr--;
            if (cptr > sptr) cptr++;
            zptr = mmptr->Msgs[mmptr->LanguageNumber]->HostListPtr =
               VmGet (cptr-sptr+1);
            memcpy (zptr, sptr, cptr-sptr);
            zptr[cptr-sptr] = '\0';
         }

         if (mmptr->Msgs[mmptr->LanguageNumber]->LanguageNumber == 0)
            MetaConReport (mcptr, METACON_REPORT_ERROR, ErrorMsgLangDisabled);

         /* get (any) primary language from (any) list */
         zptr = (sptr = mmptr->Msgs[mmptr->LanguageNumber]->LanguageName) +
                sizeof(mmptr->Msgs[mmptr->LanguageNumber]->LanguageName)-1;
         for (cptr = mmptr->Msgs[mmptr->LanguageNumber]->LanguageList;
              *cptr && *cptr != ',' && sptr < zptr;
              *sptr++ = *cptr++);
         *sptr = '\0';

         if (WATCH_MODULE(WATCH_MOD_MSG))
            WatchThis (WATCHALL, WATCH_MOD_MSG, "!UL !&Z !&Z !&Z !&Z",
                       mmptr->Msgs[mmptr->LanguageNumber]->LanguageNumber,
                       mmptr->Msgs[mmptr->LanguageNumber]->LanguageName,
                       mmptr->Msgs[mmptr->LanguageNumber]->LanguageList,
                       mmptr->Msgs[mmptr->LanguageNumber]->CharsetPtr,
                       mmptr->Msgs[mmptr->LanguageNumber]->HostListPtr);

         return (true);
      }

      /****************************/
      /* check language numbering */
      /****************************/

      if (!mmptr->CheckedLanguages)
      {
         mmptr->CheckedLanguages = true;
         for (Count = 1; Count <= mmptr->LanguageCount; Count++)
         {
            if (mmptr->Msgs[Count]->LanguageNumber < 0 ||
                mmptr->Msgs[Count]->LanguageNumber > mmptr->LanguageCount)
            {
               if (HttpdServerStartup)
                  ErrorExitVmsStatus (0, ErrorMsgLangNumber, FI_LI);
               else
               {
                  MetaConReport (mcptr, METACON_REPORT_ERROR,
                                 ErrorMsgLangNumber);
                  return (false);
               }
            }
         }
      }

      /****************/
      /* [group-name] */
      /****************/

      IgnoreIsMap = false;

      /* the '\b' backspace is a sentinal, will never occur in a message */
      if (strsame (cptr, "[auth]", 6))
      {
         mmptr->MessageBase = MSG_AUTH__BASE;
         mmptr->MessageMax = MSG_AUTH__MAX;
         mmptr->Msgs[1]->TextPtr[mmptr->MessageBase] = "\b[auth]";
      }
      else
      if (strsame (cptr, "[dir]", 6))
      {
         mmptr->MessageBase = MSG_DIR__BASE;
         mmptr->MessageMax = MSG_DIR__MAX;
         mmptr->Msgs[1]->TextPtr[mmptr->MessageBase] = "\b[dir]";
      }
      else
      if (strsame (cptr, "[general]", 9))
      {
         mmptr->MessageBase = MSG_GENERAL__BASE;
         mmptr->MessageMax = MSG_GENERAL__MAX;
         mmptr->Msgs[1]->TextPtr[mmptr->MessageBase] = "\b[general]";
      }
      else
      if (strsame (cptr, "[htadmin]", 9))
      {
         mmptr->MessageBase = MSG_HTADMIN__BASE;
         mmptr->MessageMax = MSG_HTADMIN__MAX;
         mmptr->Msgs[1]->TextPtr[mmptr->MessageBase] = "\b[htadmin]";
      }
      else
      if (strsame (cptr, "[http]", 6))
      {
         mmptr->MessageBase = MSG_HTTP__BASE;
         mmptr->MessageMax = MSG_HTTP__MAX;
         mmptr->Msgs[1]->TextPtr[mmptr->MessageBase] = "\b[http]";
      }
      else
      if (strsame (cptr, "[ismap]", 7))
      {
         IgnoreIsMap = true;
         MetaConReport (mcptr, METACON_REPORT_INFORM, ErrorMsgObsoleteGroup);
      }
      else
      if (strsame (cptr, "[mapping]", 9))
      {
         mmptr->MessageBase = MSG_MAPPING__BASE;
         mmptr->MessageMax = MSG_MAPPING__MAX;
         mmptr->Msgs[1]->TextPtr[mmptr->MessageBase] = "\b[mapping]";
      }
      else
      if (strsame (cptr, "[proxy]", 7))
      {
         mmptr->MessageBase = MSG_PROXY__BASE;
         mmptr->MessageMax = MSG_PROXY__MAX;
         mmptr->Msgs[1]->TextPtr[mmptr->MessageBase] = "\b[proxy]";
      }
      else
      if (strsame (cptr, "[put]", 5))
      {
         mmptr->MessageBase = MSG_PUT__BASE;
         mmptr->MessageMax = MSG_PUT__MAX;
         mmptr->Msgs[1]->TextPtr[mmptr->MessageBase] = "\b[put]";
      }
      else
      if (strsame (cptr, "[request]", 9))
      {
         mmptr->MessageBase = MSG_REQUEST__BASE;
         mmptr->MessageMax = MSG_REQUEST__MAX;
         mmptr->Msgs[1]->TextPtr[mmptr->MessageBase] = "\b[request]";
      }
      else
      if (strsame (cptr, "[script]", 8))
      {
         mmptr->MessageBase = MSG_SCRIPT__BASE;
         mmptr->MessageMax = MSG_SCRIPT__MAX;
         mmptr->Msgs[1]->TextPtr[mmptr->MessageBase] = "\b[script]";
      }
      else
      if (strsame (cptr, "[ssi]", 5))
      {
         mmptr->MessageBase = MSG_SSI__BASE;
         mmptr->MessageMax = MSG_SSI__MAX;
         mmptr->Msgs[1]->TextPtr[mmptr->MessageBase] = "\b[ssi]";
      }
      else
      if (strsame (cptr, "[status]", 8))
      {
         mmptr->MessageBase = MSG_STATUS__BASE;
         mmptr->MessageMax = MSG_STATUS__MAX;
         mmptr->Msgs[1]->TextPtr[mmptr->MessageBase] = "\b[status]";
      }
      else
      if (strsame (cptr, "[upd]", 5))
      {
         mmptr->MessageBase = MSG_UPD__BASE;
         mmptr->MessageMax = MSG_UPD__MAX;
         mmptr->Msgs[1]->TextPtr[mmptr->MessageBase] = "\b[upd]";
      }
      else
      {
         mmptr->MessageBase = -1;
         if (HttpdServerStartup)
            ErrorExitVmsStatus (0, ErrorMsgUnknownGroup, FI_LI);
         else
            MetaConReport (mcptr, METACON_REPORT_ERROR, ErrorMsgUnknownGroup);
      }

      return (true);
   }

   /***********/
   /* message */
   /***********/

   if (IgnoreIsMap) return (true);

   if (!mmptr->VersionChecked)
   {
      if (HttpdServerStartup)
         ErrorExitVmsStatus (0, ErrorMsgVersionNotChecked, FI_LI);
      else
      {
         MetaConReport (mcptr, METACON_REPORT_ERROR,
                        ErrorMsgVersionNotChecked);
         return (false);
      }
   }

   if (!mmptr->LanguageCount)
   {
      if (HttpdServerStartup)
         ErrorExitVmsStatus (0, ErrorMsgLangNotSpecified, FI_LI);
      else
      {
         MetaConReport (mcptr, METACON_REPORT_ERROR, ErrorMsgLangNotSpecified);
         return (false);
      }
   }

   if (mmptr->MessageBase < 0)
   {
      /* before any [goup-name] has been specified */
      MetaConReport (mcptr, METACON_REPORT_ERROR, ErrorMsgNoGroup);
      return (false);
   }

   if (!isalpha(*cptr))
   {
      /* language name is not being specified at start of line */
      MetaConReport (mcptr, METACON_REPORT_ERROR, ErrorMsgNoLangName);
      return (false);
   }

   zptr = (sptr = LanguageName) + sizeof(LanguageName)-1;
   while (*cptr && !ISLWS(*cptr) && sptr < zptr) *sptr++ = *cptr++;
   *sptr = '\0';
   /* find the index for the specified (perhaps leading) language */
   for (Count = 1; Count <= mmptr->LanguageCount; Count++)
   {
      if (WATCH_MODULE(WATCH_MOD_MSG))
         WatchThis (WATCHALL, WATCH_MOD_MSG, "!UL !&Z !&Z",
                    Count, LanguageName,
                    mmptr->Msgs[mmptr->LanguageCount]->LanguageName);
      if (strsame (LanguageName, mmptr->Msgs[Count]->LanguageName, -1))
      {
          mmptr->LanguageNumber = Count;
          break;
      }
   }
   if (Count > mmptr->LanguageCount)
   {
      MetaConReport (mcptr, METACON_REPORT_ERROR, ErrorMsgLangNotFound);
      return (false);
   }

   while (*cptr && !ISLWS(*cptr)) cptr++;
   while (*cptr && ISLWS(*cptr)) cptr++;

   if (!isdigit(*cptr))
   {
      /* no message number has been specified */
      MetaConReport (mcptr, METACON_REPORT_ERROR, ErrorMsgNoMessageNumber);
      return (false);
   }

   MessageNumber = atol(cptr);
   if (WATCH_MODULE(WATCH_MOD_MSG))
       WatchThis (WATCHALL, WATCH_MOD_MSG, "!UL", MessageNumber);

   /* zero disables a message or whole language */
   if (!MessageNumber || !mmptr->Msgs[mmptr->LanguageNumber]->LanguageNumber)
      return (true);

   if (MessageNumber < 0 || MessageNumber > mmptr->MessageMax)
   {
      MetaConReport (mcptr, METACON_REPORT_ERROR, ErrorMsgMessageNumber);
      return (false);
   }

   MessageNumber += mmptr->MessageBase;
   if (WATCH_MODULE(WATCH_MOD_MSG))
       WatchThis (WATCHALL, WATCH_MOD_MSG, "!UL", MessageNumber);

   while (*cptr && !ISLWS(*cptr)) cptr++;
   while (*cptr && ISLWS(*cptr)) cptr++;

   /* empty message (from configuration load) */
   if (*cptr == '\0') return (true);

   /* deliberately empty message */
   if (SAME2(cptr,'#\0')) cptr++;

   if (!mmptr->Msgs[mmptr->LanguageNumber]->TextPtr[MessageNumber])
   {
      mmptr->Msgs[mmptr->LanguageNumber]->TextPtr[MessageNumber] =
         MsgCreate (cptr);
      if (mmptr->LanguageNumber == mmptr->LanguageCount) mmptr->MessageCount++;
   }
   else
      MetaConReport (mcptr, METACON_REPORT_ERROR, ErrorMsgDupMsgNumber);

   return (true);
}

/*****************************************************************************/
/*
Allocate dynamic memory for the text of the message, copy it into it and
return a pointer to it.  The MapUrl.c module (for horrible, historical reasons)
requires a leading null character before the message.  Fudge this by creating
a one character longer string with that leading null and returning a pointer to
the first character.  The MapUrl.c module will the just use the returned
pointer minus one! (Neat huh?  Well it works anyway!)
*/

char* MsgCreate (char *Text)

{
   char  *MsgPtr;

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

   if (WATCH_MODULE(WATCH_MOD_MSG))
      WatchThis (WATCHALL, WATCH_MOD_MSG, "MsgCreate() !&Z", Text);

   MsgPtr = VmGet (strlen(Text)+2);
   *MsgPtr = '\0';
   strcpy (MsgPtr+1, Text);
   return (MsgPtr+1);
}

/*****************************************************************************/
/*
Return a pointer to a character string for the message number supplied in
'Message'.  If multiple languages are in use then use any supplied client list
of accepted languages ("Accept-Language:" request header field) to see if the
message can be supplied in a prefered language.  If none supplied or if none
match then check if the language has geographical information against it (a
list of host/domain specifications that can be used to determine if a specific
language would be more appropriate.  If none of the "hit" then return the
configuration-prefered language.
*/

char* MsgFor
(
REQUEST_STRUCT *rqptr,
int Message
)
{
   BOOL  LangMatch;
   int  Count,
        Language;
   char  *cptr, *lptr, *sptr, *tptr;

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

   if (WATCHMOD(rqptr, WATCH_MOD_MSG))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_MSG, "MsgFor() !UL", Message);

   if (!rqptr)
   {
      /* no request structure, use the default language */
      Language = MsgMeta.LanguageDefault;
   }
   else
   if (rqptr->MsgLanguage)
   {
      /* request message language has previously been set */
      Language = rqptr->MsgLanguage;
   }
   else
   if (MsgMeta.LanguageCount == 1)
   {
      /* only one language in use, use it! */
      Language = rqptr->MsgLanguage = MsgMeta.LanguageDefault;
   }
   else
   {
      if (rqptr->rqHeader.AcceptLangPtr)
      {
         /*******************************************/
         /* look for the client's prefered language */
         /*******************************************/

         lptr = rqptr->rqHeader.AcceptLangPtr;
         while (*lptr)
         {
            for (Count = 1; Count <= MsgMeta.LanguageCount; Count++)
            {
               sptr = MsgMeta.Msgs[Count]->LanguageList;
               /* can be in the comma-separated form "es-ES,es,es-*" */
               while (*sptr)
               {
                  if (WATCH_MODULE(WATCH_MOD_MSG))
                     WatchThis (WATCHALL, WATCH_MOD_MSG, "!UL !&Z !&Z",
                                Count, sptr, lptr);

                  /* compare to this language specified by the client */
                  tptr = sptr;
                  cptr = lptr;
                  while (*cptr && TOUP(*cptr) == TOUP(*sptr) &&
                         *cptr != ';' && *cptr != ',' && !ISLWS(*cptr))
                  {
                     cptr++;
                     sptr++;
                  }
                  LangMatch = true;
                  if (*sptr && *sptr != ',' && !ISLWS(*sptr) && *sptr != '*')
                     LangMatch = false;
                  else
                  if (*sptr != '*' &&
                      *cptr && *cptr != ';' && *cptr != ',' && !ISLWS(*cptr))
                     LangMatch = false;
                  if (!LangMatch)
                  {
                     while (*sptr && *sptr != ',') sptr++;
                     while (*sptr && (*sptr == ',' || ISLWS(*sptr))) sptr++;
                     continue;
                  }

                  /*********/
                  /* match */
                  /*********/

                  if (WATCHING (rqptr, WATCH_RESPONSE_HEADER))
                  {
                     while (*sptr && *sptr != ',') sptr++;
                     WatchThis (WATCHITM(rqptr), WATCH_RESPONSE_HEADER,
                        "LANG !UL:!AZ (\"!#AZ\" in \"!AZ\")",
                         MsgMeta.Msgs[Count]->LanguageNumber,
                         MsgMeta.Msgs[Count]->LanguageList,
                         sptr-tptr, tptr, rqptr->rqHeader.AcceptLangPtr);
                  }

                  Language = rqptr->MsgLanguage =
                     MsgMeta.Msgs[Count]->LanguageNumber;
                  break;
               }
               if (rqptr->MsgLanguage) break;
            }
            /* if we've found one */
            if (rqptr->MsgLanguage) break;

            /* step to the next language (if any) in the client list */
            while (*lptr && *lptr != ',' && !ISLWS(*lptr)) lptr++;
            while (*lptr && (*lptr == ',' || ISLWS(*lptr))) lptr++;
         }
      }

      if (!rqptr->MsgLanguage)
      {
         /************************/
         /* look for a host list */
         /************************/

         for (Count = 1; Count <= MsgMeta.LanguageCount; Count++)
         {
            if (WATCH_MODULE(WATCH_MOD_MSG))
               WatchThis (WATCHALL, WATCH_MOD_MSG, "!UL !&Z",
                          Count, MsgMeta.Msgs[Count]->LanguageList);

            if (MsgMeta.Msgs[Count]->HostListPtr)
            {
               if (MsgInHostList (rqptr, MsgMeta.Msgs[Count]->HostListPtr))
               {
                  if (WATCHING (rqptr, WATCH_RESPONSE_HEADER))
                     WatchThis (WATCHITM(rqptr), WATCH_RESPONSE_HEADER,
                                "LANG !UL:!AZ (!AZ)",
                                MsgMeta.Msgs[Count]->LanguageNumber,
                                MsgMeta.Msgs[Count]->LanguageList,
                                MsgMeta.Msgs[Count]->HostListPtr);

                  Language = rqptr->MsgLanguage =
                     MsgMeta.Msgs[Count]->LanguageNumber;
                  break;
               }
            }
         }
      }

      /* if none matching then fall back to the default language */
      if (!rqptr->MsgLanguage)
      {
         if (WATCHING (rqptr, WATCH_RESPONSE_HEADER))
             WatchThis (WATCHITM(rqptr), WATCH_RESPONSE_HEADER,
                        "LANG !UL:!AZ (default)",
                        MsgMeta.Msgs[MsgMeta.LanguageDefault]->LanguageNumber,
                        MsgMeta.Msgs[MsgMeta.LanguageDefault]->LanguageList);

         Language = rqptr->MsgLanguage = MsgMeta.LanguageDefault;
      }
   }

   if (Message <= 0 || Message > MSG_RANGE ||
       Language < 1 || Language > MsgMeta.LanguageCount)
      ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);

   /*******************/
   /* get the message */
   /*******************/

   /* if a message has been assigned then return it */
   if (cptr = MsgMeta.Msgs[Language]->TextPtr[Message])
   {
      if (rqptr && MsgMeta.Msgs[Language]->CharsetPtr)
         rqptr->rqResponse.MsgCharsetPtr = MsgMeta.Msgs[Language]->CharsetPtr;
      if (MsgMeta.LanguageCount == 1) return (cptr);
      if (WATCHING (rqptr, WATCH_RESPONSE_HEADER))
      {
         WatchThis (WATCHITM(rqptr), WATCH_RESPONSE_HEADER, "LANG !UL:!AZ",
                    Language, MsgMeta.Msgs[Language]->LanguageList);
         WatchDataFormatted ("!AZ\n", cptr);
      }
      return (cptr);
   }

   /*********************************/
   /* fallback to the base language */
   /*********************************/

   if (cptr = MsgMeta.Msgs[MsgMeta.LanguageCount]->TextPtr[Message])
   {
      if (rqptr && MsgMeta.Msgs[MsgMeta.LanguageCount]->CharsetPtr)
         rqptr->rqResponse.MsgCharsetPtr =
            MsgMeta.Msgs[MsgMeta.LanguageCount]->CharsetPtr;
      if (MsgMeta.LanguageCount == 1) return (cptr);
      if (WATCHING (rqptr, WATCH_RESPONSE_HEADER))
      {
         WatchThis (WATCHITM(rqptr), WATCH_RESPONSE_HEADER, "LANG !UL:!AZ",
                    MsgMeta.LanguageCount,
                    MsgMeta.Msgs[MsgMeta.LanguageCount]->LanguageList);
         WatchDataFormatted ("!AZ\n", cptr);
      }
      return (cptr);
   }

   /**************************************/
   /* no message was set for this event! */
   /**************************************/

   if (WATCH_MODULE(WATCH_MOD_MSG))
      WatchThis (WATCHALL, WATCH_MOD_MSG, "!UL !&Z", Language, MsgNotSet);
   return (MsgNotSet);
}

/*****************************************************************************/
/*
If the client's IP host name or address matches the wildcard string in
'HostList' then return true, else return false.
*/ 

BOOL MsgInHostList
(
REQUEST_STRUCT *rqptr,
char *HostList
)
{
   char  ch;
   char  *cptr, *hptr, *sptr;

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

   if (WATCHMOD(rqptr, WATCH_MOD_MSG))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_MSG,
                 "MsgInHostList() !&Z !&Z !&Z",
                 rqptr->ClientPtr->Lookup.HostName,
                 &rqptr->ClientPtr->IpAddressString,
                 HostList);

   hptr = HostList;
   while (*hptr)
   {
      while (*hptr && (*hptr == ',' || ISLWS(*hptr))) hptr++;
      sptr = hptr;
      while (*hptr && *hptr != ',' && !ISLWS(*hptr)) hptr++;
      ch = *hptr;
      *hptr = '\0';
      /* match against host address or name */
      if (isdigit(*sptr))
         cptr = &rqptr->ClientPtr->IpAddressString;
      else
         cptr = rqptr->ClientPtr->Lookup.HostName;
      if (WATCH_MODULE(WATCH_MOD_MSG))
          WatchThis (WATCHALL, WATCH_MOD_MSG, "!&Z !&Z", cptr, sptr);
      if (StringMatch (rqptr, cptr, sptr))
      {
         *hptr = ch;
         return (true);
      }
      *hptr = ch;
      if (*hptr) hptr++;
   }

   return (false);
}                             

/*****************************************************************************/
/*
A server administration report on the server's configuration. This function
just wraps the reporting function, loading a temporary database if necessary
for reporting from the configuration file.
*/ 

MsgConfigReport
(
REQUEST_STRUCT *rqptr,
BOOL UseServerDatabase
)
{
   int  status;
   META_CONFIG  *mcptr;

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

   if (WATCHMOD(rqptr, WATCH_MOD_MSG))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_MSG,
                 "MsgConfigReport() !&F !UL",
                 &MsgConfigReport, UseServerDatabase);

   if (UseServerDatabase)
      MsgConfigReportNow (rqptr, MetaGlobalMsgPtr);
   else
   {
      status = MsgConfigLoad (&mcptr);
      if (VMSnok (status))
      {
         /* severe error reported */
         rqptr->rqResponse.HttpStatus = 403;
         ErrorGeneral (rqptr, mcptr->LoadReport.TextPtr, FI_LI);
      }
      else
         MsgConfigReportNow (rqptr, mcptr);
      MetaConUnload (&mcptr, NULL);
   }

   AdminEnd (rqptr);
}

/*****************************************************************************/
/*
A server administration report on the message database.
*/ 

MsgConfigReportNow
(
REQUEST_STRUCT *rqptr,
META_CONFIG *mcptr
)
{
   static char  LanguageTable [] =
"<p><table class=\"ctgry\">\n\
<tr><th>Languages</th></tr>\n\
<tr><td>\n\
<table class=\"lftlft\">\n\
<tr>\
<th></th>\
<th class=\"sbttl\">Lang</th>\
<th class=\"sbttl\">Charset</th>\
<th class=\"sbttl\">Host List</th>\
!&@\
</tr>\n";

   static char  EndLanguageTable [] =
"</table>\n\
</td></tr>\n\
</table>\n";

   static char  OneLanguageFao [] =
"<tr>\
<td><b>!UL</b></td>\
<td>!AZ</td>\
<td>!AZ</td>\
<td>!AZ</td>\
!&@\
</tr>\n";

   static char  GroupFao [] =
"<p><table class=\"ctgry\">\n\
<tr><th style=\"font-size:110%\">!AZ</th></tr>\n\
<tr><td>\n\
<table class=\"lftlft\">\n";

   static char  MessageFao [] =
"<tr>!&@<th>!&;AZ</th><td>!&@</td></tr>\n";
   
   char  EndOfGroup [] =
"</table>\n\
</td></tr>\n\
</table>";

   char  EndOfPage [] =
"</table>\n\
</body>\n\
</html>\n";

   BOOL  MultipleLines;
   int  status,
        GroupCount,
        Count,
        Language,
        RowCount;
   unsigned long  FaoVector [32];
   unsigned long  *vecptr;
   char  *cptr;
   MSG_META  *mmptr;

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

   if (WATCHMOD(rqptr, WATCH_MOD_MSG))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_MSG, "MsgConfigReportNow()");

   /* get a pointer to the meta-config data */
   mmptr = mcptr->MsgMetaPtr;

   AdminPageTitle (rqptr, "Server Messages");
   AdminMetaConReport (rqptr, mcptr, MetaGlobalMsgPtr);
   AdminMetaConSource (rqptr, mcptr, MetaGlobalMsgPtr);

   /********************/
   /* language summary */
   /********************/

   vecptr = FaoVector;

   if (mmptr->SearchListCount > 1)
      *vecptr++ = "<th><u>File</u></th>";
   else
      *vecptr++ = "";

   status = FaolToNet (rqptr, LanguageTable, &FaoVector);
   if (VMSnok (status)) ErrorNoticed (rqptr, status, NULL, FI_LI);

   for (Count = 1; Count <= mmptr->LanguageCount; Count++)
   {
      vecptr = FaoVector;
      *vecptr++ = Count;
      *vecptr++ = mmptr->Msgs[Count]->LanguageList;
      if (mmptr->Msgs[Count]->CharsetPtr)
         *vecptr++ = mmptr->Msgs[Count]->CharsetPtr;
      else
         *vecptr++ = "<i>(default)</i>";
      if (mmptr->Msgs[Count]->HostListPtr)
         *vecptr++ = mmptr->Msgs[Count]->HostListPtr;
      else
         *vecptr++ = "<i>(none)</i>";

      if (mmptr->SearchListCount > 1)
      {
         *vecptr++ = "<td>!AZ</td>";
         *vecptr++ = mmptr->Msgs[Count]->FileName;
      }
      else
         *vecptr++ = "";

      status = FaolToNet (rqptr, OneLanguageFao, &FaoVector);
      if (VMSnok (status)) ErrorNoticed (rqptr, status, NULL, FI_LI);
   }

   status = FaolToNet (rqptr, EndLanguageTable, NULL);
   if (VMSnok (status)) ErrorNoticed (rqptr, status, NULL, FI_LI);

   /*****************************/
   /* loop through all messages */
   /*****************************/

   Count = GroupCount = 0;

   for (Count = 0; Count < MSG_RANGE; Count++)
   {
      if (mmptr->Msgs[1]->TextPtr[Count])
      {
         /* the '\b' backspace is a sentinal, will never occur in a message */
         if (SAME2(mmptr->Msgs[1]->TextPtr[Count],'\b['))
         {
            if (GroupCount++)
            {
               status = FaolToNet (rqptr, EndOfGroup, NULL);
               if (VMSnok (status))
                  ErrorNoticed (rqptr, status, NULL, FI_LI);
            }

            vecptr = FaoVector;
            *vecptr++ = mmptr->Msgs[1]->TextPtr[Count]+1;

            status = FaolToNet (rqptr, GroupFao, &FaoVector);
            if (VMSnok (status)) ErrorNoticed (rqptr, status, NULL, FI_LI);

            Count++;
            mmptr->MessageCount = 0;
         }
      }

      RowCount = 0;
      for (Language = 1; Language <= mmptr->LanguageCount; Language++)
         if (mmptr->Msgs[Language]->TextPtr[Count]) RowCount++;

      for (Language = 1; Language <= mmptr->LanguageCount; Language++)
      {
         if (!mmptr->Msgs[Language]->TextPtr[Count]) continue;

         /* '*cptr' will not be a pointer to a null character if linefeeds */
         for (cptr = mmptr->Msgs[Language]->TextPtr[Count];
              *cptr && *cptr != '\n';
              cptr++);
         if (*cptr)
            MultipleLines = true;
         else
            MultipleLines = false;

         vecptr = FaoVector;

         if (RowCount > 1)
         {
            *vecptr++ = "<th rowspan=\"!UL\">!2ZL</th>";
            *vecptr++ = RowCount;
            *vecptr++ = ++mmptr->MessageCount;
            RowCount = 0;
         }
         else
         if (RowCount == 1)
         {
            *vecptr++ = "<th>!2ZL</th>";
            *vecptr++ = ++mmptr->MessageCount;
         }
         else
            *vecptr++ = "";

         *vecptr++ = mmptr->Msgs[Language]->LanguageName;
         if (MultipleLines)
            *vecptr++ = "<pre>!&;AZ</pre>";
         else
            *vecptr++ = "<tt>!&;AZ</tt>";
         *vecptr++ = mmptr->Msgs[Language]->TextPtr[Count];

         status = FaolToNet (rqptr, MessageFao, &FaoVector);
         if (VMSnok (status)) ErrorNoticed (rqptr, status, NULL, FI_LI);
      }
   }

   /**************/
   /* end report */
   /**************/

   status = FaolToNet (rqptr, EndOfPage, NULL);
   if (VMSnok (status)) ErrorNoticed (rqptr, status, NULL, FI_LI);

   rqptr->rqResponse.PreExpired = PRE_EXPIRE_ADMIN;
   ResponseHeader200 (rqptr, "text/html", &rqptr->NetWriteBufferDsc);
}

/*****************************************************************************/
/*
A server administration menu for message configuration. This function just
wraps the revision function, loading a temporary database if necessary for
reporting from the message configuration file.
*/ 

MsgConfigRevise
(
REQUEST_STRUCT *rqptr,
BOOL UseServerDatabase
)
{
   int  status;
   META_CONFIG  *mcptr;

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

   if (WATCHMOD(rqptr, WATCH_MOD_MSG))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_MSG,
                 "MsgConfigRevise() !&F !UL",
                 &MsgConfigRevise, UseServerDatabase);

   if (UseServerDatabase)
      MsgConfigReviseNow (rqptr, MetaGlobalMsgPtr);
   else
   {
      status = MsgConfigLoad (&mcptr);
      if (VMSnok (status))
      {
         /* severe error reported */
         rqptr->rqResponse.HttpStatus = 403;
         ErrorGeneral (rqptr, mcptr->LoadReport.TextPtr, FI_LI);
      }
      else
         MsgConfigReviseNow (rqptr, mcptr);
      MetaConUnload (&mcptr, NULL);
   }

   AdminEnd (rqptr);
}

/*****************************************************************************/
/*
A server administration revision for the message database.
*/ 

MsgConfigReviseNow
(
REQUEST_STRUCT *rqptr,
META_CONFIG *mcptr
)
{
   static char  LanguageTable [] =
"<p><table class=\"ctgry\">\n\
<tr><th>Languages</th></tr>\n\
<tr><td>\n\
<table class=\"lftlft\">\n\
<tr><th class=\"sbttl\">Order</th>\
<th class=\"sbttl\">Language</th>\
<th class=\"sbttl\">Charset</th>\
<th class=\"sbttl\">Host List</th></tr>\n\
<input type=\"hidden\" name=\"hidden$lf\" value=\"&#94;[Version]  !AZ\">\n";

   static char  EndLanguageTable [] =
"<tr><td colspan=\"4\" \
style=\"width:20em;white-space:normal;color:#ff0000;\">\n\
<b>IMPORTANT:</b>&nbsp;\n\
<i>The primary language (that with <u>all</u> message texts complete \
- usually &quot;en&quot;) must be the <u>highest numbered</u> (order) \
language.\n\
Failure to ensure this will render the server unable to start!!</i>\n\
</td></tr>\n\
</table>\n</td></tr>\n</table>\n";

   static char  OneLanguageFao [] =
"<tr><td>\n\
<input type=\"hidden\" name=\"hidden$lf\" value=\"&#94;[Language]&#32;&#32;\">\n\
<input type=\"text\" size=\"3\" name=\"Order\" value=\"!&@\">\n\
</td><td>\n\
<input type=\"hidden\" name=\"hidden$lf\" value=\"&#32;&#32;\">\n\
<input type=\"text\" size=\"20\" name=\"Language\" value=\"!AZ\">\n\
</td><td>\n\
<input type=\"hidden\" name=\"hidden$lf\" value=\"&#32;&#32;charset&#61;\">\n\
<input type=\"text\" size=\"20\" name=\"Charset\" value=\"!AZ\">\n\
</td><td>\n\
<input type=\"hidden\" name=\"hidden$lf\" value=\"&#32;&#32;hosts&#61;\">\n\
<input type=\"text\" size=\"3\"5 name=\"HostList\" value=\"!AZ\">\n\
</td></tr>\n";

   static char  GroupFao [] =
"<p><table class=\"ctgry\">\n\
<input type=\"hidden\" name=\"hidden$lf\" value=\"&#94;&#94;!AZ&#94;\">\n\
<tr><th><font size=\"+1\">!AZ</font></th></tr>\n\
<tr><td>\n\
<table class=\"lftlft\">\n";

   static char  MessageFao [] =
"<tr><td>!2ZL<td>!&;AZ</td><td>!&@</td></tr>\n";

   char  EndOfGroup [] = "</table>\n</td></tr>\n</table>";

   int  status,
        GroupCount,
        Count,
        Language,
        LineCount;
   unsigned long  FaoVector [32];
   unsigned long  *vecptr;
   char  *cptr, *sptr, *zptr,
         *MsgGroupPtr,
         *MsgTextPtr;
   char  MultiLineBuffer [2048];
   MSG_META  *mmptr;

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

   if (WATCHMOD(rqptr, WATCH_MOD_MSG))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_MSG, "MsgConfigReviseNow()");

   /* get a pointer to the meta-config data */
   mmptr = mcptr->MsgMetaPtr;

   AdminPageTitle (rqptr, "Server Messages");
   AdminMetaConReport (rqptr, mcptr, MetaGlobalMsgPtr);
   AdminMetaConSource (rqptr, mcptr, MetaGlobalMsgPtr);

   AdminMetaConBeginUpdateForm (rqptr);

   /********************/
   /* language summary */
   /********************/

   vecptr = FaoVector;
   *vecptr++ = MSG_VERSION;

   status = FaolToNet (rqptr, LanguageTable, &FaoVector);
   if (VMSnok (status)) ErrorNoticed (rqptr, status, NULL, FI_LI);

   for (Count = 1; Count <= mmptr->LanguageCount; Count++)
   {
      vecptr = FaoVector;
      *vecptr++ = "!UL";
      *vecptr++ = Count;
      *vecptr++ = mmptr->Msgs[Count]->LanguageList;
      if (mmptr->Msgs[Count]->CharsetPtr)
         *vecptr++ = mmptr->Msgs[Count]->CharsetPtr;
      else
         *vecptr++ = "";
      if (mmptr->Msgs[Count]->HostListPtr)
         *vecptr++ = mmptr->Msgs[Count]->HostListPtr;
      else
         *vecptr++ = "";

      status = FaolToNet (rqptr, OneLanguageFao, &FaoVector);
      if (VMSnok (status)) ErrorNoticed (rqptr, status, NULL, FI_LI);
   }
   vecptr = FaoVector;
   *vecptr++ = "";
   *vecptr++ = "";
   *vecptr++ = "";
   *vecptr++ = "";
   status = FaolToNet (rqptr, OneLanguageFao, &FaoVector);
   if (VMSnok (status)) ErrorNoticed (rqptr, status, NULL, FI_LI);

   status = FaolToNet (rqptr, EndLanguageTable, NULL);
   if (VMSnok (status)) ErrorNoticed (rqptr, status, NULL, FI_LI);

   /*****************************/
   /* loop through all messages */
   /*****************************/

   Count = GroupCount = 0;

   for (Count = 0; Count < MSG_RANGE; Count++)
   {
      if (mmptr->Msgs[1]->TextPtr[Count])
      {
         /* the '\b' backspace is a sentinal, will never occur in a message */
         if (SAME2(mmptr->Msgs[1]->TextPtr[Count],'\b['))
         {
            if (GroupCount++)
            {
               status = FaolToNet (rqptr, EndOfGroup, NULL);
               if (VMSnok (status))
                  ErrorNoticed (rqptr, status, NULL, FI_LI);
            }

            vecptr = FaoVector;
            *vecptr++ = MsgGroupPtr = mmptr->Msgs[1]->TextPtr[Count]+1;
            *vecptr++ = mmptr->Msgs[1]->TextPtr[Count]+1;

            status = FaolToNet (rqptr, GroupFao, &FaoVector);
            if (VMSnok (status)) ErrorNoticed (rqptr, status, NULL, FI_LI);

            Count++;
            mmptr->MessageCount = 0;
         }
      }

      mmptr->MessageCount++;
      for (Language = 1; Language <= mmptr->LanguageCount; Language++)
      {
         if (!mmptr->Msgs[Language]->TextPtr[Count])
         {
            MsgTextPtr = "";
            /* fall back to the size of the message in the base language */
            cptr = mmptr->Msgs[mmptr->LanguageCount]->TextPtr[Count];
         }
         else
            MsgTextPtr = cptr = mmptr->Msgs[Language]->TextPtr[Count];

         /* count number of lines in message */
         LineCount = 1;
         for ( /*above*/ ; *cptr; cptr++) if (*cptr == '\n') LineCount++;

         vecptr = FaoVector;

         *vecptr++ = mmptr->MessageCount;
         *vecptr++ = mmptr->Msgs[Language]->LanguageName;

         if (LineCount > 1)
         {
            /* add a '\' (line continuation) character at each end-of-line */
            cptr = MsgTextPtr;
            zptr = (sptr = MultiLineBuffer) + sizeof(MultiLineBuffer);
            while (*cptr && sptr < zptr)
            {
               if (*cptr == '\n' && sptr < zptr) *sptr++ = '\\';
               if (sptr < zptr) *sptr++ = *cptr++;
            }
            if (sptr >= zptr)
            {
               ErrorGeneralOverflow (rqptr, FI_LI);
               break;
            }
            if (sptr > MultiLineBuffer) sptr--;
            while (sptr > MultiLineBuffer &&
                   (ISLWS(*sptr) || *sptr == '\n' || *sptr == '\\')) sptr--;
            if (sptr > MultiLineBuffer) sptr++;
            *sptr = '\0';

            *vecptr++ =
"<input type=\"hidden\" name=\"hidden$lf\" value=\"&#94;!AZ !2ZL  \">\n\
<textarea name=\"!AZ!AZ!2ZL\" rows=\"!UL\" cols=\"60\">!&;AZ</textarea>";
            *vecptr++ = mmptr->Msgs[Language]->LanguageList;
            *vecptr++ = mmptr->MessageCount;
            *vecptr++ = MsgGroupPtr;
            *vecptr++ = mmptr->Msgs[Language]->LanguageList;
            *vecptr++ = mmptr->MessageCount;
            *vecptr++ = LineCount;
            *vecptr++ = MultiLineBuffer;
         }
         else
         {
            *vecptr++ =
"<input type=\"hidden\" name=\"hidden$lf\" value=\"&#94;!AZ !2ZL  \">\n\
<input name=\"!AZ!AZ!2ZL\" type=\"text\" size=\"60\" value=\"!&;AZ\">";
            *vecptr++ = mmptr->Msgs[Language]->LanguageList;
            *vecptr++ = mmptr->MessageCount;
            *vecptr++ = MsgGroupPtr;
            *vecptr++ = mmptr->Msgs[Language]->LanguageList;
            *vecptr++ = mmptr->MessageCount;
            *vecptr++ = MsgTextPtr;
         }

         status = FaolToNet (rqptr, MessageFao, &FaoVector);
         if (VMSnok (status)) ErrorNoticed (rqptr, status, NULL, FI_LI);
      }
   }

   /**************/
   /* end revise */
   /**************/

   status = FaolToNet (rqptr, "</table>\n</td></tr>\n</table>\n", NULL);
   if (VMSnok (status)) ErrorNoticed (rqptr, status, NULL, FI_LI);

   AdminMetaConEndUpdateForm (rqptr);

   status = FaolToNet (rqptr, "</body>\n</html>\n", NULL);
   if (VMSnok (status)) ErrorNoticed (rqptr, status, NULL, FI_LI);

   rqptr->rqResponse.PreExpired = PRE_EXPIRE_ADMIN;
   ResponseHeader200 (rqptr, "text/html", &rqptr->NetWriteBufferDsc);
}

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

