<?php
/**
 * Authenticate and realize logon of sytem user.
 *
 * @author Camilo Carromeu <camilo@carromeu.com>
 * @category class
 * @package core
 * @subpackage security
 * @copyright 2005-2017 Titan Framework
 * @license http://www.titanframework.com/license/ BSD License (3 Clause)
 * @see Security, UserType, Group, AjaxLogon, AjaxPasswd, Ldap
 */
class User
{
	static private $user = FALSE;

	private $array = array ();

	private $hash = '';

	private $lastAccess	= 0;

	private $systems = array ();

	private $permissions = array ();

	private $chatRooms = array ();

	private $admin = FALSE;

	private $deleted = TRUE;

	private $active = FALSE;

	private $type = NULL;

	private $registered = array ();

	private $alerts = TRUE;

	private final function __construct ()
	{
		$this->array = array (
			'id' 		 => NULL,
			'name' 		 => '',
			'login' 	 => '',
			'email'		 => '',
			'createDate' => '',
			'updateDate' => '',
			'lastLogon'	 => ''
		);
	}

	static public function singleton ()
	{
		if (self::$user !== FALSE)
			return self::$user;

		$class = __CLASS__;

		if (isset ($_SESSION ['user']))
			self::$user =& $_SESSION ['user'];
		else
			self::$user = new $class ();

		return self::$user;
	}

	public function __set ($field, $value)
	{
		if (array_key_exists ($field, $this->array))
			$this->array [$field] = $value;
	}

	public function __get ($field)
	{
		if (array_key_exists ($field, $this->array))
			return $this->array [$field];

		return NULL;
	}

	public function loadById ($id)
	{
		$sql = "SELECT *,
				to_char(_create_date, 'HH24-MI-SS-MM-DD-YYYY') AS _create_date,
				to_char(_update_date, 'HH24-MI-SS-MM-DD-YYYY') AS _update_date,
				to_char(_last_logon, 'HH24-MI-SS-MM-DD-YYYY') AS _last_logon
				FROM _user
				WHERE _id = :id";

		$sth = Database::singleton ()->prepare ($sql);

		$sth->bindParam (':id', $id, PDO::PARAM_INT);

		$sth->execute ();

		$obj = $sth->fetch (PDO::FETCH_OBJ);

		$this->id 	 	= $obj->_id;
		$this->name  	= $obj->_name;
		$this->login 	= $obj->_login;
		$this->email 	= $obj->_email;
		$this->active 	= (int) $obj->_active  ? TRUE : FALSE;
		$this->deleted 	= (int) $obj->_deleted ? TRUE : FALSE;
		$this->type		= Security::singleton ()->getUserType ($obj->_type);

		if (isset ($obj->_language))
			Localization::singleton ()->setLanguage ($obj->_language);

		if (isset ($obj->_alert))
			$this->alerts = (int) $obj->_alert ? TRUE : FALSE;

		if (isset ($obj->_timezone) && trim ($obj->_timezone) != '')
			Instance::singleton ()->setTimeZone ($obj->_timezone);

		$cd = explode ('-', $obj->_create_date);
		$ud = explode ('-', $obj->_update_date);
		$ll = explode ('-', $obj->_last_logon);

		$this->createDate  	= strftime ('%c', mktime ((int) $cd [0], (int) $cd [1], (int) $cd [2], (int) $cd [3], (int) $cd [4], (int) $cd [5]));
		$this->updateDate 	= strftime ('%c', mktime ((int) $ud [0], (int) $ud [1], (int) $ud [2], (int) $ud [3], (int) $ud [4], (int) $ud [5]));
		$this->lastLogon 	= strftime ('%c', mktime ((int) $ll [0], (int) $ll [1], (int) $ll [2], (int) $ll [3], (int) $ll [4], (int) $ll [5]));

		$this->lastAccess = time ();

		$this->hash	= sha1 ($this->login . Security::singleton ()->getHash () . $this->lastAccess);

		$this->setGroups ();

		return TRUE;
	}

