/*

 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.
 
*/

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

// 5 minutes
static const int REFRESH_INTERVAL = 5 * 60;    // refresh interval for certain pages (seconds)
// 7 days
static const int COOKIE_EXPIRY = 60 * 60 * 24 * 7;   // when cookie expires 

static const int MAXRECORDS = 200;    // max rooms in rooms list

//~ PLAYER (number):
  
const string ROOM = "room";     // current room
const string COINS = "coins";   // coins s/he is carrying

//~ PLAYER (string):

const string NAME = "name";     // player name

const string DoHeader (WebClient & w,           // which client
                      const int messageType,  // type, eg, 200, 404
                      const string & title,         // what to put into title
                      const bool sendBody)    // if true, start a message body
  {
  w.inbuf_.clear ();
  
  w.connstate_ = eSendingResponse;
  
  string message = SendMessage (w, messageType);
  w << "Date: " << GetBrowserTime () << "\n";
  w << "Server: " << MUDNAME << " " << VERSION << "\n";
  w << "Connection: close\n";
  w << "X-Powered-By: BabbleMUD/" << VERSION << "\n";  
  
  if (w.verified_)
    w << "Set-Cookie: token=" << w.user_.gets ("token") << "; "
      << "path=/; "
      << "expires=" << GetBrowserTime (time (NULL) + COOKIE_EXPIRY) << ";\n";
 //     << "secure=0\n";    // doesn't seem to work

  if (sendBody)
    {
    // if the title is empty use the message type (eg. 404 error) as the title
    if (!title.empty ())
      message = title;
    
    string header = FindAndReplace (control ().gets ("html_header"), "~", message);
    
    w << "Content-Type: text/html; charset=iso-8859-1\n";
    w << "\n" << header << "\n";
    
    if (w.verified_)
      {
      w << "<p align=\"right\">You are logged in as "
        << tocapitals (w.user_.gets ("name")) 
        << ". <a href=\"logout.php\">[Log Out]</a><p>\n";
      } // end of being logged in
    
    // someone has to say it! ;)
    w << "\n\n<!-- HTML generated by BabbleMUD integrated web server, written by Nick Gammon. -->\n\n\n";
    
    } // end of body wanted
  
  return message;
  } // end of DoHeader

// show things 
template <typename T>
struct showWebAttribute
{
  WebClient & w_;
  // ctor
  showWebAttribute (WebClient & w, const string & name) : w_ (w) 
    {
    w_ << "<tr bgcolor=\"#99C49B\">" <<
          "<th colspan=\"2\">" << FixHTML (name) << "</th>\n";
    }
   
  void operator() (const pair<string, T> & data)
    {
    w_ << "<tr><th>" << FixHTML (data.first) << "</th>" <<
          "<td>" << FixHTML (MAKE_STRING (data.second)) << "</td></tr>\n";  
    }
};  // end of showWebAttribute

template <typename T>
void TableRow (WebClient & w, const string & heading, const T & data, const bool right = false)
  {
  string align;
  if (right)
   align = " align=\"right\" style=\"text-align:right;\" ";
  
  w << "<tr>";
  w << "<th>" << FixHTML (heading) << "</th>";
  w << "<td " << align << ">" << FixHTML (MAKE_STRING (data)) << "</td>";
  w << "</tr>\n";
  } // end of TableRow

// general thing-shower - used for displays (not XML-writing)
static void ShowWebThing (WebClient & w, const thing & o)
  {
  
  w << "<p><table>\n";
  // string data
  if (!o.get_sdata ().empty ())
    for_each_noreturn (ISEQ (o.get_sdata ()), showWebAttribute<string> (w, "Strings"));  
  
  // int data
  if (!o.get_idata ().empty ())
    for_each_noreturn (ISEQ (o.get_idata ()), showWebAttribute<int> (w, "Numbers"));  
  
  // float data
  if (!o.get_fdata ().empty ())
    for_each_noreturn (ISEQ (o.get_fdata ()), showWebAttribute<double> (w, "Floats"));  
  
  // bool data
  if (!o.get_bdata ().empty ())
    for_each_noreturn (ISEQ (o.get_bdata ()), showWebAttribute<bool> (w, "Flags"));  

  w << "</table></p>\n";
  
  } // end of ShowWebThing

struct fWebShowThing
{
  WebClient & w_;
  int count_;
  const string & name_;

  // ctor
  fWebShowThing (WebClient & w, const string & name) : w_ (w), count_ (0), name_ (name) {}
  
