<?php 
/**
* @version		$Id: session.class.php 2382 2020-12-19 09:42:05Z IOS $
* @package		Elxis
* @subpackage	Session
* @copyright	Copyright (c) 2006-2023 elxis.org (https://www.elxis.org). All rights reserved.
* @license		Elxis Public License ( https://www.elxis.org/elxis-public-license.html )
* @author		Elxis Team ( https://www.elxis.org )
* @description 	Elxis CMS is free software. Read the license for copyright notices and details
*/

defined('_ELXIS_') or die ('Direct access to this location is not allowed');


class elxisSession {

	private $session_name = 'PHPSESSID';
	private $session_id = '';
	private $crypt = null; //elxis crypt object (if encryption is enabled)
	private $user_agent = null;
	private $ip_address = null;
	private $session_handler = 'none';
	private $handler = null; //session handler object
	private $expire = 900; //Maximum age of unused session in seconds
	private $matchip = 0;
	private $matchbrowser = 1;
	private $matchreferer = 0;
	private $encrypt = 0;
	private $security_level = 0;
	private $now = 0; //current timestamp
	private $save_path = '';
	private $cookie_domain = 'localhost';
	private $cookie_path = '/';
	private $cookie_secure = false;
	private $cookie_httponly = true;
	private $cookie_samesite = 'Lax';
	private $state = 'active'; //internal state: one of 'active'|'expired'|'destroyed|'error'