	public function authenticate ($login, $password)
	{
		$db = Database::singleton ();

		$validate = array ("'", '"', '\\', '--', '/*', '*/');
		$validLogin = str_replace ($validate, '', $login);
		$validPassword = str_replace ($validate, '', $password);

		if ($login !== $validLogin || $password !== $validPassword)
			throw new Exception (__ ('Incorrect User or Password!'));

		if (Security::singleton ()->encryptOnClient () && strlen ($password) != 40)
			throw new Exception (__ ('Incorrect User or Password!'));

		$sql = "SELECT *,
				to_char(_create_date, 'HH24-MI-SS-MM-DD-YYYY') AS _create_date,
				to_char(_update_date, 'HH24-MI-SS-MM-DD-YYYY') AS _update_date,
				to_char(_last_logon, 'HH24-MI-SS-MM-DD-YYYY') AS _last_logon
				FROM _user
				WHERE _login = :login";

		$sth = $db->prepare ($sql);

		$sth->bindParam (':login', $login, PDO::PARAM_STR);

		$sth->execute ();

		$obj = $sth->fetch (PDO::FETCH_OBJ);

		if ($obj)
		{
			$type = Security::singleton ()->getUserType ($obj->_type);

			if (!is_object ($type))
				throw new Exception (__ ('User type not exists! Contact administrator.'));

			if ($type->useLdap ())
			{
				$ldap = $type->getLdap ();

				if (!$ldap->connect ($login, $password))
					throw new Exception (__ ('Incorrect User or Password!'));

				$ldap->close ();
			}
			elseif ((Security::singleton ()->encryptOnClient () && $password !== $obj->_password) || (!Security::singleton ()->encryptOnClient () && sha1 ($password) !== $obj->_password))
				throw new Exception (__ ('Incorrect User or Password!'));
		}
		else
		{
			while ($type = Security::singleton ()->getUserType ())
			{
				if (!$type->useLdap ())
					continue;

				$ldap = $type->getLdap ();

				if (!$ldap->connect ($login, $password))
				{
					$ldap->close ();

					continue;
				}

				$search = array ('displayname', 'cn', 'givenname', 'mail');

				$array = $ldap->load ($login, $search);

				$ldap->close ();

				$nameValidate = array ('cn', 'displayname', 'givenname');

				$name = $login;

				foreach ($nameValidate as $trash => $value)
				{
					if (!array_key_exists ($value, $array) || trim ($array [$value]) == '')
						continue;

					$name = $array [$value];

					break;
				}

				$userId = Database::nextId ('_user', '_id');

				$fields = array (
					'_id' 	 	 => array ($userId, PDO::PARAM_INT),
					'_login' 	 => array ($login, PDO::PARAM_STR),
					'_name'	 => array ($name, PDO::PARAM_STR),
					'_email'	 => array (trim (@$array ['mail']), PDO::PARAM_STR),
					'_password' => array (randomHash (13) .'_INVALID_HASH_'. randomHash (13), PDO::PARAM_STR),
					'_active'	 => array (1, PDO::PARAM_INT),
					'_deleted'	 => array (0, PDO::PARAM_INT),
					'_type'	 => array ($type->getName (), PDO::PARAM_STR)
				);

				$sql = "INSERT INTO _user (". implode (", ", array_keys ($fields)) .") VALUES (:". implode (", :", array_keys ($fields)) .")";

				$sth = $db->prepare ($sql);

				foreach ($fields as $key => $aux)
					$sth->bindParam (':'. $key, $aux [0], $aux [1]);

				$sth->execute ();

				try
				{
					$sql = "SELECT _group FROM _type_group WHERE _type = :type";

					$sth = $db->prepare ($sql);

					$sth->bindParam (':type', $type->getName (), PDO::PARAM_STR);

					$sth->execute ();

					$sthUser = $db->prepare ("INSERT INTO _user_group (_user, _group) VALUES (:user, :group)");

					while ($obj = $sth->fetch (PDO::FETCH_OBJ))
						$sthUser->execute (array (':user' => $userId, ':group' => $obj->_group));
				}
				catch (PDOException $e)
				{
					$message->addWarning (__('Unable to bind initial groups to the user. You should manually set the groups of the new user. [ [1] ]', $e->getMessage ()));
				}

				$sql = "SELECT *,
						to_char(_create_date, 'HH24-MI-SS-MM-DD-YYYY') AS _create_date,
						to_char(_update_date, 'HH24-MI-SS-MM-DD-YYYY') AS _update_date,
						to_char(_last_logon, 'HH24-MI-SS-MM-DD-YYYY') AS _last_logon
						FROM _user
						WHERE _login = :login";

				$sth = $db->prepare ($sql);

				$sth->bindParam (':login', $login, PDO::PARAM_STR);

				$sth->execute ();

				$obj = $sth->fetch (PDO::FETCH_OBJ);

				break;
			}

			if (!$obj)
				throw new Exception (__ ('Incorrect User or Password!'));
		}

		$this->id 	 	= $obj->_id;
		$this->name  	= $obj->_name;
		$this->login 	= $obj->_login;
		$this->email 	= $obj->_email;
		$this->active 	= (int) $obj->_active  ? TRUE : FALSE;
		$this->deleted 	= (int) $obj->_deleted ? TRUE : FALSE;
		$this->type		= $type;

		if (isset ($obj->_language))
			Localization::singleton ()->setLanguage ($obj->_language);

		if (isset ($obj->_alert))
			$this->alerts = (int) $obj->_alert ? TRUE : FALSE;

		if (isset ($obj->_timezone) && trim ($obj->_timezone) != '')
			Instance::singleton ()->setTimeZone ($obj->_timezone);

		$cd = explode ('-', $obj->_create_date);
		$ud = explode ('-', $obj->_update_date);
		$ll = explode ('-', $obj->_last_logon);

		$this->createDate  	= strftime ('%c', mktime ((int) $cd [0], (int) $cd [1], (int) $cd [2], (int) $cd [3], (int) $cd [4], (int) $cd [5]));
		$this->updateDate 	= strftime ('%c', mktime ((int) $ud [0], (int) $ud [1], (int) $ud [2], (int) $ud [3], (int) $ud [4], (int) $ud [5]));
		$this->lastLogon 	= strftime ('%c', mktime ((int) $ll [0], (int) $ll [1], (int) $ll [2], (int) $ll [3], (int) $ll [4], (int) $ll [5]));

		if (!$this->isActive ())
			throw new Exception (__ ('This user is inactive into the system!'));

		try
		{
			$db->beginTransaction ();

			$db->exec ("ALTER TABLE _user DISABLE TRIGGER USER");

			$db->exec ("UPDATE _user SET _last_logon = NOW() WHERE _id = '". $obj->_id ."'");

			$db->exec ("ALTER TABLE _user ENABLE TRIGGER USER");

			$db->commit ();
		}
		catch (PDOException $e)
		{
			$db->rollBack ();

			toLog ('Impossible to change information about logon time in _user table. ['. $e->getMessage () .']');
		}

		$this->lastAccess = time ();

		$this->hash	= sha1 ($this->login . Security::singleton ()->getHash () . $this->lastAccess);

		$this->setGroups ();

		$_SESSION ['user'] =& $this;

		return TRUE;
	}