  void operator() (const Object & thing)
    {
    w_ << "<li><b> " << name_ << " " << ++count_ << "</b>\n";
    ShowWebThing (w_, thing); 
    w_ << "<p></p><hr>\n";

    if (!thing.contents_.empty ())
      {
      w_ << "<p><b>Contents</b></p><ul>\n";
      for_each (ISEQ (thing.contents_), fWebShowThing (w_, "Contents item"));
      w_ << "</ul>\n";
      }
  
    }
};  // end of fWebShowThing

void WebShowOnePlayer (WebClient & w)
  {
  string name = w.getData_ ["name"];
  
  DoHeader (w, 200, "Player " + name);
    
  Character * p = FindPlayer (name);
  
  if (!p)
    throw runtime_error (name + " is not playing.");
  
  ShowWebThing (w, *p);
  
  w << "<hr><h2>Inventory</h2><ul>\n";
  
  for_each (ISEQ (p->inventory ()), fWebShowThing (w, "Inventory item"));
  
  w << "</ul>\n";
  
  w << "<p><a href=\"/who.php\">";
  w << "Show who list</a>\n";
      
  }

void WebWhoList (WebClient & w)
  {
  
  if (!w.getData_ ["name"].empty ())
    {
    WebShowOnePlayer (w);
    return;
    }
  
  DoHeader (w, 200, "Who List");
  
  w << "<h2>Who list</h2>\n";
  w << "<meta http-equiv=\"refresh\" content=\"" << REFRESH_INTERVAL << "\">\n";  // refresh every x minutes
  w << "<ul>\n";
  
  int count = 0;
  for (PlayerListIterator iter = playerlist ().begin ();
       iter != playerlist ().end ();
       ++iter)
    {
    Player * pTarget = *iter;    // the player  
    if (pTarget->IsPlaying ())
      {
      w << "<li><a href=\"who.php?name=" << pTarget->gets (NAME) << "\">"
            << FixHTML (pTarget->gets (NAME)) << 
            "</a> in room " << pTarget->geti (ROOM) << "\n";
      ++count;
      } // end of if playing
    } // end of doing each player
  
  w << "</ul>\n";
  
  w << "<p>" << count << " player(s)</p>\n";    
  
  } // end of WebWhoList

void WebShowOneRoom (WebClient & w)
  {
  DoHeader (w, 200, "Room");
  
  w << "<h2>Room " << FixHTML (w.getData_ ["room"]) << "</h2>\n";
  
  int vnum = convertString<int>  (w.getData_ ["room"], "bad numeric value");
  
  World & r = FindRoom (vnum);
  
  ShowWebThing (w, r);
  
  w << "<hr><h2>Exits</h2><ul>\n";
  
  for_each (ISEQ (r.exits ()), fWebShowThing (w, "Exit"));
  
  w << "</ul>\n";
  
  w << "<p><a href=\"/room.php\">";
  w << "Show rooms list</a>\n";
    
  } // end of WebShowOneRoom

struct fWebShowOneRoom
{
  WebClient & w_;
  int count_;

  // ctor
  fWebShowOneRoom (WebClient & w) : w_ (w), count_ (0) {}
  
  void operator() (const pair<int, World> item)
    {
    if (count_ < MAXRECORDS)
      {
      w_ << "<li>";
      w_ << "<a href=\"/room.php?room=" << item.first << "\">";
      w_ << "vnum: " << item.first << "</a> " << 
        FixHTML (item.second.roomName ("")) << "\n";
      }
    ++count_; 
    }
  
  const int GetCount () { return count_; }

};  // end of fWebShowOneRoom

void WebRoomList (WebClient & w)
  {
  if (!w.getData_ ["room"].empty ())
    {
    WebShowOneRoom (w);
    return;
    }
  
  DoHeader (w, 200, "Room List");
  w << "<h2>Room list</h2>\n";
  w << "<ul>\n";
  int count = for_each (ISEQ (world ()), fWebShowOneRoom (w)).GetCount ();
  w << "</ul>\n";
  w << "<p>" << count << " room(s)</p>\n";   

  if (count > MAXRECORDS)
    w << "<p><b>(List above limited to " << MAXRECORDS << " items)</b></p>\n";  

  } // end of WebRoomList

void WebControlList (WebClient & w)
  {
  DoHeader (w, 200, "Control data");
  ShowWebThing (w, control ());
  } // end of WebControlList

