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

    supportware
    Copyright (C) 2001-2008 by c.holzheuer
    c.holzheuer@sourceworx.org

    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.

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


#include <ddusermanager.h>
#include <ddiourl.h>
#include <dduserid.h>
#include <ddwebconnection.h>
#include <sys/time.h>
#include <stdlib.h>
#include <string.h>

/**
	Default constructor.
*/

ddusermanager::ddusermanager()
{
}


/**
	Destructor.
*/

ddusermanager::~ddusermanager()
{
}


/**
	Mit Hilfe der Konfigurationsdatei die Userdatenbank laden.
*/

void ddusermanager::init_data( ddconfig& config )
{
	// setup usermanager
  int idx = config.index_of( "users" );
  if( idx<0 ) 
		throw runtime_error( "usermanager config missing!" );
	
	 _mytable.init_queries( config[idx], config["main"]["connectstring"] );
	
	if( _mytable.fill() <= 0 )
		throw runtime_error( "usermanager fill failed!" );
	
	// neu: selbst ist der zonc:
	for( int i=0; i<_mytable.size(); ++i )
		_mydata.add( dduserid( this, _mytable[ i ] ) );
	
	init_index();
	
	// weg mit dem mist
	_mytable.clear();
	
	// die tabelle der der neuen postings initialiseren
	idx = config.index_of( "newpostings" );
	_newpostings.init_queries( config[idx], config["main"]["connectstring"] );
	
	//dump();
	
}


/**
	Init the internal lookup index.
*/

void ddusermanager::init_index()
{
 	_mydata.get_index().clear();
  _nickindex.clear();
  int x = (int) size();
  for( int i=0; i < x; ++i )
  {
    _mydata.add_key( _mydata[i].get_nick_name().as_lower() , i );
		_nickindex[ _mydata[i].get_id() ] = _mydata[i].get_nick_name();
  }
}


/**
	Erzeugt eine die liste der Anzahl neuer Postings seit dem letzten
	Besuch eines Users.
*/

ddstringmap& ddusermanager::get_new_postings_count( dduserid& uid, const ddstring& dbname )
{

	ddstringmap& entries = _postingcount[ uid.get_id() + dbname ];
	
	// ggf. aufbauen
	if( entries.empty() );
	{
		_newpostings.fill( "select", dbname, uid.get_last_login_date() );
		for( int i = 0; i < _newpostings.size(); ++i )
		{
			int numentries = _newpostings[i][1].as_int();
			// keine nuller
			if( !numentries )
				continue;
			bool toobig =  numentries >= 100;			
			entries[ _newpostings[i][0] ] = toobig ? "~" : "(" + _newpostings[i][1] + ")";
		}
	}

	return entries;
	
}


/**
	Dem Nutzer eine Pagermessage senden.
*/

void ddusermanager::send_pager_message( dduserid& sender, const ddstring& receivernick, ddwebcgi& cgi, ddmailmanager& mm )
{
	
	ddthreadautomutex tmplock( _pagerlock );	
	
	int idx = index_of( receivernick.as_lower() );
	if( idx<0 )
		return;

	// prepare cgi data
	dduserid& receiver = _mydata[idx];
	cgi["id_sender"]   = sender.get_id();
	cgi["id_receiver"] = receiver.get_id();		
	cgi["sn_msgbody"].replace_all( "\r\n", "<br>" );
	cgi["sn_nicksender"] = sender.get_nick_name();		
	cgi["sn_nickreceiver"] = receivernick;		
	cgi.dump();
	
	// pump message
	_mytable.execute( "pagermsg_xx", cgi );
	
	// noch eine sysmessage nachschieben
	

	receiver.set_system_message( "Neue Nachrichten f&uuml;r Dich", "Hier klicken um deine Nachrichten zu lesen", "/pager/" + sender.get_id() );
	// ggf. auch ne mail hinterherschieben
	if( receiver.check_property( "xx_prop_mail_pm" ) )
	{
		mm.create_hint_mail
			( 
				receivernick,
				receiver.get_nick_mail(),
				"Neue Pager-Nachrichten für Dich",
				"mooo"
			);
	}
	sender.set_system_message( "Deine Nachricht wurde verschickt." );
	
	
}


