/*

 BabbleMud - A MUD server

 Author:  Nick Gammon 
          http://www.gammon.com.au/ 

(C) Copyright Nick Gammon 2004. Permission to copy, use, modify, sell and
distribute this software is granted provided this copyright notice appears
in all copies. This software is provided "as is" without express or implied
warranty, and with no claim as to its suitability for any purpose.
 
*/

// debug incoming headers?

#define DEBUG_HTTP false

// immediately write to control file?

#define COMMIT_CHANGES true

#include "commands.h"
#include "webclient.h"
#include "save.h"

// Actions can be:

static const string ADD_ACTION        = "add";        //  add record (which is now on form)
static const string SEARCH_ACTION     = "search";     //  search for a record by match of some sort
static const string LIST_ACTION       = "list";       //  list existing records 
static const string NEW_ACTION        = "new";        //  start a new, blank record
static const string DUPLICATE_ACTION  = "duplicate";  //  duplicate the current record
static const string DELETE_ACTION     = "delete";     //  delete the current record
static const string AMEND_ACTION      = "change";     //  change the current record
static const string EDIT_ACTION       = "edit";       // change to editing new thing (ie. change tables)

// (blank)     - just show action buttons

static const int MAXRECORDS = 200;    // maximum to display for long lists, searches
static const int BOXCOLS = 78;        // columns for input boxes

                                                
// reserved names for using on the form
//  by using underscores they shouldn't clash with actual data field names

static const string NAMEFIELD     = "_name";      // name of table (eg. spells)
static const string KEYFIELD      = "_key";       // which key they are changing (eg. heal)
static const string ACTIONFIELD   = "_action";    // what action (eg. add)
static const string NEWNAME       = "_newname";   // the name of the new table
static const string HASHFIELD     = "_hash";      // the hash of the original record 
static const string SEARCHFIELD   = "_searchfield";  // what to search on  (eg. "hp")
static const string SEARCHVALUE   = "_searchvalue";  // what to search for  (eg. 10)
static const string CONFIRMDELETE = "_confirmdelete";  // true to confirm deleting

// gets a reference to the correct list (eg. spells, rooms etc.)
// this is for main dtds, sub-dtds are selected later on
ObjectList * getObjectList (WebClient & w, const string & name)
  {  
  if (name == "rooms")    
    return & rooms ();    
  else if (name == "objects")    
    return & objects ();    
  else if (name == "mobs")    
    return & mobs ();    
  else if (name == "resets")    
    return & resets ();    
  else if (name == "spells")
    return & control ().items_ ["spells"];
  else if (name == "languages")
    return & control ().items_ ["languages"];
  else if (name == "classes")
    return & control ().items_ ["classes"];
  else if (name == "web_pages")
    return & control ().items_ ["web_pages"];
  else if (name == "web_users")
    return & control ().items_ ["web_users"];
  else if (name == "dtd")
    return & control ().items_ ["dtd"];   // the DTD itself
  else
    throw runtime_error (name + " not implemented");
  }

class EditorData
  {
  private:
    ObjectList * itemList_;    // the list the item is found in (eg. the spell list)
  public:
    string name_;      // what to edit (eg. spell)
    string action_;    // add, change, delete etc.
    string keyname_;   // the name of the key (eg. "name")
    string key_;       // what the key is (eg. "heal")
    Object dtd_;       // dtd for this table
    thing item_;       // a copy of the thing being edited (eg. the spell)
    set<string> eraseItems_;  // which ones to erase (ie. gone from used to empty)
    list<pair<string, string> > languages_;    // which languages for multi-lingual
    StringMap errors_;  // error messages, per field  (eg. "field required", "not numeric" etc.)
    Regexp regexp_;     // regexp for search
    string searchField_;  // field name to look in for search
    string searchValue_;  // what to look for
    bool addingAction_;   // true if this is adding a new record
    string baseType_;     // top-level key type (eg. rooms, spells)
  
  // ctor
  EditorData () : itemList_ (NULL) {}
  
  // for setting the itemlist
  void setItemList (ObjectList * itemList)  
    { 
    itemList_ = itemList; 
    }

  ObjectList * getItemList () const { return itemList_; }

  // finds key_ (with key name keyname_) in the relevant list/map
  thing * findRecord ()
    {
    ObjectIter i;
    if (name_ == "rooms")
      i = FindObject (*itemList_, keyname_, convertString<int> (key_, "Bad vnum"));
    else
      i = FindObject (*itemList_, keyname_, key_);
    if (i == itemList_->end ())
      return NULL;
    return &(*i);   // return pointer to item ;)
    }
  
  // insert a new record into the appropriate list/map
  void insert ()
    {
    Object obj;
    CopyThing (item_, obj);
    itemList_->push_back (obj);
    if (baseType_ == "rooms")
      RebuildWorldMap ();
#if COMMIT_CHANGES
    write ();
#endif   
    } // end of insert
  
  // erase the keyed record from the appropriate list/map
  void erase ()
    {
    ObjectIter i = FindObject (*itemList_, keyname_, key_);
    if (i != itemList_->end ())
      itemList_->erase (i);
    if (baseType_ == "rooms")
      RebuildWorldMap ();
#if COMMIT_CHANGES
    write ();
#endif   
    
    } // end of erase
  
    
  // true if form has errors on it
  const bool haveError () const { return !errors_.empty (); }

  // return true if a particular field had an error on it
  const bool fieldHasError (const string & fullName)
    {
    StringMap::const_iterator error_iter = errors_.find (fullName);
    return error_iter != errors_.end ();
    }
  
  void write ()
    {
    string filename;
    string doctype;
    if (baseType_ == "rooms")
      {
      filename = ROOMS_DIR + "rooms.xml";
      doctype = "rooms";
      }
    else if (baseType_ == "resets")
      {
      filename = RESETS_DIR + "resets.xml";
      doctype = "resets";
      }
    else if (baseType_ == "objects")
      {
      filename = OBJECTS_DIR + "objects.xml";
      doctype = "objects";
      }
    else if (baseType_ == "mobs")
      {
      filename = MOBS_DIR + "mobs.xml";
      doctype = "mobs";
      }
    else
      {
      filename = CONTROL_FILE;
      doctype = "control";
      WriteTopThing (CONTROL_FILE, "control", control ());   
      return;   // we have done the write here
      }
    
    WriteObjectList (*itemList_, filename, doctype);
    } // end of write
  
  };    // end of class EditorData