void WebShowOneSpell (WebClient & w)
  {
  DoHeader (w, 200, "Spell");
  
  w << "<h2>Spell " << FixHTML (w.getData_ ["spell"]) << "</h2>\n";

  ObjectList & spells = control ().items_ ["spells"];
  ObjectIter iSpell = FindObject (spells, "name", w.getData_ ["spell"]);
  if (iSpell == spells.end ())
    throw runtime_error ("There is no spell " + w.getData_ ["spell"]);
 
  ShowWebThing (w, *iSpell);
  
  w << "<hr><h2>Effects</h2><ul>\n";
  
  for_each (ISEQ ((*iSpell).contents_), fWebShowThing (w, "Effect"));
  
  w << "</ul>\n";
  
  w << "<p><a href=\"/spell.php\">";
  w << "Show spells list</a>\n";
    
  } // end of WebShowOneSpell

struct fWebShowOneSpell
{
  WebClient & w_;
  int count_;

  // ctor
  fWebShowOneSpell (WebClient & w) : w_ (w), count_ (0) {}
  
  void operator() (const Object & spell)
    {
    w_ << "<li>";
    w_ << "<a href=\"/spell.php?spell=" << spell.gets ("name") << "\">";
    w_ << spell.gets ("name") << "</a> " << 
       spell.gets  (FixHTML ("summary")) << "\n";
    ++count_; 
    }
  
  const int GetCount () { return count_; }

};  // end of fWebShowOneSpell

void WebSpellList (WebClient & w)
  {
  
  if (!w.getData_ ["spell"].empty ())
    {
    WebShowOneSpell (w);
    return;
    }

  ObjectList & spells = control ().items_ ["spells"];

  DoHeader (w, 200, "Spell List");
  w << "<h2>Spell list</h2>\n";
  w << "<ul>\n";
  int count = for_each (ISEQ (spells), fWebShowOneSpell (w)).GetCount ();
  w << "</ul>\n";
  w << "<p>" << count << " spell(s)</p>\n";    
  } // end of WebSpellList

void WebSend (WebClient & w)
  {
  
  DoHeader (w, 200, "Send message");
  w << "<h2>Send message</h2>\n";
 
  if (w.postData_ ["action"] == "send_to_all" &&
      !w.postData_ ["message"].empty ())
    {
    // send message *code*
    SendToAll (w.postData_ ["message"], messagemap (), ThingMap ());
    w << "<p>Message code <b>" << FixHTML (w.postData_ ["message"]) 
      << "</b> sent to everybody.</p>";    
    }
  else
    throw runtime_error ("No message");
    
  } // end of WebSend

void WebShowDtd (WebClient & w)
  {
  string dtdName = w.getData_ ["dtd"];
  string dtdField = w.getData_ ["field"];
  
  DoHeader (w, 200, "Field information for " + dtdField + " in " + dtdName);
  
  w << "<h2>Field information for "  << FixHTML (dtdField) << " in " << FixHTML (dtdName) << "</h2>\n";

  // find DTD (eg. spells)
  ObjectList & dtd = control ().items_ ["dtd"];
  ObjectIter dtdIter = FindObject (dtd, "name", dtdName);
  if (dtdIter == dtd.end ())
    throw runtime_error ("There is no DTD for table " + dtdName);
 
  // find field (eg. spellname)
  ObjectIter fieldIter = FindObject (dtdIter->contents_, "name", dtdField);
  if (fieldIter == dtd.end ())
    throw runtime_error ("There is no DTD for field " + dtdField + " in DTD " + dtdName);  
  
  ShowWebThing (w, *fieldIter);
  
  } // end of WebShowDtd

struct fWebShowOneCommand
{
  WebClient & w_;
  int count_;

  // ctor
  fWebShowOneCommand (WebClient & w) : w_ (w), count_ (0) {}
  
  void operator() (const pair<string, string> command )
    {
    TableRow (w_, command.first, command.second);
    ++count_; 
    }
  
  const int GetCount () { return count_; }

};  // end of fWebShowOneCommand