	public function authenticateBySocialNetwork ($driver, $id, $idType = PDO::PARAM_STR)
	{
		if (!Social::isActive () || !Social::singleton ()->socialNetworkExists ($driver))
			return FALSE;

		$db = Database::singleton ();

		$driver = Social::singleton ()->getSocialNetwork ($driver);

		$column = $driver->getIdColumn ();

		$sql = "SELECT *,
				to_char(_create_date, 'HH24-MI-SS-MM-DD-YYYY') AS _create_date,
				to_char(_update_date, 'HH24-MI-SS-MM-DD-YYYY') AS _update_date,
				to_char(_last_logon, 'HH24-MI-SS-MM-DD-YYYY') AS _last_logon
				FROM _user
				WHERE ". $column ." = :id";

		$sth = $db->prepare ($sql);

		$sth->bindParam (':id', $id, $idType);

		$sth->execute ();

		$obj = $sth->fetch (PDO::FETCH_OBJ);

		if (!$obj)
			throw new Exception (__ ('There is no user in the system linked to this social network profile!'));

		$this->id 	 	= $obj->_id;
		$this->name  	= $obj->_name;
		$this->login 	= $obj->_login;
		$this->email 	= $obj->_email;
		$this->active 	= (int) $obj->_active  ? TRUE : FALSE;
		$this->deleted 	= (int) $obj->_deleted ? TRUE : FALSE;
		$this->type		= Security::singleton ()->getUserType ($obj->_type);

		if (isset ($obj->_language))
			Localization::singleton ()->setLanguage ($obj->_language);

		if (isset ($obj->_alert))
			$this->alerts = (int) $obj->_alert ? TRUE : FALSE;

		if (isset ($obj->_timezone) && trim ($obj->_timezone) != '')
			Instance::singleton ()->setTimeZone ($obj->_timezone);

		$cd = explode ('-', $obj->_create_date);
		$ud = explode ('-', $obj->_update_date);
		$ll = explode ('-', $obj->_last_logon);

		$this->createDate  	= strftime ('%c', mktime ((int) $cd [0], (int) $cd [1], (int) $cd [2], (int) $cd [3], (int) $cd [4], (int) $cd [5]));
		$this->updateDate 	= strftime ('%c', mktime ((int) $ud [0], (int) $ud [1], (int) $ud [2], (int) $ud [3], (int) $ud [4], (int) $ud [5]));
		$this->lastLogon 	= strftime ('%c', mktime ((int) $ll [0], (int) $ll [1], (int) $ll [2], (int) $ll [3], (int) $ll [4], (int) $ll [5]));

		if (!$this->isActive ())
			throw new Exception (__ ('This user is inactive into the system!'));

		try
		{
			$db->beginTransaction ();

			$db->exec ("ALTER TABLE _user DISABLE TRIGGER USER");

			$db->exec ("UPDATE _user SET _last_logon = NOW() WHERE _id = '". $obj->_id ."'");

			$db->exec ("ALTER TABLE _user ENABLE TRIGGER USER");

			$db->commit ();
		}
		catch (PDOException $e)
		{
			$db->rollBack ();

			toLog ('Impossible to change information about logon time in _user table. ['. $e->getMessage () .']');
		}

		$this->lastAccess = time ();

		$this->hash	= sha1 ($this->login . Security::singleton ()->getHash () . $this->lastAccess);

		$this->setGroups ();

		$_SESSION ['user'] =& $this;

		return TRUE;
	}