/*

Access control:

First test is that the user needs the "editor" flag set:

  <flag editor="1" />

Examples:

  All access to everything:       full_access_tables="all"
  All access to certain tables:   full_access_tables="spells, rooms"

"All access" means view, add, change, delete.

  View any table:                 view_tables="all"
  View cerain tables:             view_tables="spells, rooms, languages"

Note: viewing is a prerequisite to other actions, like changing.

  Add to any table:               add_tables="all"
  Add to cerain tables:           add_tables="spells, rooms, languages"

  Change any table:               change_tables="all"
  Add to cerain tables:           change_tables="spells, rooms, languages"

  Delete from any table:          delete_tables="all"
  Delete from cerain tables:      delete_tables="spells, rooms, languages"

Exclusions
----------

Exclusions are tested first, so you could have someone who can modify everything
*except* the dtd table:

  full_access_tables="all"      <--- they can do anything
  no_viewtables="dtd"           <--- except view (and thus modify) the dtd table
  
*/

bool CanAccessTable (WebClient & w, const string & action, const string & name)
  {
  set<string, ciLess> fullAccessTables;   // tables they have full access to
  set<string, ciLess> goodTables;         // tables they can view/add/change/delete
  set<string, ciLess> badTables;          // tables the cannot view/add/change/delete

  stringToContainer (w.user_.gets ("full_access_tables"), fullAccessTables);
  stringToContainer (w.user_.gets (action + "_tables"), goodTables);  // eg. add_tables
  stringToContainer (w.user_.gets ("no" + action + "_tables"), badTables);  // eg. noadd_tables
  
  // if in prohibited table list, they cannot
  if (badTables.find ("all") != badTables.end ())
    return false; 
  
  // this table excluded?
  if (badTables.find (name) != badTables.end ())
    return false; 
  
  // full access to all tables?
  if (fullAccessTables.find ("all") != fullAccessTables.end ())
    return true; 
  
  // full access to *this* table?
  if (fullAccessTables.find (name) != fullAccessTables.end ())
    return true; 
  
  // requested access (view/add/change/delete) to all tables?
  if (goodTables.find ("all") != goodTables.end ())
    return true; 
  
  // requested access (view/add/change/delete) to *this* tables?
  if (goodTables.find (name) != goodTables.end ())
    return true; 
  
  return false;
  }

// put a button on the form
void Button (WebClient & w, const string & name, const string & label)
  {
  w << "<input Type=\"submit\" "
    << "Name=\"_" << name << "\" "
    << "Value=\"" << label << "\">\n";
  } // end of Button

// hyperlink, name is the key (eg. spell=fireblast) description is what they see
void Hyperlink (WebClient & w, const EditorData & data,
                const string & value, const string & description)
  {
  w << "<a href=\"/" << w.URL_ << "?" << NAMEFIELD << "=" << data.name_ 
    << "&" << KEYFIELD << "=" << value << "\">"
    << FixHTML (description) << "</a>\n";
  } // end of Hyperlink

// put hidden text on the form
void Hidden (WebClient & w, const string & name, const string & contents)
  {
  w << "<input Type=\"hidden\" "
    << "Name=\"" << name << "\" "
    << "Value=\"" << contents << "\">\n";
   } // end of Hidden

void InputField (WebClient & w, const string & name, const string & contents,
                 const int size, const int maxlength, const bool right, const bool password)
 {
 string align;
 if (right)
   align = " align=\"right\" style=\"text-align:right;\" ";
 
 string type = "text";
 if (password)
   type="password";
 
  w << "<input Type=\"" << type << "\" "
    << "Name=\"" << name << "\" "
    << "Value=\"" << FixHTML (contents) << "\" " 
    << "size=\"" << size << "\" "
    << "maxlength=\"" << maxlength << "\" "
    << align
    << ">\n"; 
 }  // end of InputField

void InputBox (WebClient & w, const string & name, const string & contents,
                 const int lines)
 {
  w << "<textarea "
    << "Name=\"" << name << "\" "
    << "rows=\"" << lines << "\" "
    << "cols=\"" << BOXCOLS << "\" >\n"; 

  w << FixHTML (contents);
  w << "</textarea>\n";
 }  // end of InputBox

void ComboBox (WebClient & w, const string & name, const string & contents,
                 const string matchlist)
 {
    w << "<select Name=\"" << name << "\" size=\"1\">\n";
  
  set<string> matches;
  stringToContainer (matchlist, matches);
 
  for (set<string>::const_iterator i = matches.begin ();
       i != matches.end ();
       ++i)
    {
    // indicate one currently selected
    string selected;
    if (*i == contents)
      selected = "selected";
    w << "<option Value=\"" << *i << "\" " << selected << ">"
      << FixHTML (*i) << "\n";
    }
  w << "</select>\n";
  
 }  // end of ComboBox

void CheckBox (WebClient & w, const string & name, const bool contents)
 {
  w << "<input Type=\"checkbox\" "
    << "Name=\"" << name << "\" "
    << "Value=\"1\" ";
  if (contents)
    w << "checked ";
  w << ">\n"; 
 }  // end of CheckBox

