/**********************************************************************
 *
 * userdb.cpp -- functions to handle a user database
 * Copyright (C) 1999  DigiLib Systems Limited, New Zealand
 *
 * A component of the Greenstone digital library software
 * from the New Zealand Digital Library Project at the
 * University of Waikato, New Zealand.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 *********************************************************************/

#include "gsdlconf.h"
#include "userdb.h"
#include "gsdltimes.h"
#include "fileutil.h"
#include <stdlib.h>


#if defined(USE_GDBM)
#include "gdbmclass.h"
#define DBCLASS gdbmclass
#define USERDBFNAME "users.gdb"
#define KEYDBFNAME "key.gdb"

#elif defined(USE_JDBM)
#include "jdbmnaiveclass.h"
#define DBCLASS jdbmnaiveclass
#define USERDBFNAME "users.jdb"
#define KEYDBFNAME "key.jdb"

#elif defined(USE_SQLITE)
#include "sqlitedbclass.h"
#define DBCLASS sqlitedbclass
#define USERDBFNAME "users.litedb"
#define KEYDBFNAME "key.litedb"

#elif defined(USE_MSSQL)
#include "mssqldbclass.h"
#define DBCLASS mssqldbclass
#define USERDBFNAME "users.msdb"
#define KEYDBFNAME "key.msdb"

#else
#error "Unable to compile Greenstone. Need at least one database backend enabled."
#endif


// include crypt
#if defined(__WIN32__)
#include "crypt.h"
#else
#if defined(HAVE_CRYPT_H)
#include <crypt.h>
#else 
#include <unistd.h>
#endif
#endif


//==========================================//
//      userinfo_t functions (Start)        //
//==========================================//
userinfo_t::userinfo_t()
{
  clear();
}

userinfo_t::~userinfo_t(){};

void userinfo_t::clear () {
  username.clear();
  password.clear();
  enabled = false;
  groups.clear();
  comment.clear();
}

userinfo_t &userinfo_t::operator=(const userinfo_t &x) {
  username = x.username;
  password = x.password;
  enabled = x.enabled;
  groups = x.groups;
  comment = x.comment;
  
  return *this;
}
//==========================================//
//       userinfo_t functions (END)         //
//==========================================//

//==========================================//
//      userdbclass functions (Start)       //
//==========================================//
userdbclass::userdbclass(const text_t &gsdlhome)
{

  storeduserdbfilename = filename_cat(gsdlhome, "etc", USERDBFNAME);

  // Create a dbclass of the correct type
  userdb = NULL;

  // Use the correct database type
  userdb = new DBCLASS(gsdlhome);

  // Check a dbclass of some type has been created
  if (userdb == NULL)
  {
    activated = false;
    return;
  }

  activated = (!userdb->opendatabase(storeduserdbfilename, DB_READER, 1000, true)) ? false : true;
  if (activated == false) 
    {
      activated = (!userdb->opendatabase(storeduserdbfilename, DB_WRITER_CREATE, 1000, true)) ? false : true;
      if (activated == true)
        {
          userdb->closedatabase();
          activated = (!userdb->opendatabase(storeduserdbfilename, DB_READER, 1000, true)) ? false : true;
        }
    }
    
  external_db = false;
}

userdbclass::~userdbclass()
{
  if (external_db == false)
  {
    userdb->closedatabase();
    delete userdb;
  }
}

// few useful functions
text_t userdbclass::crypt_text (const text_t &text) 
{
  static const char *salt = "Tp";
  text_t crypt_password;
  
  if (text.empty()) return g_EmptyText;
  
  // encrypt the password
  char *text_cstr = text.getcstr();
  if (text_cstr == NULL) return g_EmptyText;
  crypt_password = crypt(text_cstr, salt);
  delete []text_cstr;

  return crypt_password;
}

// username_ok tests to make sure a username is ok. a username
// must be at least 2 characters long, but no longer than 30
// characters long. it can contain the characters a-z A-Z 0-9
// . and _
bool userdbclass::username_ok (const text_t &username) 
{
  if (username.size() < 2 || username.size() > 30) return false;

  text_t::const_iterator here = username.begin();
  text_t::const_iterator end = username.end();
  while (here != end) 
    {
      if ((*here >= 'a' && *here <= 'z') ||
          (*here >= 'A' && *here <= 'Z') ||
          (*here >= '0' && *here <= '9') ||
          *here == '.' ||
          *here == '_') 
        {
          // ok
        } else return false;
      ++here;
    }
  
  return true;
}

