<?php

namespace Vn\Web;

use Vn\Lib\Locale;
use Vn\Lib\UserException;

const MIN   = 60;
const HOUR  = 60 * MIN;
const DAY   = 24 * HOUR;
const WEEK  = 7 * DAY;

/**
 * Thrown when user credentials could not be fetched.
 **/
class SessionExpiredException extends UserException {}

/**
 * Thrown when user credentials are invalid.
 **/
class BadLoginException extends UserException {}

/**
 * Thrown when client version is outdated.
 **/
class OutdatedVersionException extends UserException {}

/**
 * Main class for web applications.
 **/
abstract class Service
{
	protected $app;
	protected $db;
	protected $userDb = NULL;

	function __construct ($app)
	{
		$this->app = $app;
		$this->db = $app->getSysConn ();
	}

	/**
	 * Starts the user session.
	 **/
	function startSession ()
	{
		$db = $this->db;

		ini_set ('session.cookie_secure', $this->isHttps ());
		ini_set ('session.use_only_cookies', FALSE);
		ini_set ('session.hash_function', 'sha256');

		session_set_save_handler (new DbSessionHandler ($db));
		session_start ();

		// Setting the locale

		if (isset ($_SERVER['HTTP_ACCEPT_LANGUAGE']))
		if (!isset ($_SESSION['httpLanguage'])
		|| $_SESSION['httpLanguage'] != $_SERVER['HTTP_ACCEPT_LANGUAGE'])
		{
			$_SESSION['httpLanguage'] = $_SERVER['HTTP_ACCEPT_LANGUAGE'];			
			$regexp = '/([a-z]{1,4})(?:-[a-z]{1,4})?\s*(?:;\s*q\s*=\s*(?:1|0\.[0-9]+))?,?/i';

			preg_match_all ($regexp, $_SERVER['HTTP_ACCEPT_LANGUAGE'], $languages);

			foreach ($languages[1] as $lang)
			if (stream_resolve_include_path ("locale/$lang"))
			{
				$_SESSION['lang'] = $lang;
				break;
			}
		}

		if (!isset ($_SESSION['lang']))
			$_SESSION['lang'] = NULL;
		
		Locale::set ($_SESSION['lang']);

		// Registering the visit

		if (!isset ($_COOKIE['PHPSESSID'])
		|| isset ($_SESSION['access'])
		|| isset ($_SESSION['skipVisit']))
			return;

		$agent = $_SERVER['HTTP_USER_AGENT'];
		$browser = get_browser ($agent, TRUE);
		
		if (isset ($browser['crawler']) && $browser['crawler'])
		{
			$_SESSION['skipVisit'] = TRUE;
			return;
		}

		if (isset ($_SERVER['REMOTE_ADDR']))
			$ip = ip2long ($_SERVER['REMOTE_ADDR']);

		$row = $db->getRow (
			'CALL visitRegister (#, #, #, #, #, #, #, #, #)',
			[
				 nullIf ($_COOKIE, 'vnVisit')
				,nullIf ($browser, 'platform')
				,nullIf ($browser, 'browser') 
				,nullIf ($browser, 'version')
				,nullIf ($browser, 'javascript')
				,nullIf ($browser, 'cookies')
				,isset ($agent) ? $agent : NULL
				,isset ($ip) && $ip ? $ip : NULL
				,nullIf ($_SERVER, 'HTTP_REFERER')
			]
		);

		if (isset ($row['access']))
		{
			setcookie ('vnVisit', $row['visit'], time () + 31536000); // 1 Year
			$_SESSION['access'] = $row['access'];
		}
		else
			$_SESSION['skipVisit'] = TRUE;
	}
	