void ShowButtons (WebClient & w, EditorData & data)
  {  
  // can only show main buttons if they have selected something to edit
  if (!data.name_.empty ())
    {
    
    pair<string, string> s = GetWord (data.searchField_, ":");
    string searchField = s.first;
    
    // generate search list - show all possible fields in the DTD
    w << "<p><p>Search: &nbsp;\n";
    w << "<select Name=\"" << SEARCHFIELD << "\" size=\"1\">\n";
    // list all dtds
    for (ObjectList::const_iterator dtditer = data.dtd_.contents_.begin ();
         dtditer !=  data.dtd_.contents_.end ();
         ++dtditer)
      {
      string name = dtditer->gets ("name");
        
      // mucking around for multiple languages
      string language;
      string prefix;
      bool multiLanguage = dtditer->getb ("multi_language");
      list<pair<string, string> >::const_iterator lang_iter = data.languages_.begin ();
      string fullName = name;   // name with language prefix  
      
     // repeat for each supported language - I do it this way because I want to
      // go through the loop at least once
      do
        {
        // get the current language
        if (multiLanguage)
          {
          if (lang_iter != data.languages_.end ())
            {
            language = lang_iter->first;
            prefix = lang_iter->second;     
            fullName = prefix + name;   // prefix name so success_room becomes fr_success_room
            } // end of another language to go
          }   // end of multi-languages on *this* field
      
          // indicate one we are currently searching for
          string selected;
          if (fullName == searchField)
            selected = "selected";
          w << "<option Value=\"" << fullName << ":"    // name:type
            << dtditer->gets ("type") << "\" " << selected << ">"
            << FixHTML (fullName) << "\n";
          
          // onto next language
         if (lang_iter != data.languages_.end ())
            ++lang_iter;
        } while (multiLanguage && lang_iter != data.languages_.end ());
        
      }   // end of each dtd item
    w << "</select>\n";
    
    w << "&nbsp; for match on: &nbsp;";
    InputField (w, SEARCHVALUE, data.searchValue_, 30, 255);
    Button (w, SEARCH_ACTION, "Search");
  
        // start new line for rest of them
    w << "<p>\n";

    // List All button
    Button (w, LIST_ACTION, "List All");
      
    bool canAdd     = CanAccessTable (w, "add", data.baseType_);
    bool canChange  = CanAccessTable (w, "change", data.baseType_);
    bool canDelete  = CanAccessTable (w, "delete", data.baseType_);

    // New record button (unless already doing a new record)
    if (data.action_ != NEW_ACTION && canAdd)
        Button (w, NEW_ACTION, "New");
    
    // we can only add if we started with a blank record, or a faulty add
  
    if (data.addingAction_ && canAdd)
        Button (w, ADD_ACTION, "Add");
      
    // we can only change or delete if we have a key to do it to
      
    if (!data.key_.empty () && 
        data.action_ != DUPLICATE_ACTION && 
        data.action_ != DELETE_ACTION)
      {
      if (canChange)
        Button (w, AMEND_ACTION, "Change");
      if (canDelete)
        Button (w, DELETE_ACTION, "Delete");
      if (canAdd)
        Button (w, DUPLICATE_ACTION, "Duplicate");
      // safety checkbox
      if (canDelete)
        w << "<p>Check To Confirm Delete: <input type=\"checkbox\" name=\""
          << CONFIRMDELETE << "\" Value=\"1\"> (if using 'Delete' button)</p>\n";
        
      // put primary key into form as a hidden field for updating or deleting purposes
      Hidden (w, KEYFIELD, data.key_);
      }  // end of having primary key
    
    w << "</p>\n";
    
    // what we are editing -- see list now
    Hidden (w, NAMEFIELD, data.name_);
    } // end of having a table to edit
  
  // pull them out into a set to get them into sequence
  set<string> tablenames;
    // list all dtds
  for (ObjectList::const_iterator dtditer = control ().items_ ["dtd"].begin ();
       dtditer != control ().items_ ["dtd"].end ();
       ++dtditer)
    {
    // exclude sub-dtds
    if (!dtditer->getb ("sub_dtd") &&
        CanAccessTable (w, "view", dtditer->gets ("name")))
      tablenames.insert (dtditer->gets ("name"));
    }
  
  // show list of things to edit
  w << "<hr />\n";
  w << "Edit: <select Name=\"" << NEWNAME << "\" size=\"1\">\n";
  
  // "choose" if nothing selected
  w << "<option Value=\"\">(Choose)\n";
  
  // now get them in alphabetical order
  for (set<string>::const_iterator tblIter = tablenames.begin ();
       tblIter != tablenames.end ();
       ++tblIter)
    {

   // indicate one we are currently editing
    string selected;
    if (*tblIter == data.name_)
      selected = "selected";
    w << "<option Value=\"" << *tblIter << "\" " << selected << ">"
      << FixHTML (*tblIter) << "\n";
    
    }
 
  w << "</select>\n";
  
  Button (w, EDIT_ACTION, "Edit new thing");
  
  w << "<p><a href=\"editor.htm\">Online editor help</a></p>\n";
  
  } // end of ShowButtons


void FindRecord (WebClient & w, EditorData & data)
  {
  
  // find base record 
  thing * t = data.findRecord ();
  if (t == NULL)
    throw runtime_error ("There is no '" + data.key_ + "' in '" + data.name_ + "'");
 
  // get copy of record
  data.item_ = *t;
    
  } // end of FindRecord

void ValidateString (WebClient & w, EditorData & data, 
                  const Object & dtditem, const string & fullName)
  {
  // what they entered
  string enteredData = trim (w.postData_ [fullName]);

  // most checks are bypassed for empty fields
  // if you want them applied check "required" first
  
  if (enteredData.empty ())
    {
    data.item_.erases (fullName);  // erase altogether
    data.eraseItems_.insert (fullName);  // add to list to erase
    return; // all done!
    }

  // not empty
  
  if (dtditem.hasi ("min_length") && 
      enteredData.size () < dtditem.geti ("min_length"))
     {
     data.errors_ [fullName] = 
        MAKE_STRING ("Value too short, minimum is " << dtditem.geti ("min_length") << " characters.");
     return;
     }
    
  if (dtditem.hasi ("max_length") && 
      enteredData.size () > dtditem.geti ("max_length"))
     {
     data.errors_ [fullName] = 
        MAKE_STRING ("Value too long, maximum is " << dtditem.geti ("max_length") << " characters.");
     return;
     }
       
  Regexp re;
  string s;

  // check it matches the regular expression
  s = dtditem.gets ("match_regexp");
  if (!s.empty ())
    {
    re.Compile (s);
    if (!re.Execute (enteredData))
       {
       data.errors_ [fullName] = 
          MAKE_STRING ("Value does not match required 'match' regular expression.");
       return;
       }
    }   // end of testing match_regexp

  // check it does not match the regular expression
  s = dtditem.gets ("nomatch_regexp");
  if (!s.empty ())
    {
    re.Compile (s);
    if (re.Execute (enteredData))
       {
       data.errors_ [fullName] = 
          MAKE_STRING ("Value matches required 'no match' regular expression.");
       return;
       }
    }   // end of testing nomatch_regexp
  
  // check it matches the set of values
  // note - it accepts an empty string, if this will not do make the field "required"
  s = dtditem.gets ("match_set");
  if (!s.empty ())
    {
    set<string> matches;
    stringToContainer (s, matches);
    if (matches.find (enteredData) == matches.end ())
      {
      ostringstream os;
      const string delimiter = ", ";
      copy (ISEQ (matches), ostream_iterator<string> (os, delimiter.c_str ()));
      string wanted = os.str ();
      wanted = wanted.substr (0, wanted.size () - delimiter.size ()); // drop trailing ", "
      data.errors_ [fullName] = 
         MAKE_STRING ("Value is not in required list (" + wanted + ")");
      return;
      }
    }   // end of testing match_set
  
  // if "regexp" flag is set the field is itself a regexp - make sure it compiles
  if (dtditem.getb ("regexp"))
    {
    try
      {
      re.Compile (enteredData);
      } // end of try
    catch (exception & e)
      {
       data.errors_ [fullName] = 
         MAKE_STRING ("Bad regular expression: " << e.what ());
      return;
      } // end of catch
    } // end of field being a regexp
  
  // if "handler" flag is set the field must be locatable in the code handler table
  if (dtditem.getb ("handler"))
    {
    CommandHandlerMap::const_iterator handlerIter = getCommandHandlerMap ().find (enteredData);
      
    if (handlerIter == getCommandHandlerMap ().end ())
      {
       data.errors_ [fullName] = 
         MAKE_STRING ("That code handler is not defined in the server");
      return;
      } // end of no handler    
    
    } // end of field requiring a handler
  
 // if "webhandler" flag is set the field must be locatable in the code handler table
  if (dtditem.getb ("webhandler"))
    {
    WebHandlerMap::const_iterator handlerIter = getWebHandlerMap ().find (enteredData);
      
    if (handlerIter == getWebHandlerMap ().end ())
      {
       data.errors_ [fullName] = 
         MAKE_STRING ("That web handler is not defined in the server");
      return;
      } // end of no handler    
    
    } // end of field requiring a web handler
  
  // copy into object, so it is displayed when we show the form
  data.item_.sets (fullName, enteredData);
   
  }   // end of ValidateString