// password_ok tests to make sure a password is ok. a password
// must be at least 3 characters long but no longer than 8 characters
// long. it can contain any character in the range 0x20-0x7e
bool userdbclass::password_ok (const text_t &password) 
{
  // DB: 20/02/08. Why mustn't the password exceed 8 chars, 
  // windows 3.1 complicance?  Even then this doesn't make sense to me
  // as storing this in GDBM wouldn't trigger a 8-char limit

  // Have increased this to 128.  Not because I think someone will type
  // in something that long, but some encryptions schemes (e.g. wireless
  // networks) work with such lengths and someone one day might like
  // to use such a generated password in Greenstone.
  // 

  if (password.size() < 3 || password.size() > 128) return false;

  text_t::const_iterator here = password.begin();
  text_t::const_iterator end = password.end();
  while (here != end) {
    if (*here >= 0x20 && *here <= 0x7e) 
      {
        // ok
      } else return false;
    ++here;
  }

  return true;
}

// removes spaces from user groups
text_t userdbclass::format_user_groups(const text_t user_groups)
{
  text_t new_groups = g_EmptyText;
  text_t::const_iterator here = user_groups.begin();
  text_t::const_iterator end = user_groups.end();
  while (here != end) {
    if (*here != ' '&& *here != '\t' && *here != '\n') 
      {
        new_groups.push_back(*here);
      }
    ++here;
  }
  return new_groups;
}

// functions dealing with user databases
// returns true on success (in which case userinfo will contain
// the information for this user)
// @return 0 success
// @return -1 database is not currently connected
// @return -2 not defined username
int userdbclass::get_user_info (const text_t &username, userinfo_t &userinfo) 
{
  // Let's make sure the connection has been established.
  if (activated == true)
    {
      userinfo.clear();
      infodbclass info;
      // See if we can get the user's infomration
      if (userdb->getinfo (username, info)) 
        {
          userinfo.username = info["username"];
          userinfo.password = info["password"];
          userinfo.enabled = (info["enabled"] == "true");
          userinfo.groups = info["groups"];
          userinfo.comment = info["comment"];
          return ERRNO_SUCCEED;
        }
      // If we failed to retrieve the users information, we should check if the username is admin or not.
      // If it is the admin user, let's create a new account for the admin user.
      else if (username == "admin") 
        {
          userinfo.clear();
          userinfo.username = "admin";
          userinfo.password = crypt_text("admin");
          userinfo.enabled = true;
          userinfo.groups = "administrator,all-collections-editor";
          userinfo.comment = "change the password for this account as soon as possible";
          // Return the set result.
          return set_user_info (username, userinfo);
        }
      // The username is not found, return false
      return ERRNO_USERNOTFOUND;
    }
  // Failed to connect to the database, return false.
  return ERRNO_CONNECTIONFAILED;
}

// returns true on success
int userdbclass::set_user_info (const text_t &username, const userinfo_t &userinfo) 
{
  // Let's make sure the connection has been established.
  if (activated == true)
    {
      infodbclass info;
      info["username"] = userinfo.username;
      info["password"] = userinfo.password;
      info["enabled"] = userinfo.enabled ? "true" : "false";
      info["groups"] = userinfo.groups;
      info["comment"] = userinfo.comment;
      userdb->closedatabase();
      userdb->opendatabase(storeduserdbfilename, DB_WRITER_CREATE, 1000, true);
      int result = (userdb->setinfo (username, info)) ? ERRNO_SUCCEED : ERRNO_DBACTIONFAILED;
      userdb->closedatabase();
      userdb->opendatabase(storeduserdbfilename, DB_READER, 1000, true);         
      return  result;
    }
  return ERRNO_CONNECTIONFAILED;
}