	public function update ()
	{
		$db = Database::singleton ();

		$sql = "SELECT *,
				to_char(_update_date, 'HH24-MI-SS-MM-DD-YYYY') AS _update_date
				FROM _user
				WHERE _id = '". $this->getId () ."'";

		$sth = $db->prepare ($sql);

		$sth->execute ();

		$obj = $sth->fetch (PDO::FETCH_OBJ);

		if (!$obj)
			throw new Exception (__('Fail in the recover the updated data!'));

		$this->name  	= $obj->_name;
		$this->login 	= $obj->_login;
		$this->email 	= $obj->_email;

		if (isset ($obj->_language))
			Localization::singleton ()->setLanguage ($obj->_language);

		if (isset ($obj->_timezone) && trim ($obj->_timezone) != '')
			Instance::singleton ()->setTimeZone ($obj->_timezone);

		$ud = explode ('-', $obj->_update_date);

		$this->updateDate 	= strftime ('%c', mktime ((int) $ud [0], (int) $ud [1], (int) $ud [2], (int) $ud [3], (int) $ud [4], (int) $ud [5]));

		return TRUE;
	}

	public function isActive ()
	{
		return $this->active && !$this->deleted;
	}

	public function setGroups ()
	{
		$db = Database::singleton ();

		$this->permissions = array ();

		$sth = $db->prepare ("	SELECT _group.*, _permission._name AS _permission
								FROM _permission
								LEFT JOIN _group ON _group._id = _permission._group
								LEFT JOIN _user_group ON _user_group._group = _group._id
								WHERE _user_group._user = '". $this->id ."'");

		$sth->execute ();

		$this->chatRooms = array ();

		while ($obj = $sth->fetch (PDO::FETCH_OBJ))
		{
			$this->permissions [$obj->_permission] = $obj->_permission;
			$this->systems [$obj->_id] = $obj->_name;

			if ((int) $obj->_admin) $this->admin = TRUE;

			if ((int) $obj->_chat)
				$this->chatRooms [] = $obj->_name;
		}

		if (!sizeof ($this->systems))
		{
			$sth = $db->prepare ("	SELECT _group.*
									FROM _user_group
									LEFT JOIN _group ON _user_group._group = _group._id
									WHERE _user_group._user = '". $this->id ."'");

			$sth->execute ();

			while ($obj = $sth->fetch (PDO::FETCH_OBJ))
			{
				$this->systems [$obj->_id] = $obj->_name;

				if ((int) $obj->_admin) $this->admin = TRUE;

				if ((int) $obj->_chat)
					$this->chatRooms [] = $obj->_name;
			}
		}

		return TRUE;
	}

	public function getGroups ()
	{
		return $this->systems;
	}

	public function getChatRooms ()
	{
		return $this->chatRooms ;
	}

	public function getType ()
	{
		return $this->type;
	}

	public function isLogged ()
	{
		$security = Security::singleton ();

		if (is_null ($this->getId ()) ||
			$this->hash != sha1 ($this->login . $security->getHash () . $this->lastAccess) ||
			($this->lastAccess + $security->getTimeout ()) < time ())
			return FALSE;

		$this->lastAccess = time ();

		$this->hash	= sha1 ($this->login . $security->getHash () . $this->lastAccess);

		return TRUE;
	}

	public function changeLanguage ($language)
	{
		try
		{
			Database::singleton ()->exec ("UPDATE _user SET _language = '". $language ."' WHERE _id = '". $this->getId () ."'");
		}
		catch (PDOException $e)
		{
			return FALSE;
		}

		return TRUE;
	}

	public function getName ()
	{
		return $this->name;
	}

	public function getLogin ()
	{
		return $this->login;
	}

	public function getId ()
	{
		return $this->id;
	}

	public function getEmail ()
	{
		return $this->email;
	}

	public function alertsEnabled ()
	{
		return $this->alerts;
	}

	public function getCreateDate ()
	{
		return $this->createDate;
	}

	public function getUpdateDate ()
	{
		return $this->updateDate;
	}

	public function getLastLogon ()
	{
		return $this->lastLogon;
	}

	public function isAdmin ()
	{
		return (bool) $this->admin;
	}

	public function addPermission ($permission, $forSection = FALSE)
	{
		if ($forSection === FALSE)
			$forSection = Business::singleton ()->getSection (Section::TCURRENT)->getName ();

		$permission = 'PERMISSION_'. $forSection .'_'. $permission;

		$this->permissions [$permission] = $permission;

		self::$user->permissions [$permission] = $permission;

		$_SESSION ['user']->permissions [$permission] = $permission;
	}

	public function hasPermission ($permission, $forSection = FALSE)
	{
		if ($forSection === FALSE)
			$forSection = Business::singleton ()->getSection (Section::TCURRENT)->getName ();

		return isset ($this->permissions ['PERMISSION_'. $forSection .'_'. $permission]);
	}

	public function accessSection ($section)
	{
		if (isset ($this->permissions ['ACCESS_SECTION_'. $section]))
			return TRUE;

		if ($this->isAdmin () && Business::singleton ()->sectionExists ($section) && Business::singleton ()->getSection ($section)->adminAccessible ())
			return TRUE;

		return FALSE;
	}

	public function accessAction ($action, $section)
	{
		if (isset ($this->permissions ['ACCESS_ACTION_'. $section .'_'. $action]))
			return TRUE;

		if ($this->isAdmin () && Business::singleton ()->getSection ($section)->adminAccessible ())
			return TRUE;

		return FALSE;
	}

	public function accessData ($id, $primary, $table, $column = '_user')
	{
		$sql = "SELECT COUNT(*) FROM ". $table ." WHERE ". $primary ." = '". $id ."' AND ". $column ." = '". $this->getId () ."'";

		$query = Database::singleton ()->query ($sql);

		if ((int) $query->fetchColumn ())
			return TRUE;

		return FALSE;
	}

	public function register ($table, $column, $primary, $value = NULL)
	{
		if (is_null ($value))
			$this->registered [$table][$column][$primary] = TRUE;
		elseif (!isset ($this->registered [$table][$column][$primary]) || is_array ($this->registered [$table][$column][$primary]))
			$this->registered [$table][$column][$primary][$value] = TRUE;
	}

	public function unregister ($table, $column, $primary, $value = NULL)
	{
		if (!isset ($this->registered [$table][$column][$primary]))
			return TRUE;
		elseif (is_null ($value) || !is_array ($this->registered [$table][$column][$primary]))
			unset ($this->registered [$table][$column][$primary]);
		else
			unset ($this->registered [$table][$column][$primary][$value]);
	}

	public function isRegistered ($table, $column, $primary, $value = NULL)
	{
		if (isset ($this->registered [$table][$column][$primary]) && !is_array ($this->registered [$table][$column][$primary]))
			return TRUE;

		if (is_null ($value))
			return isset ($this->registered [$table][$column][$primary]) && !is_array ($this->registered [$table][$column][$primary]);

		return isset ($this->registered [$table][$column][$primary][$value]);
	}
}