void ValidateInt (WebClient & w, EditorData & data, 
                  const Object & dtditem, const string & fullName)
  {
  // what they entered
  string enteredData = w.postData_ [fullName];
  int value = 0;
      
  // most checks are bypassed for empty fields
  // if you want them applied check "required" first
  
  if (enteredData.empty ())
    {
    data.item_.erasei (fullName);  // erase altogether
    data.eraseItems_.insert (fullName);  // add to list to erase
    return; // all done!
    }

// not empty

  istringstream is (enteredData);

  is >> ws >> value >> ws;
  if (is.fail () || !is.eof ())
     {
     data.errors_ [fullName] = "Entry not numeric";
     return;
     }
  
  if (dtditem.hasi ("min_value") && 
      value < dtditem.geti ("min_value"))
     {
     data.errors_ [fullName] = MAKE_STRING ("Value too low, minimum is " << dtditem.geti ("min_value"));
     return;
     }
    
  if (dtditem.hasi ("max_value") && 
      value > dtditem.geti ("max_value"))
     {
     data.errors_ [fullName] = MAKE_STRING ("Value too high, maximum is " << dtditem.geti ("max_value"));
     return;
     }
  
   // check room exists
  if (dtditem.getb ("room"))
    {
    WorldMapIterator i = world ().find (value);
    if (i == world ().end ())
      {
      data.errors_ [fullName] = "This room does not exist.";
      return;
      }
    }   // end of room check
  
  // copy into object, so it is displayed when we show the form
  data.item_.seti (fullName, value);
   
  }   // end of ValidateInt

void ValidateFloat (WebClient & w, EditorData & data, 
                  const Object & dtditem, const string & fullName)
  {
  // what they entered
  string enteredData = w.postData_ [fullName];
  double value = 0.0;
  
  // most checks are bypassed for empty fields
  // if you want them applied check "required" first
  
  if (enteredData.empty ())
    {
    data.item_.erasef (fullName);  // erase altogether
    data.eraseItems_.insert (fullName);  // add to list to erase
    return; // all done!
    }

// not empty    
  
  istringstream is (enteredData);

  is >> ws >> value >> ws;
  if (is.fail () || !is.eof ())
     {
     data.errors_ [fullName] = "Entry not numeric";
     return;
     }
  
  if (dtditem.hasf ("min_value") && 
      value < dtditem.getf ("min_value"))
     {
     data.errors_ [fullName] = MAKE_STRING ("Value too low, minimum is " << dtditem.getf ("min_value"));
     return;
     }
    
  if (dtditem.hasf ("max_value") && 
      value > dtditem.getf ("max_value"))
     {
     data.errors_ [fullName] = MAKE_STRING ("Value too high, maximum is " << dtditem.getf ("max_value"));
     return;
     }
   
  // copy into object, so it is displayed when we show the form
  data.item_.setf (fullName, value);
   
  }   // end of ValidateFloat

void ValidateBool (WebClient & w, EditorData & data, 
                  const Object & dtditem, const string & fullName)
  {
  // what they entered
  string enteredData = w.postData_ [fullName];
      
  if (enteredData.empty ())
    enteredData = "0";
  
  bool value;
  istringstream is (enteredData);

  is >> ws >> value >> ws;
  if (is.fail () || !is.eof ())
     {
     data.errors_ [fullName] = "Entry not boolean";
     return;
     }
  
  // copy into object, so it is displayed when we show the form
  data.item_.setb (fullName, value);
   
  }   // end of ValidateBool


struct validateField
{
  WebClient & w_;
  EditorData & data_;
  int count_;
  char type_;

  // ctor
  validateField (WebClient & w, EditorData & data) 
      : w_ (w), data_ (data), count_ (0) {}
   
  void operator() (Object & dtditem)
    {
   
    string name = dtditem.gets ("name");    // field name
    string type = dtditem.gets ("type");    // type of data: s, n, f, b
    
    if (type.empty ())
      throw runtime_error ("No data type in DTD for " + name);
    
    type_ = tolower (type [0]);
        
    // the key field is always required
    if (name == data_.keyname_)
      dtditem.setb ("required", true);
    
    // lengths
    int max_length = dtditem.geti ("max_length", 60);
    int min_length = dtditem.geti ("min_length", 0);
    
    string language;
    string prefix;
    bool multiLanguage = dtditem.getb ("multi_language");
    
    list<pair<string, string> >::const_iterator lang_iter = data_.languages_.begin ();
    
    string fullName = name;   // name with language prefix
      
    // repeat for each supported language - I do it this way because I want to
    // go through the loop at least once
    do
      {
      // get the current language
      if (multiLanguage)
        {
        if (lang_iter != data_.languages_.end ())
          {
          language = lang_iter->first;
          prefix = lang_iter->second;     
          fullName = prefix + name;   // prefix name so success_room becomes fr_success_room
          }// end of another language to go
        }  // end of multi-languages on *this* field
           
      // if field not there, error, otherwise validate it
      if (dtditem.getb ("required") && trim (w_.postData_ [fullName]).empty ())
        data_.errors_ [fullName] = "This field must be entered.";
      else
        {
        if (dtditem.getb ("no_change"))
          data_.item_.erase (fullName);  // erase from list so it won't be copied          
        else
          switch (type_)
            {
            case 's': ValidateString  (w_, data_, dtditem, fullName);   break;
            case 'n': ValidateInt     (w_, data_, dtditem, fullName);   break;
            case 'f': ValidateFloat   (w_, data_, dtditem, fullName);   break;
            case 'b': ValidateBool    (w_, data_, dtditem, fullName);   break;
            default:
              throw runtime_error ("Unknown data type '" + type + "' in DTD for " + name);
              
            } // end of switch on type_
        } // end of not missing required field
      // onto next language
     if (lang_iter != data_.languages_.end ())
        ++lang_iter;
    } while (multiLanguage && lang_iter != data_.languages_.end ());
  }   // end of operator() 
};  // end of validateField

void ValidateInputFields (WebClient & w, EditorData & data)
  {
  // validate everything in the DTD  
  for_each_noreturn (ISEQ (data.dtd_.contents_), validateField (w, data));
  
  } // end of ValidateInputFields