/**
	Dem Nutzer eine Pagermessage senden.
*/

void ddusermanager::send_admin_pager_message( dduserid& sender, ddwebcgi& cgi )
{
	
	ddthreadautomutex tmplock( _pagerlock );	
	
	// prepare cgi data
	cgi["id_sender"]   = sender.get_id();
	cgi["sn_msgbody"].replace_all( "\r\n", "<br>" );
	cgi["sn_nicksender"] = sender.get_nick_name();
	
	// pump message
	_mytable.execute( "pagermsg_admin", cgi );
	
}



/**
	Bastelt einen Sessionkey zusammen.
*/

ddstring ddusermanager::create_session_key()
{
	ddstring key = ddstrutil::to_hex( ::time( 0 ), 8 );
	// create random crypt
  struct timeval tv;
  ::gettimeofday(&tv, 0);
  ::srand(tv.tv_usec);
  return key + ddstrutil::to_hex( (size_t) ::rand(), 8 );
}


void ddusermanager::create_auto_nickname( dduserid& user )
{	
	ddstring lastid;
	_mytable.execute( "last_id" ).read( lastid );
	user.set_prepared_nickname( "Benutzer" + lastid );
}

/**
	Extract login data from the sn cookie.
	NEU:
*/
     
/*     
bool ddusermanager::prepare_login_mydata( ddconnection& connection, ddstring& key, ddstring& login, ddstring& passwd )
{
	
		
	ddrequest& request = connection.get_request();
	connection.get_response().get_cookie().invalidate_mydata();  
	
	// 1. cookie vorhanden, ist es ok?
	
	if(	request.has_cookie( DDCOOKIEKEY ) )
	{
		ddstring& cookieval = request.get_cookie( DDCOOKIEKEY ).get_value();
		// login & pass extrahieren und im erfolgsfall speichern
		if( cookieval.split( DDCOOKIESEPARATOR, login, passwd ) )
		{	
			key = connection.get_host_ip() + ".";
			// ist es ein gast ?
			if( login == DDGUESTNICK )
			{
				key += passwd;
				return false;
			}
			
			key += ddstrutil::to_lower( login );
			return true; // für member 
		}
		
	}
	
	// 2. kein oder fehlerhaftes cookie, statt dem unique-login
	// müssen wir jetzt einen sessionkey verwenden
	
	key = create_session_key();
	login = DDGUESTNICK;

	// set new cookie data
	const ddstring& host = request.get_domain();
  ddwebcookie& cookie = connection.get_response().get_cookie();
	cookie.set_cookie_mydata( DDCOOKIEKEY, login + DDCOOKIESEPARATOR + key, 0, host );	

	key = connection.get_host_ip() + "." +  key;

	return false; // für gast

}
*/

/**
	NEU:
	Init a user session by cookie data. If not empty, the cookie contains the user id and passwd.
	If the cookie is empty, a guest session is created.
	Warning! this is not thread save! Use with care.
	@return a new valid session. (==userid)
	@see dduserid
*/

/*
dduserid* ddusermanager::get_session_id( ddconnection& connection )
{
	
	_usermap.dump();
	
	// Fallunterscheidung:
  //  - Falls der cookie leer ist, also der nutzer gast ist,
  //  wird die ip als session key verwendet.
  //  - Sonst wird der sessionkey aus dem login string im cookie ermittelt.
  
	ddautomutex tmplock( _loginlock, "loginlock" );
	
	dduserid* userid = 0;
  ddstring key, login, passwd;
	

	// nutzer ist member und hat ein gültiges cookie
	if( prepare_login_mydata( connection, key, login, passwd ) )
	{		
		int idx = check_login( login, passwd, true );
	  // login ok
		if( idx > -1 )
		{
			// set pointer
			userid = &_mydata[idx];
			// nur dann eintragen, wenn noch nicht vorhanden.
			if( !_usermap.contains_key( key ) )
			{
				_usermap.add_user_id( key, userid );
				// update login date
				usr_update_single( userid->get_id(), "sw_datelastlogin", "now()" );	
			}
			return userid;
		}
		
	}
	
	// login failed or user is guest -> create guest login	  
  // user already there?
  userid = _usermap.get_user_id( key );
  if( !userid )
	{
  	userid = new dduserid( this );
 		_usermap.add_user_id( key, userid );
	}	
  // finally ...  
	return userid;
	
}
*/