	/*********************/
	/* MAGIC CONSTRUCTOR */
	/*********************/
	public function __construct($options = array()) {
		$elxis = eFactory::getElxis();
		$this->session_handler = $elxis->getConfig('SESSION_HANDLER');
		$this->expire = (int)$elxis->getConfig('SESSION_LIFETIME');
		$this->matchip = (int)$elxis->getConfig('SESSION_MATCHIP');
		$this->matchbrowser = (int)$elxis->getConfig('SESSION_MATCHBROWSER');
		$this->matchreferer = (int)$elxis->getConfig('SESSION_MATCHREFERER');
		$this->encrypt = (int)$elxis->getConfig('SESSION_ENCRYPT');
		$this->security_level = (int)$elxis->getConfig('SECURITY_LEVEL');
		$this->now = eFactory::getDate()->getTS();
		$parsed = parse_url($elxis->getConfig('URL').'/');
		if (defined('ELXIS_ADMIN') && ($elxis->getConfig('SSL') >= 1)) {
			$this->cookie_secure = true;
		} else {
			$this->cookie_secure = (strtolower($parsed['scheme']) == 'https') ? true : false;
		}
		$this->cookie_domain = $parsed['host'];
		$this->cookie_path = (isset($parsed['path']) && ($parsed['path'] != '')) ? $parsed['path'] : '/';
		unset($parsed);
		$this->cookie_httponly = true;//Always set HTTPOnly flag on cookies

		//Overwrite cookie options is allowed since 19.08.2020
		if (isset($options['cookie_domain'])) { $this->cookie_domain = $options['cookie_domain']; }
		if (isset($options['cookie_path'])) { $this->cookie_path = $options['cookie_path']; }
		if (isset($options['cookie_secure'])) { $this->cookie_secure = $options['cookie_secure']; }
		if (isset($options['cookie_httponly'])) { $this->cookie_httponly = $options['cookie_httponly']; }
		if (isset($options['cookie_samesite'])) { $this->cookie_samesite = $options['cookie_samesite']; }

		if ($this->security_level > 0) {
			if ($this->security_level > 1) {
				$this->matchbrowser = 1;
				$this->encrypt = 1;
			}
		}

		$repo_path = $elxis->getConfig('REPO_PATH');
		if ($repo_path == '') { $repo_path = ELXIS_PATH.'/repository'; }
		$this->save_path = $repo_path.'/sessions/';
		if ($this->session_handler == 'files') {
			if (!file_exists($this->save_path) || !is_dir($this->save_path)) {
				trigger_error('Session save path '.$this->save_path.' does not exist!', E_USER_ERROR);
			}
			if (!is_writable($this->save_path)) {
				trigger_error('Session save path '.$this->save_path.' is not writable!', E_USER_ERROR);
			}
		}
		if ($this->session_handler == 'none') { $this->encrypt = 0; }
		if ($this->encrypt == 1) { $this->crypt = $elxis->obj('crypt'); }

		if (isset($_SERVER['HTTP_USER_AGENT'])) {
			$this->user_agent = trim($elxis->sanitizeInput('HTTP_USER_AGENT', INPUT_SERVER));
			if ($this->user_agent != '') {
				$pat = '\-\-|\*|\/\*|\*\/|\<|\>|\`|\'|\"|\x01|\x02|\x03|\x04|\x05|\x06|\x07|\x08|\x0e|\x0f|\x10|\x11|\x12|\x13|\x14|\x15|\x16|\x17|\x18|\x19|\x1a|\x1b|\x1c|\x1d|\x1e|\x1f';
				if (preg_match('~'.$pat.'~', strtolower($this->user_agent))) {
					exitPage::make('security', 'SESS-0001', 'Bad user agent!');
				}
			}
		}
		$this->ip_address = $elxis->obj('IP')->clientIP();

		if (session_id()) {
			session_unset();
			session_destroy();
		}

		ini_set('session.save_handler', 'files');
		ini_set('session.use_trans_sid', '0');
		ini_set('session.use_cookies', '1');
		ini_set('session.use_only_cookies', '1');
		if ($this->session_handler == 'files') {
			ini_set('session.save_path', $this->save_path);
		}
		ini_set('session.gc_maxlifetime', $elxis->getConfig('SESSION_LIFETIME'));
		ini_set('session.gc_probability', 1);
		ini_set('session.gc_divisor', 100);
		ini_set('session.hash_function', 1);
		ini_set('session.hash_bits_per_character', 6);
		$mins = (int)($elxis->getConfig('SESSION_LIFETIME') / 60);
		session_cache_expire($mins);
		session_cache_limiter(false);
		if (version_compare(PHP_VERSION, '7.3.0') < 0) {
			ini_set('session.cookie_samesite', $this->cookie_samesite);
		}

		if (isset($options['encname'])) {
			$this->session_name = $options['encname'];
		} elseif (isset($options['name'])) {
			$this->session_name = $this->getHash($options['name']);
		} else {
			if (trim($this->session_name == '')) { $this->session_name = 'elxissessid'; }
		}
		session_name($this->session_name);
		if (isset($options['id'])) { session_id($options['id']); }

		$cookie_domain = ($this->cookie_domain == 'localhost') ? null : $this->cookie_domain;//fix problem with chrome rejecting cookies on localhost

		if (version_compare(PHP_VERSION, '7.3.0') >= 0) {
			$cookoptions = array(
				'lifetime' => $elxis->getConfig('SESSION_LIFETIME'),
				'path' => $this->cookie_path,
				'domain' => $cookie_domain,
				'secure' => $this->cookie_secure,
				'httponly' => $this->cookie_httponly,
				'samesite' => $this->cookie_samesite
			);
			session_set_cookie_params($cookoptions);
		} else {
			session_set_cookie_params(
				$elxis->getConfig('SESSION_LIFETIME'),
				$this->cookie_path.'; SameSite='.$this->cookie_samesite,
				$cookie_domain,
				$this->cookie_secure,
				$this->cookie_httponly
			);
		}

		$this->setSaveHandler();
		$this->start();
		$this->setTimers();
		$this->state = 'active';
		$this->validate();

		//we need to do this in order to change the session expiry time every time the user clicks a page, else the expire 
		//time set by session_set_cookie_params will not refresh even if the user clicks various pages
		//Check if you need to also put this in the refresh and other functions
		if (version_compare(PHP_VERSION, '7.3.0') >= 0) {
			$cookoptions = array (
				'expires' => time() + $this->expire,
				'path' => $this->cookie_path,
				'domain' => $this->cookie_domain,
				'secure' => $this->cookie_secure,
				'httponly' => $this->cookie_httponly,
				'samesite' => $this->cookie_samesite
			);
			setcookie(session_name(), session_id(), $cookoptions); 
		} else {
			//setcookie(session_name(), session_id(), time() + $this->expire, $this->cookie_path, $this->cookie_domain, $this->cookie_secure, $this->cookie_httponly);
			setcookie(session_name(), session_id(), time() + $this->expire, $this->cookie_path.'; samesite='.$this->cookie_samesite, $this->cookie_domain, $this->cookie_secure, $this->cookie_httponly);
		}
	}