void AddRecord (WebClient & w, EditorData & data)
  {
    
  // now is the time to generate the new key
  if (data.dtd_.getb ("auto_id"))
    data.item_.sets (data.keyname_, UniqueID ());
  
  // get value of new key
  if (data.item_.hasi (data.keyname_))  // rooms will have numeric keys
    data.key_ = MAKE_STRING (data.item_.geti (data.keyname_));
  else
    data.key_ = data.item_.gets (data.keyname_);

  w << "<h2>Insert item '" << FixHTML (data.key_) << "' in " << FixHTML (data.name_) << "</h2>\n";
  
  if (!CanAccessTable (w, "add", data.baseType_))
    throw runtime_error ("You are not permitted to add to this item's data");
  
  // check for duplicates
  
  // find base record 
  thing * t = data.findRecord ();
  if (t != NULL)
    throw runtime_error ("Duplicate item '" + data.key_ + "' in '" + data.name_ + "'");

  // add to relevant list
  data.insert ();
    
  w << "<p><b>Item added.</b></p>";

  webserverlog ("Editor: Inserted item '" + data.key_ + "' into " + data.name_, &w.user_);
    
  } // end of AddRecord

void ChangeRecord (WebClient & w, EditorData & data)
  {
  w << "<h2>Change item '" << FixHTML (data.key_) << "' in " << FixHTML (data.name_) << "</h2>\n";
  
  if (!CanAccessTable (w, "change", data.baseType_))
    throw runtime_error ("You are not permitted to change this item's data");
  
  // find base record 
  thing * t = data.findRecord ();
  if (t == NULL)
    throw runtime_error ("There is no '" + data.key_ + "' in '" + data.name_ + "'");
  
  // if the hash has changed, then someone has edited it while we were looking at it
  if (HashThing (*t) != w.postData_ [HASHFIELD])
    throw runtime_error ("That data has been changed by someone else. Please re-locate it and try again.");
  
  // replace it
  CopyThing (data.item_, *t);
  
  // now erase things that have gone from used to empty (the copy would leave them there
  for (set<string>::const_iterator eraseIter = data.eraseItems_.begin ();
       eraseIter != data.eraseItems_.end ();
       ++eraseIter)
    t->erase (*eraseIter);    // erase from thing so it is gone
  
  // if the hash is the same, they didn't really change anything ;)
  if (HashThing (data.item_) == w.postData_ [HASHFIELD])
    w << "<p><b>Nothing changed.</b></p>";
  else
    {
    w << "<p><b>Item updated.</b></p>";
#if COMMIT_CHANGES
    data.write ();
#endif      
    webserverlog ("Editor: Changed item '" + data.key_ + "' in " + data.name_, &w.user_);
    
    }
  
  } // end of ChangeRecord

void DeleteRecord (WebClient & w, EditorData & data)
  {
  w << "<h2>Delete item '" << FixHTML (data.key_) << "' in " << FixHTML (data.name_) << "</h2>\n";
    
  if (!CanAccessTable (w, "delete", data.baseType_))
    throw runtime_error ("You are not permitted to delete this item's data");
  
  // find base record 
  thing * t = data.findRecord ();
  if (t == NULL)
    throw runtime_error ("There is no '" + data.key_ + "' in '" + data.name_ + "'");
  
   // if the hash has changed, then someone has edited it while we were looking at it
  if (HashThing (*t) != w.postData_ [HASHFIELD])
    throw runtime_error ("That data has been changed by someone else. Please re-locate it and try again.");
  
  if (w.postData_ [CONFIRMDELETE] != "1")
    throw runtime_error ("'Confirm Delete' checkbox was not checked.");
  
  // erase it
  data.erase ();
  
  w << "<p><b>Item deleted.</b></p>";
  
  webserverlog ("Editor: Deleted item '" + data.key_ + "' in " + data.name_, &w.user_);
    
  } // end of DeleteRecord

struct fWebShowOneItem
{
  WebClient & w_;
  EditorData & data_;
  const string & keyField_;
  const string & descriptionField_;
  int count_;
  string searchField_;
  char searchFieldType_;

  // we need to pass down the type in the form because of multi-lingual search fields
  //   (eg. fr_short) which won't be in the DTD
  // thus I use fr_short:s  where the s is the field type

  // ctor
  fWebShowOneItem (WebClient & w, EditorData & data, 
                  const string & keyField, const string & descriptionField) 
      : w_ (w), data_ (data), keyField_ (keyField), 
        descriptionField_ (descriptionField), count_ (0), searchFieldType_ ('s')
        {
        pair<string, string> s = GetWord (data_.searchField_, ":");
        searchField_ = s.first;
        if (!s.second.empty ())
          searchFieldType_ =  tolower (s.second [0]);
        }
  
  void operator() (const thing & item)
    {
    // eliminate via regular expression, if wanted
    if (data_.regexp_.OK ())
      {
      string value;
      switch (searchFieldType_)
        {
        case 's': value = item.gets (searchField_);   break;
        case 'n': value = MAKE_STRING (item.geti (searchField_));   break;
        case 'f': value = MAKE_STRING (item.getf (searchField_));   break;
        case 'b': value = MAKE_STRING (item.getb (searchField_));   break;
        default:
          throw runtime_error ("Unknown data type '" + MAKE_STRING (searchFieldType_) + "' in DTD");
        } // end of switch on type
    
      // no match? give up
      if (!data_.regexp_.Execute (value))
        return;
      } // end of having a regexp
    
    if (count_ < MAXRECORDS)
      {
      w_ << "<li>";
      string key;
      if (item.hasi (keyField_))  // rooms may have numeric keys
        key = MAKE_STRING (item.geti (keyField_));
      else
        key = item.gets (keyField_);
      
      string linkArgs = key;
      
      if (!data_.searchField_.empty ())
         linkArgs += "&" + SEARCHFIELD + "=" + data_.searchField_;

      if (!data_.searchValue_.empty ())
         linkArgs += "&" + SEARCHVALUE + "=" + data_.searchValue_;
      
      Hyperlink (w_, data_, linkArgs, key);
      w_ << FixHTML (item.gets (descriptionField_)) << "\n";
      }
    ++count_; 
    }
    
  const int GetCount () { return count_; }

};  // end of fWebShowOneItem

void ListTable (WebClient & w, EditorData & data, 
                const string & key_field, const string & summary_field)
  {
  
  w << "<ul>\n";

  int count = for_each (ISEQ (*data.getItemList ()), 
                        fWebShowOneItem (w, data, key_field, summary_field)).GetCount ();
  
  w << "</ul>\n";

  w << "<p>" << count << " item(s) in total</p>\n";  
  if (count > MAXRECORDS)
    w << "<p><b>(List above limited to " << MAXRECORDS << " items)</b></p>\n";  
  
  } // end of ListTable