	/**
	 * Tries to retrieve user credentials from many sources such as POST,
	 * SESSION or COOKIES. If $_POST['remember'] is defined the user credentials 
	 * are saved on the client brownser for future logins, cookies names are
	 * 'vn_user' for the user name and 'vn_pass' for user password, the
	 * password is encoded using base64_encode() function and should be decoded
	 * using base64_decode().
	 *
	 * return Db\Conn The database connection
	 **/
	function login ()
	{
		$db = $this->db;
		$user = NULL;
		
		if (isset ($_POST['user']) && isset ($_POST['password']))
		{
			$user = strtolower ($_POST['user']);
			
			try {
				$db->query ('CALL account.userLogin (#, #)',
					[$user, $_POST['password']]);
			}
			catch (\Exception $e)
			{
				throw new BadLoginException ();
			}
		}
		else
		{
			if (isset ($_POST['token']) || isset ($_GET['token']))
			{
				if (isset ($_POST['token']))
					$token = $_POST['token'];
				if (isset ($_GET['token']))
					$token = $_GET['token'];

				$key = $db->getValue ('SELECT jwtKey FROM config');
				$jwtPayload = Jwt::decode ($token, $key);
				$expiration = $jwtPayload['exp'];

				if (empty ($expiration) || $expiration <= time())
					throw new SessionExpiredException ();

				$user = $jwtPayload['sub'];
			}
			else
				$user = $db->getValue ('SELECT guest_user FROM config');

			$db->query ('CALL account.userLoginWithName (#)', [$user]);
		}

		$_SESSION['user'] = $user;

		// Registering the user access

		if (isset ($_SESSION['access'])
		&& !isset ($_SESSION['visitUser']))
		{
			$_SESSION['visitUser'] = TRUE;
			
			$db->query (
				'CALL visitUserNew (#, #)',
				[$_SESSION['access'], session_id ()]
			);
		}	
	}

	/**
	 * Logouts the current user. Cleans the last saved used credentials.
	 **/
	function logout ()
	{
		unset ($_SESSION['visitUser']);
		unset ($_SESSION['user']);
	}
	
	/**
	 * Creates or returns a database connection where the authenticated user
	 * is the current logged user.
	 *
	 * @return {Db\Conn} The database connection
	 **/
	function getUserDb ($user)
	{
		if ($this->userDb)
			return $this->userDb;
			
		$password = $this->db->getValue (
			'SELECT password FROM account.user WHERE name = #', [$user]);
		return $this->userDb = $this->app->createConnection ($user, $password);
	}
	
	/**
	 * Generates a JWT authentication token for the specified $user.
	 *
	 * @param {string} $user The user name
	 * @param {boolean} $remember Wether to create long live token
	 * @return {string} The JWT generated token
	 **/
	function createToken ($user, $remember = FALSE)
	{
		if ($remember)
			$tokenLife = WEEK;
		else
			$tokenLife = 30 * MIN;

		$payload = [
			'sub' => $user,
			'exp' => time () + $tokenLife
		];
		$key = $this->db->getValue ('SELECT jwtKey FROM config');
		return Jwt::encode ($payload, $key);
	}

	/**
	 * Runs a method.
	 **/
	function loadMethod ($class)
	{
		$db = $this->db;
		$this->login ();

		$method = $this->app->loadMethod (
			$_REQUEST['method'], $class, './rest');
		$method->service = $this;

		if ($method::SECURITY == Security::DEFINER)
		{
			$isAuthorized = $db->getValue ('SELECT userCheckRestPriv (#)',
				[$_REQUEST['method']]);
				
			if (!$isAuthorized)
				throw new UserException ('You don\'t have enough privileges');				
			
			$methodDb = $db;
		}
		else
			$methodDb = $this->getUserDb ($_SESSION['user']);
			

		if ($method::PARAMS !== NULL && !$method->checkParams ($_REQUEST, $method::PARAMS))
			throw new UserException  (s('Missing parameters'));

		$res = $method->run ($methodDb);
		$db->query ('CALL account.userLogout ()');
		
		return $res;
	}

	/**
	 * Checks if the HTTP connection is secure.
	 *
	 * @return boolean Return %TRUE if its secure, %FALSE otherwise
	 **/
	function isHttps ()
	{
		return isset ($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on';
	}
	
	/**
	 * Obtains the application version number. It is based on de last
	 * modification date of the main script.
	 *
	 * @return string The version number
	 **/
	function getVersion ()
	{
		return (int) strftime ('%G%m%d%H%M%S',
			filectime ($_SERVER['SCRIPT_FILENAME']));
	}
	
	/**
	 * Obtains the relative path to document root from an absolute path.
	 *
	 * @return string The relative path
	 **/
	function getDir ($absoluteDir)
	{
		error_log ("Absolute: $absoluteDir");
		error_log ("Root: {$_SERVER['DOCUMENT_ROOT']}");
		error_log ("Script: {$_SERVER['SCRIPT_FILENAME']}");
		error_log ("Self: {$_SERVER['PHP_SELF']}");
	
		$rootLen = strlen ($_SERVER['DOCUMENT_ROOT']);
		return substr ($absoluteDir, $rootLen);
	}
}

?>