// returns true if the user's password is correct.
int userdbclass::check_passwd (const text_t &username, const text_t &password) 
{
  userinfo_t thisuser;
  int returned = get_user_info(username, thisuser);
  if(returned != ERRNO_SUCCEED) return returned;
  // couple of basic checks
  if (thisuser.username.empty() || thisuser.password.empty() ||
      password.empty()) return ERRNO_MISSINGPASSWORD;

  text_t crypt_password = crypt_text(password);
  return (thisuser.password == crypt_password) ? ERRNO_SUCCEED : ERRNO_PASSWORDMISMATCH;
}

int userdbclass::add_user (const userinfo_t &userinfo) 
{
  // Let's make sure the connection has been established.
  if (activated == true)
    {
      infodbclass info;
      if (userdb->getinfo (userinfo.username, info)) 
        {
          // There is an existing username already
          return ERRNO_EXISTINGUSERNAME;
        }
      else
        {
          return set_user_info(userinfo.username, userinfo);
        }      
    }
  return ERRNO_CONNECTIONFAILED;
}

int userdbclass::edit_user (const userinfo_t &userinfo) 
{
  // Let's make sure the connection has been established.
  if (activated == true)
    {
      infodbclass info;
      if (userdb->getinfo (userinfo.username, info)) 
        {
          return set_user_info(userinfo.username, userinfo);
        }
      else
        {
          // The user does not exist in the database.
          return ERRNO_USERNOTFOUND;
        }      
    }
  return ERRNO_CONNECTIONFAILED;
}

int userdbclass::delete_user (const text_t &username) 
{  
  // Let's make sure the connection has been established.
  if (activated == true)
    {
      userdb->closedatabase();
      userdb->opendatabase(storeduserdbfilename, DB_WRITER_CREATE, 1000, true);
      userdb->deletekey (username);
      userdb->closedatabase();
      userdb->opendatabase(storeduserdbfilename, DB_READER, 1000, true);
      return ERRNO_SUCCEED;
    }
  return ERRNO_CONNECTIONFAILED;
}

// gets all the users' information in the database. returns true
// on success
int userdbclass::get_all_users(userinfo_tarray &userinfo_array) 
{
  // Let's make sure the connection has been established.
  if (activated == true)
    {
      userinfo_array.erase(userinfo_array.begin(), userinfo_array.end());
      text_tarray userlist = userdb->getkeys();
      text_tarray::iterator user_iterator = userlist.begin();
      while (user_iterator != userlist.end())
      {
        userinfo_t one_userinfo;
        int returned = get_user_info(*user_iterator, one_userinfo);
        if (returned != ERRNO_SUCCEED) return returned;
        userinfo_array.push_back(one_userinfo);
	user_iterator++;
      }
      return ERRNO_SUCCEED;
    }
  return ERRNO_CONNECTIONFAILED;
}

// gets a list of all the users in the database. returns true
// on success
int userdbclass::get_user_list (text_tarray &userlist) 
{
  // Let's make sure the connection has been established.
  if (activated == true)
    {
      userlist.erase (userlist.begin(), userlist.end());
      userlist = userdb->getkeys();
      return ERRNO_SUCCEED;
    }
  return ERRNO_CONNECTIONFAILED;
}
//==========================================//
//       userdbclass functions (End)        //
//==========================================//

//==========================================//
//      keydbclass functions (Start)        //
//==========================================//
keydbclass::keydbclass(const text_t &gsdlhome)
{
  storedkeydbfilename = filename_cat(gsdlhome, "etc", KEYDBFNAME);

  // Create a dbclass of the correct type
  keydb = NULL;

  // Use the correct DB class type at this stage
  keydb = new DBCLASS(gsdlhome);

  // Check a dbclass of some type has been created
  if (keydb == NULL)
  {
    activated = false;
    return;
  }

  activated = keydb->opendatabase(storedkeydbfilename, DB_READER, 1000, true);
  if (activated == false) 
    {
      activated = keydb->opendatabase(storedkeydbfilename, DB_WRITER_CREATE, 1000, true);
      if (activated == true)
        {
          keydb->closedatabase();
          activated = keydb->opendatabase(storedkeydbfilename, DB_READER, 1000, true);
        }
    }
  external_db = false;
}

keydbclass::~keydbclass()
{
  if (external_db == false)
  {
    keydb->closedatabase();
    delete keydb;
  }
}