void SearchRecord (WebClient & w, EditorData & data)
  {
  w << "<h2>Search in " << FixHTML (data.name_) << "</h2>\n";
  
  
  if (data.searchValue_.empty ())
    throw runtime_error ("Nothing to search for.");
  
  // compile the search-for regular expression
  data.regexp_.Compile (data.searchValue_);
 
  pair<string, string> s = GetWord (data.searchField_, ":");

  if (s.second.empty ())  
    throw runtime_error ("No data type defined for search field " + data.searchField_);
 
  // now list matching results
  ListTable (w, data, data.keyname_, data.dtd_.gets ("summary_field"));
  
  } // end of SearchRecord

void ListRecords (WebClient & w, EditorData & data)
  {
  
  w << "<h2>List of items in " << data.name_ << "</h2>";

  ListTable (w, data, data.keyname_, data.dtd_.gets ("summary_field"));
  
  } // end of ListRecords

struct editAttribute
{
  WebClient & w_;
  EditorData & data_;
  const string & heading_;
  const char type_;
  int count_;

  // ctor
  editAttribute (WebClient & w, EditorData & data, const string & heading, const char type) 
      : w_ (w), data_ (data), heading_ (heading), type_ (type),  count_ (0) {}
   
  void operator() (const Object & dtditem)
    {
   
    string name = dtditem.gets ("name");    // field name
    string type = dtditem.gets ("type");    // type of data: s, n, f, b
    
    if (type.empty ())
      throw runtime_error ("No data type in DTD for " + name);
    
        // skip wrong types (so we group all numbers together for example)
    if (tolower (type) [0] != type_)
      return;

    // show heading (Eg. "Strings")
    if (++count_ == 1)
      w_ << "<tr bgcolor=\"#99C49B\">" << "<th colspan=\"2\">" 
        << FixHTML (heading_) << "</th>\n";

    // work out how much room to allow
    int size = 20;  // note, defaults are below in the geti calls
    switch (type_)
      {
      case 's': size = dtditem.geti ("max_length", 60); break;  // strings might be long
      case 'n': size = dtditem.geti ("max_length", 10); break;   // numbers, probably only about 10
      case 'f': size = dtditem.geti ("max_length", 10); break;
      } // end of switch on type_
    int maxlength = min (size, 80);   // cut of maximum actually shown at 80 characters
      
    // cannot change key unless we are entering a new record
    bool cannotChange = (name == data_.keyname_) &&  
                            !data_.addingAction_;  // field is key
    
    // some fields we just don't want to change (eg. dates set by the server)
    if (dtditem.getb ("no_change"))
      cannotChange = true;
    
    string language;
    string prefix;
    bool multiLanguage = dtditem.getb ("multi_language");
    
    list<pair<string, string> >::const_iterator lang_iter = data_.languages_.begin ();
    
    string fullName = name;   // name with language prefix
    
    // repeat for each supported language - I do it this way because I want to
    // go through the loop at least once
    do
      {
      // get the current language
      if (multiLanguage)
        {
        if (lang_iter != data_.languages_.end ())
          {
          language = lang_iter->first;
          prefix = lang_iter->second;     
          fullName = prefix + name;   // prefix name so success_room becomes fr_success_room
          }// end of another language to go
        }  // end of multi-languages on *this* field
           
      // field label - "fullName" from dtd
      w_ << "<tr><th>" << FixHTML (fullName) << "</th>";
      // field data
      w_ << "<td>";
      
      string value;
      
      // On a new record, there is no existing data
      if (data_.action_ != NEW_ACTION) 
         value = w_.postData_ [fullName];    // what they actually typed
      
      // auto-generated keys will be just shown as "(auto)" until they are generated
      if (name == data_.keyname_ &&     // field is key
          data_.addingAction_ &&        // and we are adding a record
          data_.dtd_.getb ("auto_id"))  // and they want auto-generation
          {
          data_.item_.sets (fullName, "(auto)");
          cannotChange = true;
          }
     
       /*
      This mucking around here is because I need to display two different things:
      
      a) The incorrect data you actually type, if it is wrong (eg. "abcd" in a numeric field)
      b) The actual correct data from the data record, if it isn't wrong (or on a list or
         such situation where you don't get the chance to make mistakes)
      
      I won't bother with bools, they shouldn't really be wrong, being a checkbox
      */
      
      switch (tolower (type_))
        {
        case 's': if (!data_.fieldHasError (fullName))
                    value = data_.item_.gets (fullName);
                  if (cannotChange)
                    {
                    w_ << FixHTML (value);   //  can't be edited
                    Hidden (w_, fullName, value);   // have to preserve it however
                    } // end of cannotChange
                  else
                    {
                    // combo box, input box, or straight single-line field
                    if (dtditem.getb ("choose_set") && !dtditem.gets ("match_set").empty ())
                      ComboBox (w_, fullName, value, dtditem.gets ("match_set"));
                    else if (dtditem.geti ("lines") > 1)
                      InputBox (w_, fullName, value, dtditem.geti ("lines")); 
                    else
                      InputField (w_, fullName, value, size, maxlength); 
                    } // end of not primary key
                  break;
        case 'n': if (!data_.fieldHasError (fullName))
                    {
                    if (data_.item_.hasi (fullName))   // only if present
                      value = MAKE_STRING (data_.item_.geti (fullName));
                    } // end of not error
                  if (cannotChange)
                    {
                    w_ << FixHTML (value);   // can't be edited
                    Hidden (w_, fullName, value);   // have to preserve it however
                    } // end of cannotChange
                  else
                    InputField (w_, fullName, value, size, maxlength, true); 
                  break;
        case 'f': if (!data_.fieldHasError (fullName))
                    {
                    if (data_.item_.hasf (fullName))   // only if present
                      value = MAKE_STRING (data_.item_.getf (fullName));
                    } // end of not error
                  if (cannotChange)
                    {
                    w_ << FixHTML (value);   // can't be edited
                    Hidden (w_, fullName, value);   // have to preserve it however
                    } // end of cannotChange
                  else
                    InputField (w_, fullName, value, size, maxlength, true); 
                  break;
        case 'b': if (cannotChange)
                    {
                    w_ << FixHTML (MAKE_STRING (value));   // can't be edited
                    Hidden (w_, fullName, value);   // have to preserve it however
                    } // end of cannotChange
                  else
                    CheckBox   (w_, fullName, data_.item_.getb (fullName)); 
                  break;
        default:
          throw runtime_error ("Unknown data type '" + type + "' in DTD for " + name);
          
        } // end of switch on type_
      
      // show error for this field, if any
      if (data_.fieldHasError (fullName))
        w_ << "<br><b><font color=\"#FF0000\">"
           << data_.errors_ [fullName] << "</font></b>\n";
      
      // put description of field in small letters underneath it
      string describe = dtditem.gets ("describe");
      if (!describe.empty () || (!language.empty () && multiLanguage))
        {
        w_ << "<br><font size=-2>";
        w_ << nl2br (FixHTML (describe));
        if (!language.empty () && multiLanguage )
          w_ << " (Language: " << FixHTML (tocapitals (language)) << ")";
        w_ << "</font>\n";
        }
      
      // show "field required" in dark green
      if (cannotChange)
        w_ << " <br><font color=\"#006400\">(Field cannot be changed)</font>\n";
      else
        if (dtditem.getb ("required") || name == data_.keyname_)
          w_ << "<br><font color=\"#006400\">(Required field)</font>\n";

      if (dtditem.getb ("regexp"))
        w_ << "<font color=\"#006400\"> (Regular expression)</font>\n";
        
      // link to dtd info for this field (in new browser window)
      w_ << " <font size=-2><a href=\"dtd.php?dtd=" << data_.dtd_.gets ("name") 
         << "&field=" << dtditem.gets ("name") 
         << "\" target=\"_blank\">(?)</a></font>\n";
      
      w_ << "</td></tr>\n";
      
      // onto next language
     if (lang_iter != data_.languages_.end ())
        ++lang_iter;
    } while (multiLanguage && lang_iter != data_.languages_.end ());
  }   // end of operator() 
};  // end of editAttribute


