<?php
/**
* @version $Id: session.class.php 2382 2020-12-19 09:42:05Z IOS $
* @package Elxis
* @subpackage Session
* @copyright Copyright (c) 2006-2021 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(filter_input(INPUT_SERVER, 'HTTP_USER_AGENT', FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW || FILTER_FLAG_STRIP_HIGH));
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 = filter_input(INPUT_SERVER, 'HTTP_REFERER', FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);
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);
}
}
?>