void WebCommandList (WebClient & w)
  {
    
// now list commands they know

CiStringMap knownCommands;
list<Object>::iterator commandIter;

// build map of commands they know and can use
  for (commandIter = commands ().begin ();
        commandIter != commands ().end ();
        ++commandIter)
    {
    Object & command = *commandIter;
    string name = command.gets ("name");
    string summary = command.gets ("summary");
    if (command.gets ("needflag").empty () && // don't show commands that need special permissions
        !name.empty () &&       // need a name
        !summary.empty ())      // need a description
      knownCommands [name] = summary;
    }
  
  DoHeader (w, 200, "Commands List");
  w << "<h2>Commands list</h2>\n";
  w << "<table>\n";
  int count = for_each (ISEQ (knownCommands), fWebShowOneCommand (w)).GetCount ();
  w << "</table>\n";
  w << "<p>" << count << " commands</p>\n";    
  } // end of WebCommandList

void WebServerInfo (WebClient & w)
  {
  DoHeader (w, 200, "Server Information");
  w << "<h2>Server Information</h2>\n";
  w << "<meta http-equiv=\"refresh\" content=\"" << REFRESH_INTERVAL << "\">\n";  // refresh every x minutes
  w << "<p>(Figures for this session only)</p>\n";
  
  // time now
  time_t t = time (NULL);
  
  // time up in human-readable form
  string timeUp = intervalString (t - serverInfo ().geti ("started_time"));
  
  w << "<table>\n";
  
  TableRow (w, "Server started at", serverInfo ().gets ("started_at"));
  TableRow (w, "Time now",          stringDate (t));
  TableRow (w, "Server up time",    timeUp);
  TableRow (w, "Last player to connect",        serverInfo ().gets ("last_player_started"));
  TableRow (w, "Last player on at",             serverInfo ().gets ("last_player_started_date"));
  TableRow (w, "Last player to quit",           serverInfo ().gets ("last_player_quit"));
  TableRow (w, "Last player quit at",           serverInfo ().gets ("last_player_quit_date"));
  TableRow (w, "Number of player connections",  serverInfo ().geti ("player_on_count"), true);
  TableRow (w, "Hits on integrated web server", serverInfo ().geti ("web_server_hits"), true);
  TableRow (w, "Rooms",                         world ().size (), true);
  TableRow (w, "Objects",                       objects ().size (), true);
  TableRow (w, "Mobs",                          mobs ().size (), true);
  TableRow (w, "Resets",                        resets ().size (), true);
  TableRow (w, "Commands",                      commands ().size (), true);
  TableRow (w, "Command handlers",              getCommandHandlerMap ().size (), true);
  TableRow (w, "Spells",                        control ().items_ ["spells"].size (), true);
  TableRow (w, "Classes",                       control ().items_ ["classes"].size (), true);
  TableRow (w, "Languages supported",           control ().items_ ["languages"].size (), true);
  TableRow (w, "Error messages",                errormap ().size (), true);
  TableRow (w, "Other messages",                messagemap ().size (), true);
  TableRow (w, "Events queued",                 eventQueue ().size (), true);
  TableRow (w, "Player commands entered",       serverInfo ().geti ("commands_entered"), true);
  TableRow (w, "Player commands processed successfully",  serverInfo ().geti ("command_success"), true);
  TableRow (w, "Player commands with errors",             serverInfo ().geti ("command_errors"), true);
  TableRow (w, "Mob commands processed successfully",     serverInfo ().geti ("mob_command_success"), true);
  TableRow (w, "Mob commands with errors",                serverInfo ().geti ("mob_command_errors"), true);
   
  w << "</table>\n";
  } // end of WebServerInfo

const string USERNAME = "_username";
const string PASSWORD = "_password";
const string LOGIN_ACTION = "_login";

void ShowLoginForm (WebClient & w, const bool failure)
  {
  w.verified_ = false;
  
  DoHeader (w, 200, "Login");
  
//  WebDebugVars (w);   // show headers, get, post, cookie data
  
  w << "<h1>Server login</h1>\n";
  
  if (failure)
    {
    badWebPasswords () [w.address_]++;   // count bad attempts from this IP
    w << "<p><b>That username/password combination was not correct.</b></p>";    
    }
  else
    w << "<p>You must log in to use this function.</p>";
  
 // the whole thing will be a form ...
  w << "<form Method=\"post\" Action=\"login.php\">";

  w << "<table border=\"0\" style=\"border:0;\">\n";
  w << "<tr><th>Username:</th>";
  w << "<td>"; InputField (w, USERNAME, "", 40, 40); w << "</td></tr>\n";

  w << "<tr><th>Password:</th>";
  w << "<td>"; InputField (w, PASSWORD, "", 40, 40, false, true); w << "</td></tr>\n";
  w << "</table>\n";
  
  w << "<p>";
  Button (w, LOGIN_ACTION, "Log in");
  w << "</p>\n";
  
  w << "</form>\n";  
  
  }