//----


/**
	Die Login-Daten aus dem Cookie extrahieren und dekodieren.
*/
     
bool ddusermanager::prepare_login_data( ddwebconnection& connection, ddstring& key, ddstring& login, ddstring& passwd )
{
	
	
	// FIXED: alter cookie style wurde gelöscht.
	// neu: die eMail-konformen nicks stimmmen nicht unbedingt mit den cookies überein.
	
	key =  connection.get_host_ip();	
	ddwebrequest& request = connection.get_request();
	bool result = false;
	
	// 1. new cookie style
	
	if(	request.has_cookie( "SNID" ) )
	{
		ddstring& cookieval = request.get_cookie( "SNID" ).get_value();
		result = cookieval.split( DDCOOKIESEPARATOR, login, passwd );		
		// so simpel?
		login = login.as_valid_nickname();
	}
	else
	{
		return false;
	}
	
	// 4. decoding error
	if(	!result )
		return false;
	
	// 5. clean cookie, store result
	connection.get_response().get_cookie().invalidate_data();
	key = key + "." + login.as_lower();
	
	
	return true;
	
}


/**
	Eine user-session initialisieren. Dazu werden username und password aus dem Login-Cookie
	benötigt. Ohne Cookie oder bei einer Sperre des users wird eine Gast-Session des users erzeugt.
	Warning! this is not thread save! Use with care.
	@return a new valid session. (==userid)
	@see dduserid
*/

dduserid* ddusermanager::get_session_id( ddwebconnection& connection, ddusersessionmanager& sessionmanager )
{

	// Fallunterscheidung:
  //  - Falls der cookie leer ist, also der nutzer gast ist, wird die ip als session key verwendet.
  //  - Sonst wird der sessionkey aus dem login string im cookie ermittelt.
  
	ddthreadautomutex tmplock( _loginlock );
	
	dduserid* userid = 0;
  ddstring key, login, passwd;
	
	// user is not guest -> try login

	if( prepare_login_data( connection, key, login, passwd ) )
	{		
		int idx = check_login( login, passwd );
	  // login ok
		if( idx > -1 )
		{
			// set pointer
			userid = &_mydata[idx];
			// bugfix: accessrights immer setzen
			sessionmanager.set_access_rights( userid );
			// nur dann eintragen, wenn noch nicht vorhanden.
			if( !sessionmanager.contains_key( key ) )
			{
				sessionmanager.add_user_id( key, userid );
				// das letzte logindatum sichern, für die
				// 'neue postings' liste				
				ddstring lastlogin;
				_mytable.execute( "last_login", userid->get_id() ).read( lastlogin );
				userid->set_last_login_date( lastlogin );
				
				// update login date
				usr_update_single( userid->get_id(), "sn_datelastlogin", "now()" );	
			
			}
			return userid;
		}
		else
		{
			if( passwd != "xxxxxxxxxxx" )
				log_error( "cookie login failed: " + login + ": " + passwd + " : reason: " + ddstrutil::from_int( idx ) );
		}
		
		// not found? wrong id? user is treated as guest.
	}
	
	// login failed or user is guest -> create guest login	  
  // user already there?
  userid = sessionmanager.get_user_id( key );
  if( !userid )
	{
   	userid = new dduserid( this );
 		sessionmanager.add_user_id( key, userid );
	}	
  // finally ...  
	return userid;
	
}