	/*****************/
	/* GET SEED HASH */
	/*****************/
	private function getHash($seed) {
		$string = eFactory::getElxis()->getConfig('SECRET').$seed;
		return sha1($string);
	}


	/*******************/
	/* START A SESSION */
	/*******************/
	private function start() {
		if ($this->state == 'restart') {
			session_id($this->createId());
		}
		session_cache_limiter(false);
		session_start();
		header('P3P: CP="NOI ADM DEV PSAi COM NAV OUR OTRo STP IND DEM"');
		return true;
	}


	/***********************/
	/* CREATE A SESSION ID */
	/***********************/
	private function createId() {
		$id = 0;
		while (strlen($id) < 40) {
			$id .= mt_rand(0, mt_getrandmax());
		}
		return $this->getHash(uniqid($id, true));
	}


	/******************/
	/* GET SESSION ID */
	/******************/
	public function getId() {
		if ($this->state == 'destroyed') { return null; }
		return session_id();
	}
	

	/********************/
	/* GET SESSION DATA */
	/********************/
	public function get($name, $default=null, $namespace='elxis') {
		if (($this->state != 'active') && ($this->state !== 'expired')) {
			trigger_error('Tried to access session '.$name.' but session is not initialized!', E_USER_WARNING); 
			return null;
		}
		$namespace = (trim($namespace) == '') ? 'elxis' : $namespace;
		return (isset($_SESSION[$namespace][$name])) ? $_SESSION[$namespace][$name] : $default;
	}


	/********************/
	/* SET SESSION DATA */
	/********************/
	public function set($name, $value=null, $namespace='elxis') {
		if ($this->state != 'active') {
			trigger_error('Tried to set a value for session '.$name.' but session is not active!', E_USER_WARNING);
			return null;
		}
		$namespace = (trim($namespace) == '') ? 'elxis' : $namespace;
		$v = isset($_SESSION[$namespace][$name]) ?  $_SESSION[$namespace][$name] : null;
		if ($value === null) {
			if (isset($_SESSION[$namespace][$name])) { unset($_SESSION[$namespace][$name]); }
		} else {
			if (!isset($_SESSION[$namespace])) { $_SESSION[$namespace] = array(); }
			$_SESSION[$namespace][$name] = $value;
		}
		return $v;
	}


	/********************************/
	/* CHECK IF SESSION DATA EXISTS */
	/********************************/
	private function is_set($name, $namespace='elxis') {
		if ($this->state != 'active') {
			trigger_error('Tried to check if session '.$name.' exists but session is not active!', E_USER_NOTICE);
			return null;
		}
		$namespace = (trim($namespace) == '') ? 'elxis' : $namespace;
		return isset($_SESSION[$namespace][$name]);
	}


	/**********************/
	/* SET SESSION TIMERS */
	/**********************/
	private function setTimers() {
		if( !$this->is_set('session_init')) {
			$this->set('session_init', $this->now);
			$this->set('session_previous', $this->now);
			$this->set('session_now', $this->now);
		} else {
			$this->set('session_previous', $this->get('session_now', $this->now));
			$this->set('session_now', $this->now);
		}
		return true;
	}


	/***************************/
	/* UNSET DATA FROM SESSION */
	/***************************/
	private function clear($name, $namespace='elxis') {
		$namespace = (trim($namespace) == '') ? 'elxis' : $namespace;
		if (isset($_SESSION[$namespace][$name])) {
			unset($_SESSION[$namespace][$name]);
		}
		return true;
	}