void ShowRecord (WebClient & w, EditorData & data)
  {
  
  // no action? we are just viewing it
  if (data.action_.empty ())
    w << "<h2>View item '" << FixHTML (data.key_) << "' in " << FixHTML (data.name_) << "</h2>\n";
  
  w << "<p><table>\n";
  for_each_noreturn (ISEQ (data.dtd_.contents_), editAttribute (w, data, "Strings", 's'));  
  for_each_noreturn (ISEQ (data.dtd_.contents_), editAttribute (w, data, "Numbers", 'n'));  
  for_each_noreturn (ISEQ (data.dtd_.contents_), editAttribute (w, data, "Floats", 'f'));  
  for_each_noreturn (ISEQ (data.dtd_.contents_), editAttribute (w, data, "Flags", 'b'));  
  w << "</table></p>\n";

   // if the form had an error, keep the original hash (the one they had when they
  // started editing it)
  if (data.haveError ())
   Hidden (w, HASHFIELD, w.postData_ [HASHFIELD]);  // keep original hash
  else
   Hidden (w, HASHFIELD, HashThing (data.item_));   // hash this version
  
  // we won't find sub-items when doing an add, now will we?
  if (data.addingAction_)
      return;
   
  // while we do owned items, save the itemlist pointer, and the main data name
  ObjectList * savedItemList = data.getItemList ();
  string savedName = data.name_;
      
  string subDtdName = data.dtd_.gets ("sub_dtd_name");
  
  // if it has sub-dtd produce a list
  if (!subDtdName.empty ())
    {
    // get the sub dtd (for its description, key and summary fields)
    ObjectList alldtd = control ().items_ ["dtd"];
    // find DTD for this sub-table
    ObjectIter dtditer = FindObject (alldtd, "name", subDtdName);
    if (dtditer == alldtd.end ())
      throw runtime_error ("There is no (sub) DTD for " + subDtdName);
    
    w << "<h3>" <<  FixHTML (dtditer->gets ("description")) << " ...</h3>";
        
     // find the main item
    ObjectIter mainItemIter;

    if (data.name_ == "rooms")
      mainItemIter = FindObject (*savedItemList, data.dtd_.gets ("key"), 
                                convertString<int> (data.key_, "Bad vnum"));
    else
      mainItemIter = FindObject (*savedItemList, data.dtd_.gets ("key"), data.key_);
    
    if (mainItemIter == savedItemList->end ())
      throw runtime_error ("Cannot find entry for " + data.key_);
    // make sub tables look like: table:key:sub_dtd_name
    data.name_ = savedName + ":" + data.key_ + ":" + data.dtd_.gets ("sub_dtd_name");   
    data.setItemList (& mainItemIter->contents_);
    ListTable (w, data, dtditer->gets ("key"), dtditer->gets ("summary_field"));
  
   // give them a link to add things, there may not be any there already
    Hyperlink (w, data, 
              "&" + ACTIONFIELD + "=" + NEW_ACTION, 
              "[ Add " + dtditer->gets ("description") + " ]");
    
    } // end of a sub-dtd
  
  
  // put item list pointer and name back
  data.setItemList (savedItemList);
  data.name_ = savedName;
 
  } // end of ShowRecord

void NewRecord (WebClient & w, EditorData & data)
  {
  w << "<h2>New item in " << FixHTML (data.name_) << "</h2>\n";
  
  data.key_.clear ();   // clear old key, if any, so we will see blank record
  data.item_.clear ();  // and clear all existing data
  
  } // end of NewRecord

void DuplicateRecord (WebClient & w, EditorData & data)
  {
  w << "<h2>Duplicate item '" << FixHTML (data.key_) << "' in " << FixHTML (data.name_) << "</h2>\n";
    
  } // end of DuplicateRecord

string getOrPostData (WebClient & w, EditorData & data, const string & item)
  {
  string result;
  
  result = w.postData_ [item];
  // key might be on URI also (eg for hyperlinking on a list)
  if (result.empty () && data.action_.empty ())
    result = w.getData_ [item]; 
  
  return result;
  } // end of getOrPostData