/**
	Von snpagemember::login aufgerufen, wird hier die Login/passwd Kombi überprüft
	und gegebenenfalss das login-cookie gesetzt.
	BUG FIX: DO _NOT_ SET the internal uid here, _ONLY_ _TRY_ to set the UID-Cookie.
	THE INTERNAL USER ID ONLY CAN BE SET BY EXTERNAL COOKIES!
*/

/*
int ddusermanager::prepare_login( ddwebconnection& connection, const ddstring& login, const ddstring& passwd )
{
	ddthreadautomutex tmplock( _loginlock );
	// wir haben nur noch gecryptete passwds, also "vorcrypten"
	int idx = check_login( login, ::crypt( passwd.c_str(), "sn" ) );
  // login failed, return reason
	if( idx < 0 )
	{
		log_error( "login failed: " + login + ": " + passwd );
		return idx;
	}

	// login ok, set new cookie data
	const ddstring& host = connection.get_request().get_domain_name();
  ddwebcookie& cookie = connection.get_response().get_cookie();
	cookie.set_cookie_mydata( "SNID", login + DDCOOKIESEPARATOR + _mydata[idx].get_passwd(), -1, host );	
	return idx;

}
*/


/**
	Tested die Login/passwd Kombi.
	@return den index des user objects oder den entsprechenden error code < 0
*/

int ddusermanager::check_login( const ddstring& nickname, const ddstring& passwd )
{
  
  if( nickname.empty() )
    return ddusermanager::usr_nickname_invalid;
  
	int i = index_of( nickname.as_lower() );
  if( i<0 )
    return ddusermanager::usr_nickname_not_found;
 	
 	if( _mydata[i].get_passwd() != passwd )
    return ddusermanager::usr_passwd_wrong;

	return i;
}



/**
	@return a nick name for an given id
	@param id the user id
*/

const ddstring& ddusermanager::get_nick_name( const ddstring& id )
{
  return _nickindex[id];
}


/**
	Einen neuen Datensatz aus den CGI-Daten erzeugen.
*/

void ddusermanager::usr_create( ddwebcgi& data )
{
  ddthreadautomutex tmplock( _updatelock );
 	
  ddstring nick = data["sn_nickname"];
   
  if( index_of( nick.to_lower() ) >= 0 )
    return;
		
	// die anmelde-mail aus sicherheitsgründen 
	// auch speichern
	data["sn_firstmail"] = data["sn_nickmail"];
	_mytable.execute( "on_admin_insert", data );
	// re-select the new userid from the database and store
	// in the cache memory, update internal index
	_mytable.execute( "select_single", nick ).read();
	size_t idx = _mydata.add( dduserid( this, _mytable[ 0 ] ) );
	_mydata.add_key( nick, idx );
	_nickindex[ _mydata[idx].get_id() ] = nick; 
   
}


/**
	Den Nickname eines Users ändern. Darf nur root.
*/

void ddusermanager::usr_change_nickname( const ddstring& oldname, const ddstring& newname )
{
	
	// kein lock hier: das ist in usr_update_single
	
	dduserid& user = _mydata[ oldname.as_lower() ];
	// throws ...	
	usr_update_single( user.get_id(), "sn_nickname", newname, true );
	_mytable.execute( "change_nick", newname, user.get_id() );
  // auch im speicher ...
  user.set_nick_name( newname );
  init_index();
	
} 
   


/**
	Das Passwort eines Users ändern. 
*/

void ddusermanager::usr_change_passwd( dduserid& user, const ddstring& passwd )
{			


	// kein lock hier: das ist in usr_update_single
	ddstring cryptopass = ::crypt( passwd.c_str(), "sn" );
	// throws ...
	usr_update_single( user.get_id(), "sn_cryptopass", cryptopass, true );
	// also update userid in memory
	user.set_passwd( cryptopass );
	
}


/**
	Einen Datensatz im Speicher und in der Datenbank updaten.
*/