	/********************/
	/* VALIDATE SESSION */
	/********************/
	private function validate($restart = false) {
		if ($restart) {
			$this->state = 'active';
			$this->set('session_ip', null);
			$this->set('session_browser', null);
		}

		if (($this->get('session_previous', 0) + $this->expire) < $this->get('session_now', 0)) {
			$this->state = 'expired';
			return false;
		}

		if ($this->matchip) {
			if ($this->ip_address !== null) {
				$ip = $this->get('session_ip');
				if ($ip === null) {
					$this->set('session_ip', $this->ip_address);
				} else if ($ip !== $this->ip_address) {
					$this->state = 'error';
					return false;
				}
			}
		}

		if ($this->matchbrowser) {
			$browser_signature = '';
			$keys = array('HTTP_USER_AGENT', 'HTTP_ACCEPT_CHARSET', 'HTTP_ACCEPT_LANGUAGE');
			foreach ($keys as $key) {
				if (isset($_SERVER[$key])) { $browser_signature .= $_SERVER[$key]; }
			}
			$browser_signature = $this->getHash($browser_signature);
			$sbsig = $this->get('session_browser');
			if ($sbsig === null) {
				$this->set('session_browser', $browser_signature);
			} else if ($sbsig !== $browser_signature) {
				$this->state = 'error';
				return false;
			}
		}
		
		if ($this->matchreferer) {
			if (isset($_SERVER['HTTP_REFERER']) && (trim($_SERVER['HTTP_REFERER']) != '')) {
				$ref = $elxis->sanitizeInput('HTTP_REFERER', INPUT_SERVER, true, false);
				if (!filter_var($ref, FILTER_VALIDATE_URL) === false) {
					exitPage::make('security', 'SESS-0002', 'Possible attack via HTTP REFERER');
				}
				$parts = parse_url($ref);
				if ($parts && isset($parts['host'])) {
					if ($parts['host'] != $this->cookie_domain) {
						$this->state = 'error';
						return false;
					}
				}
			}
		}

		return true;
	}


	/******************************/
	/* RESTART AN EXPIRED SESSION */
	/******************************/
	public function restart() {
		$this->destroy();
		if ($this->state != 'destroyed') { return false; }
		$this->state = 'restart';
		$id	= $this->createId();
		session_id($id);
		$this->setSaveHandler();
		$this->start();
		$this->state = 'active';
		$this->validate();
		return true;
	}


	/*********************/
	/* DESTROY A SESSION */
	/*********************/
	private function destroy() {
		if ($this->state == 'destroyed') { return true; }
		$sess_name = session_name();
		if ((string)$sess_name != '') {
			if (isset($_COOKIE[$sess_name])) {
				$t = $this->now - 2592000;
				$cookie_domain = ($this->cookie_domain == 'localhost') ? null : $this->cookie_domain;//fix problem with chrome rejecting cookies on localhost
				setcookie($sess_name, '', $t, $this->cookie_path, $cookie_domain, $this->cookie_secure, $this->cookie_httponly);
			}
		}
		session_unset();
		session_destroy();
		$this->state = 'destroyed';
		return true;
	}


	/**************************/
	/* REGENERATE THE SESSION */
	/**************************/
	public function regenerate() {
		if ($this->state != 'active') { return false; }
		session_regenerate_id(true);
		$this->session_id = session_id();
		return true;
	}


	/*********************/
	/* GET SESSION STATE */
	/*********************/
	public function getState() {
		return $this->state;
	}


	/**********************************/
	/* GET EXPIRATION TIME IN SECONDS */
	/**********************************/
	public function getExpire() {
		return $this->expire;
	}


	/******************/
	/* GET USER AGENT */
	/******************/
	public function getUA() {
		return $this->user_agent;
	}


	/******************/
	/* GET IP ADDRESS */
	/******************/
	public function getIP() {
		return $this->ip_address;
	}


	/*****************/
	/* GET IP DOMAIN */
	/*****************/
	public function getCookieDomain() {
		return $this->cookie_domain;
	}


	/*******************/
	/* GET COOKIE PATH */
	/*******************/
	public function getCookiePath() {
		return $this->cookie_path;
	}


	/*******************/
	/* GET COOKIE SECURE */
	/*******************/
	public function getCookieSecure() {
		return $this->cookie_secure;
	}