// generates a random key for the user, stores it in the database and
// returns it so that it can be used in page generation
// returns "" on failure
text_t keydbclass::generate_key (const text_t &username) 
{
  // Let's make sure the connection has been established.
  if (activated == true)
    {
      static const char *numconvert = "0123456789abcdefghijklmnopqrstuvwxyz"
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
      
      // loop looking for a suitable new key
      text_t userkey;
      text_t crypt_userkey;
      do {
        // convert to base 62 :-)
        int userkey_int = rand ();
        while (userkey_int > 0) 
          {
            userkey.push_back (numconvert[userkey_int%62]);
            userkey_int /= 62;
          }
        
        // make sure this key is not in the database
        crypt_userkey = userdbclass::crypt_text(userkey);
        if (keydb->exists (crypt_userkey)) userkey.clear();
      } while (userkey.empty());
      
      // enter the key into the database
      infodbclass keydata;
      keydata["user"] = username;
      keydata["time"] = time2text(time(NULL));
      
      keydb->closedatabase();
      keydb->opendatabase(storedkeydbfilename, DB_WRITER_CREATE, 1000, true);
      if (!keydb->setinfo (crypt_userkey, keydata)) 
        {
          userkey.clear(); // failed
        }
      keydb->closedatabase();
      keydb->opendatabase(storedkeydbfilename, DB_READER, 1000, true); 
      
      return userkey;
    }
  return "";
}

// checks to see if there is a key for this particular user in the
// database that hasn't decayed. a short decay is used when group
// is set to administrator
bool keydbclass::check_key (const userinfo_t &thisuser, const text_t &key, const text_t &group, int keydecay) 
{
  // Let's make sure the connection has been established.
  if (activated == true)
    {
      if (thisuser.username.empty() || key.empty()) return false;
      
      // the keydecay is set to 5 minute for things requiring the
      // administrator
      //  if (group == "administrator") keydecay = 300;
      
      // success if there is a key in the key database that is owned by this
      // user whose creation time is less that keydecay
      text_t crypt_key = userdbclass::crypt_text(key);
      infodbclass info;
      if (keydb->getinfo (crypt_key, info))  {
        if (info["user"] == thisuser.username) {
          time_t keycreation = text2time (info["time"]);
          if (keycreation != (time_t)-1 && difftime (text2time(time2text(time(NULL))),
                                                     keycreation) <= keydecay) {
            // succeeded, update the key's time
            info["time"] = time2text(time(NULL));
            keydb->closedatabase();
            keydb->opendatabase(storedkeydbfilename, DB_WRITER_CREATE, 1000, true);
            keydb->setinfo (crypt_key, info);
            keydb->closedatabase();
            keydb->opendatabase(storedkeydbfilename, DB_READER, 1000, true); 
            return true;
          }
        }
      }
    }
  return false;
}

// remove_old_keys will remove all keys created more than keydecay ago.
// use sparingly, it can be quite an expensive function
void keydbclass::remove_old_keys (int keydecay) 
{
  // Let's make sure the connection has been established.
  if (activated == true)
    {  
      // get a list of keys created more than keydecay seconds agon
      text_tarray oldkeys;
      infodbclass info;
      time_t timenow = text2time(time2text(time(NULL)));
      time_t keycreation = (time_t)-1;

      text_tarray keylist = keydb->getkeys();
      text_tarray::iterator key_iterator = keylist.begin();
      while (key_iterator != keylist.end())
      {
        if (keydb->getinfo (*key_iterator, info))  {
          keycreation = text2time (info["time"]);
          if (keycreation != (time_t)-1 && difftime (timenow, keycreation) > keydecay) {
            // found an old key
            oldkeys.push_back(*key_iterator);
          }
        }
        
	key_iterator++;
      }
      
      // delete the old keys
      if (oldkeys.size() > 0) {
	keydb->closedatabase();
	keydb->opendatabase(storedkeydbfilename, DB_WRITER_CREATE, 1000, true);
	text_tarray::iterator keys_here = oldkeys.begin();
	text_tarray::iterator keys_end = oldkeys.end();
	while (keys_here != keys_end) {
	  keydb->deletekey(*keys_here);
	  ++keys_here;
	}
	keydb->closedatabase();
	keydb->opendatabase(storedkeydbfilename, DB_READER, 1000, true);
      }
    }
}
//==========================================//
//       keydbclass functions (End)         //
//==========================================//