bool verifyWebUser (WebClient & w, Object & page)
  {
  string flag = page.gets ("needflag");

  // some pages may allow non-logged-in users
  if (page.getb ("allow_guest") && flag.empty ())
    return true;
  
  // this user is verified - ie. we know who they are
  if (w.verified_)
    {
    // but can they access *this* page?
    if (!flag.empty ())
      {
      if (!w.user_.getb (flag))
        {
        DoHeader (w, 200, "Not permitted");
        w << "<p><b>You are not authorised to access this page.</b></p>";    
        return false;
        }   // end of not permitted
      }   // this page needs a flag set to use it
    return true;
    }
 
  ShowLoginForm (w, false);
  
  return false;
  
  } // end of verifyWebUser

void WebLogin (WebClient & w)
  {

  // they are logged in, let them log out
  if (w.verified_)
    {
    DoHeader (w, 200, "Login");
    w << "<h1>Login</h1>\n";
    w << "<p>You are already logged in as "
      << w.user_.gets ("name") << "</p>\n";
    w << "<a href=\"logout.php\">Log Out</a><p>\n";
    return;
    }
  
  string username = trim (tolower (w.postData_ [USERNAME]));
  string password = trim (w.postData_ [PASSWORD]);

  if (username.empty () || password.empty ())
    {
    ShowLoginForm (w, true);
    return;
    } // end of no username or pasword

  ObjectList & webUsers = control ().items_ ["web_users"];
  ObjectIter iUser = FindObject (webUsers, "name", username);
  if (iUser == webUsers.end ())
    {
    ShowLoginForm (w, true);
    return;
    } // end of name not found

  if (SecureHash (password).str () != iUser->gets ("password"))
    {
    ShowLoginForm (w, true);
    return;
    } // end of password wrong
    
  if (iUser->getb ("blocked"))
    {
    ShowLoginForm (w, true);
    return;
    } // end of user blocked

  iUser->sets ("token", makeRandomHash ());   // give them a new token
  iUser->sets ("logged_on_at", stringDate (time (NULL)));
  iUser->sets ("logged_on_ip_address", w.address_);
  badWebPasswords () [w.address_] = 0;    // this IP is OK
  
  // save change so we remember their token
  WriteTopThing (CONTROL_FILE, "control", control ());
    
  w.verified_ = true;   // they have verified themselves
  w.user_ = *iUser;     // remember their details
  DoHeader (w, 200, "Login confirmation");
  
  w << "<h1>Login confirmation</h1>\n";
  w << "<p>You have logged in.</p>\n";
  
  } // end of WebLogin

void WebLogout (WebClient & w)
  {
  DoHeader (w, 200, "Logout confirmation");

  w << "<h1>Logout confirmation</h1>\n";

  if (!w.verified_)
    throw runtime_error ("Not logged in");
  
  // we need to locate them in the control file
  ObjectList & webUsers = control ().items_ ["web_users"];
  ObjectIter iUser = FindObject (webUsers, "name", w.user_.gets ("name"));
  if (iUser == webUsers.end ())
    throw runtime_error ("User not found");
  
  iUser->sets ("token", "no_token");   // give them an impossible token
  // save change so we remember their token
  WriteTopThing (CONTROL_FILE, "control", control ());
  
  w.verified_ = false;   // they have NOT verified themselves
  w.user_.clear ();       // clear their details
   
  w << "<p>You have logged out.</p>\n";
  w << "<a href=\"login.php\">Log In</a><p>\n";
  
  } // end of WebLogout

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

  cout << "Loading web handlers in " __FILE__ << endl;
    
  WEBPAGE (WebWhoList);       // who is on
  WEBPAGE (WebRoomList);      // list of rooms
  WEBPAGE (WebControlList);   // control data
  WEBPAGE (WebSpellList);     // list of spells
  WEBPAGE (WebSend);          // send a message
  WEBPAGE (WebShowDtd);       // show DTD for a field
  WEBPAGE (WebCommandList);   // show list of commands
  WEBPAGE (WebServerInfo);    // show server info
  WEBPAGE (WebLogin);         // log into server
  WEBPAGE (WebLogout);        // log out of server

  return 0;
  } // end of LoadWebMap

static int loadThemUp = LoadWebMap ();