	/************************/
	/* GET COOKIE HTML ONLY */
	/************************/
	public function getCookieHTTP() {
		return $this->cookie_httponly;
	}


	/************************/
	/* GET COOKIE HTML ONLY */
	/************************/
	public function getCookieSameSite() {
		return $this->cookie_samesite;
	}


/*********************  SESSION STORAGE *************************/


	/****************************/
	/* SET SESSION SAVE HANDLER */
	/****************************/
	private function setSaveHandler() {
		if ($this->session_handler == 'none') { return; }
		session_set_save_handler(
			array($this, $this->session_handler.'_open'),
			array($this, $this->session_handler.'_close'),
			array($this, $this->session_handler.'_read'),
			array($this, $this->session_handler.'_write'),
			array($this, $this->session_handler.'_destroy'),
			array($this, $this->session_handler.'_gc')
		);
	}


	public function database_open($save_path, $session_name) {
		return true;
	}


	public function database_close() {
		return true;
	}

	public function database_read($id) {
		$row = new sessionDbTable();
		if ($row->load($id)) {
			$row->last_activity = $this->now;
			$row->store();
			$sess_data = ($this->encrypt == 1) ? $this->crypt->decrypt($row->session_data) : $row->session_data;
		} else {
			$row->session_id = $id;
			$row->ip_address = $this->ip_address;
			$row->user_agent = $this->user_agent;
			$row->session_data = null;
			$row->store();
			$sess_data = '';
		}
		return (string)$sess_data;

	}


	public function database_write($id, $sess_data) {
		$sess_data = ($this->encrypt == 1) ? $this->crypt->encrypt($sess_data) : $sess_data;
		$row = new sessionDbTable();
		if (!$row->load($id)) {
			$row->session_id = $id;
			$row->ip_address = $this->ip_address;
			$row->user_agent = $this->user_agent;
		}

		$row->last_activity = $this->now;
		$row->session_data = $sess_data;
		return (bool)$row->store();
	}


	public function database_destroy($id) {
		$db = eFactory::getDB();
		$sql = "DELETE FROM ".$db->quoteId('#__session')." WHERE ".$db->quoteId('session_id')." = :sessid";
		$stmt = eFactory::getDB()->prepare($sql);
		$stmt->bindParam(':sessid', $id, PDO::PARAM_STR, strlen($id));
		$ok = $stmt->execute() ? true : false;
		return $ok;
	}


	public function database_gc($maxlifetime) {
		$row = new sessionDbTable();
		$row->purge($maxlifetime);
		return true;
	}


	public function files_open($save_path, $session_name) {
		return true;
	}


	public function files_close() {
		return true;
	}


	public function files_read($id) {
		if (!file_exists($this->save_path.'sess_'.$id)) { return ''; }
		$sess_data = (string)file_get_contents($this->save_path.'sess_'.$id);
		return ($this->encrypt == 1) ? $this->crypt->decrypt($sess_data) : $sess_data;
	}


	public function files_write($id, $sess_data) {
		if ($fp = @fopen($this->save_path.'sess_'.$id, "w")) {
			$sess_data = ($this->encrypt == 1) ? $this->crypt->encrypt($sess_data) : $sess_data;
			$return = fwrite($fp, $sess_data);
			fclose($fp);
			return ($return === false) ? false : true;
		} else {
			return false;
		}
	}


	public function files_destroy($id) {
		if (!file_exists($this->save_path.'sess_'.$id)) { return true; }		
		$ok = @unlink($this->save_path.'sess_'.$id);
		return $ok;
	}


	public function files_gc($maxlifetime) {
		@clearstatcache(true);
		$files = glob($this->save_path.'sess_*');
		if ($files) {
			foreach ($files as $file) {
				if ((filemtime($file) + $maxlifetime) < $this->now) {
					@unlink($file);
				}
			}
		}
		return true;
	}


	/*****************/
	/* DISABLE CLONE */
	/*****************/
    private function  __clone() {
        trigger_error('Session cloning is not allowed', E_USER_ERROR);
    }

}

?>