void WebEditor (WebClient & w)
  {
  EditorData data;
    
  // extract out data from form ...
  
  // what to edit
  data.name_ = getOrPostData (w, data, NAMEFIELD);

  // action field might be implied by the name of the button
  if (!w.postData_ ["_" + ADD_ACTION].empty ())
    data.action_ = ADD_ACTION;
  else if (!w.postData_ ["_" + SEARCH_ACTION].empty ())
    data.action_ = SEARCH_ACTION;
  else if (!w.postData_ ["_" + LIST_ACTION].empty ())
    data.action_ = LIST_ACTION;
  else if (!w.postData_ ["_" + NEW_ACTION].empty ())
    data.action_ = NEW_ACTION;
  else if (!w.postData_ ["_" + DUPLICATE_ACTION].empty ())
    data.action_ = DUPLICATE_ACTION;
  else if (!w.postData_ ["_" + DELETE_ACTION].empty ())
    data.action_ = DELETE_ACTION;
  else if (!w.postData_ ["_" + AMEND_ACTION].empty ())
    data.action_ = AMEND_ACTION;    
  else if (!w.postData_ ["_" + EDIT_ACTION].empty ())
    data.action_ = EDIT_ACTION;    
  
 // action (add, change, delete, search, list)
  if (data.action_.empty ())
    data.action_ = getOrPostData (w, data, ACTIONFIELD);
    
  // record key
  data.key_ = getOrPostData (w, data, KEYFIELD);
  
  // what field to search
  data.searchField_ = getOrPostData (w, data, SEARCHFIELD);

  // what to search for
  data.searchValue_ = getOrPostData (w, data, SEARCHVALUE);
  
  // build list of available languages
  ObjectList & languages = control ().items_ ["languages"];   
  for (ObjectList::const_iterator lang_iter = languages.begin ();
       lang_iter != languages.end ();
        ++lang_iter)
      data.languages_.push_back (
          pair<string, string> (lang_iter->gets ("name"), lang_iter->gets ("prefix")));
  
  // OK, ready to start page up ...
  DoHeader (w, 200, "Edit " + data.name_);
  
//  w << "languages = " << data.languages_.size () << "<br>";
  
#if DEBUG_HTTP  
  WebDebugVars (w);   // show headers, get, post, cookie data
#endif
    
  // the whole thing will be a form ...
  w << "<form Method=\"post\" Action=\"" + w.URL_ + "\">";

  // change tables - switch table name, clear action (for simple buttons list)
  if (data.action_ == EDIT_ACTION)
    {
    data.name_ = w.postData_ [NEWNAME];
    data.action_.clear ();    // default to viewing
    data.key_.clear ();       // default to no record
    }
  
  // we need  table name, otherwise we can't do much
  // if no name specified, just let them switch tables
  if (!data.name_.empty ())
    {
    pair<string, string> splitName = GetWord (data.name_, ":");   // split off "rooms:1234:exit"
    
    data.baseType_ = splitName.first;
    
    // integrity check - can they edit it?
    
    if (!CanAccessTable (w, "view", data.baseType_))
      throw runtime_error ("You are not permitted to edit this item's data");
    
    // get whole dtd
    ObjectList alldtd = control ().items_ ["dtd"];
    // find DTD for this table
    ObjectIter dtditer = FindObject (alldtd, "name", data.baseType_);
    if (dtditer == alldtd.end ())
      throw runtime_error ("There is no DTD for " + data.baseType_);
    // save it for validating, showing, etc.
    data.dtd_ = *dtditer;    // copy dtd
    
    // what the key is
    data.keyname_ = data.dtd_.gets ("key");
    
    // get the appropriate table's list
    ObjectList * itemlist = getObjectList (w, data.baseType_);
    data.setItemList (itemlist); 
    
    if (!(splitName.second).empty ())
      {
      pair<string, string> typeAndKey = GetWord (splitName.second, ":");   // get 1234:exit
      string key = typeAndKey.first;      // eg. (vnum) 1234
      string type = typeAndKey.second;    // exit
      string subDtdName = "!!!";
      
      // link back to parent
      
      w << "<p><a href=\"/" << w.URL_ << "?" << NAMEFIELD << "=" << data.baseType_ 
        << "&" << KEYFIELD << "=" << key << "\">"
        << FixHTML ("Edit parent: item " + key + " in " + data.baseType_) << "</a></p>\n";
      
     // find base record 
      ObjectIter i;

      if (data.baseType_ == "rooms")
        i = FindObject (*itemlist, data.keyname_, convertString<int> (key, "Bad vnum"));
      else
        i = FindObject (*itemlist, data.keyname_, key);
      if (i == itemlist->end ())
        throw runtime_error ("There is no '" + key + "' in '" + data.name_ + "'");
      
      // now switch to DTD for this sub table
      data.setItemList (&(i->contents_));
      subDtdName = type;    // eg. sub_dtd, effects, objects, blah blah
      
      // now switch to DTD for this sub table
      ObjectIter dtditer = FindObject (alldtd, "name", subDtdName);
      if (dtditer == alldtd.end ())
        throw runtime_error ("There is no DTD for sub table " + subDtdName);
      // save it for validating, showing, etc.
      data.dtd_ = *dtditer;    // copy dtd   
 
      // what the key now is
      data.keyname_ = data.dtd_.gets ("key");      
      }   // end of wanting a sub-item (eg. an exit for a room)
          
    // on an amend, delete or search hyperlink, find the record they wanted
    if (data.action_ == AMEND_ACTION ||
        data.action_ == DELETE_ACTION ||
        data.action_ == DUPLICATE_ACTION ||
        (data.action_.empty () && !data.key_.empty ()))    // search results
      FindRecord (w, data);
    
    // validate fields on an add or amend
    if (data.action_ == ADD_ACTION || 
        data.action_ == AMEND_ACTION)
      ValidateInputFields (w, data);
    
    // process various actions
         
    if (data.haveError ())   // skip add/change if validation error
      ShowError (w, tocapitals (data.action_) + " not done");
    else if (data.action_ == ADD_ACTION)
      AddRecord (w, data); 
    else if (data.action_ == AMEND_ACTION)   
      ChangeRecord (w, data); 
    else if (data.action_ == DELETE_ACTION)  
      DeleteRecord (w, data); 
    else if (data.action_ == SEARCH_ACTION)  
      SearchRecord (w, data);  
    else if (data.action_ == LIST_ACTION)    
      ListRecords (w, data);     
    else if (data.action_ == NEW_ACTION)   
      NewRecord (w, data);               
    else if (data.action_ == DUPLICATE_ACTION)   
      DuplicateRecord (w, data); 
    else if (!data.action_.empty ())
      throw runtime_error ("Unknown action: " + data.action_);
    
    // we are adding on a "new" or "duplicate" or failed "add"
    data.addingAction_ = (data.key_.empty () && 
                          (data.action_ == NEW_ACTION ||
                          (data.action_ == ADD_ACTION && data.haveError ()))
                          )
                        ||  
                          ( 
                          !data.key_.empty () &&  
                          data.action_ == DUPLICATE_ACTION 
                          );
                   
    // display the current record
    // when doing a search, or after a delete, don't display the record 
    if ((!data.action_.empty () || !data.key_.empty ())
        && data.action_ != SEARCH_ACTION 
        && data.action_ != LIST_ACTION
        && data.action_ != DELETE_ACTION)  
        ShowRecord (w, data);

    // just seeing a few buttons? tell them what they are currently editing
    if (data.action_.empty () && data.key_.empty ())
      w << "<h2>Editing " << FixHTML (data.name_) << "</h2>\n";
    
    }   // end of table name specified
  
  // show add/change/delete buttons, as appropriate
  
  ShowButtons (w, data);
     
  w << "</form>\n";
    
  } // end of WebEditor

static int LoadWebMap ()
  {
static WebHandlerMap & webmap = getWebHandlerMap ();

  cout << "Loading web handlers in " __FILE__ << endl;
    
  WEBPAGE (WebEditor);        // edit things

  return 0;
  } // end of LoadWebMap

static int loadThemUp = LoadWebMap ();