void ddusermanager::usr_update( ddwebcgi& data )
{
  
  ddthreadautomutex tmplock( _updatelock );
	data.dump();			

	
	ddstring nick = data["sn_nickname"];
		
	int i = index_of( nick.to_lower() );
  if( i<0 )
		return;	
		
	/*
	wird hier nicht mehr gesetzt
	// passwd cryted speichern, aber nur wenns neu gesetzt wurde
	if( !data["sn_passwd"].empty() )
		data["sn_cryptopass"] = ::crypt( data["sn_passwd"].c_str(), "sn" );
	*/
			
	_mytable.execute( data );    
		
	// also update userid in memory
	_mydata[i].sync( data );

}


/**
	Einen Datensatz aus dem Speicher und aus der Datenbank entfernen.
*/

void ddusermanager::usr_remove( ddwebcgi& data )
{
  
  ddthreadautomutex tmplock( _updatelock );
	
	ddstring& id = data["id"];
  if( id == "1" )
    return;
  
    // remove from db ... throws
 	_mytable.execute( "on_admin_remove", id );
    
	// remove from mem
 	int x = index_of_id( id );
 	if( x>-1 )
 	{
   	// invalidate
   	_mydata[x].set_nick_name( "(--zombie--)" );
   	init_index();
 	}

	
}


/**
	Einen User-Datensatz blocken, indem Password und Nickmail geändert 
	werden.
*/

void ddusermanager::usr_block( ddwebcgi& data )
{
  
  ddthreadautomutex tmplock( _updatelock );
  
  
  ddstring& id = data["id"];
  if( id == "1" )
    return;
 	
 	int x = index_of_id( id );
 	if( x<0 )
 		return;
	
	// userdaten im memory verstümmeln
	ddstring mail = _mydata[x].get_nick_mail() + "__";
	_mydata[x].set_nick_mail( mail );
	_mydata[x].set_passwd( data["sn_passwd"] );    	
	init_index();
	
	//
	// FIX! die live session auch verstümmeln
	// klappt nicht!
	//
	
	data[DDACTIONKEY] = "adminblock"; 
	data["sn_nickmail"] = mail; 
	_mytable.execute( data );

}


/**
	Das Punktekonto eines Users erhöhen.
	@param receiverid Der Punkte-Empfänger
	@param point Die Anzahl Punkte
*/

void ddusermanager::usr_inc_score( const ddstring& receiverid, size_t points )
{
  ddthreadautomutex tmplock( _updatelock );
	// die Punkte beim user speichern ...
  _mytable.execute( "score_inc1", ddstrutil::from_size_t( points ), receiverid );
  // ... und nochmal mit Datum 
	_mytable.execute( "score_inc2", receiverid, ddstrutil::from_size_t( points ) );
}


/**
	Ein einzelnes Datenfeld in der user-db updaten.
*/

void  ddusermanager::usr_update_single( const ddstring& id, const ddstring& field, const ddstring& value, bool is_string )
{
  ddthreadautomutex tmplock( _updatelock );
	
	if( id == "1" )
    return;
	
	ddstring querystr = is_string ? "update_single2" : "update_single1";
  _mytable.execute( querystr, field, value, id );
	
}

/**
	Löscht PMs, die älter als 90 Tage sind.
*/

void ddusermanager::clear_pager()
{ 	
	_mytable.execute( "pagerclear" );
}




/**
	Testet, ob ein nickname gültig ist.
	- alle zeichen erlaubt ?
	- schon belegt ?
*/

bool ddusermanager::check_nickname_valid( const ddstring& src )
{
	
	ddthreadautomutex tmplock( _updatelock );
	
	static const char* allow = "._-";
	
	if( src.empty() || src.size()<DDNICKMINLEN || src.size()>DDNICKMAXLEN  )
		return false;	
	
	for( size_t i=0; i<src.size(); ++i )
	{
		if( !isalnum( src[i] ) && !::strchr( allow, src[i] ) )
			return false;
	}
	
	if( src.contains( DDCOOKIESEPARATOR ) )
		return false;
	
	if( src != src.as_cleaned_text() )
		return false;
	
	return true;

}



