mirror of
https://github.com/e107inc/e107.git
synced 2025-01-16 20:28:28 +01:00
1365 lines
33 KiB
PHP
1365 lines
33 KiB
PHP
<?php
|
|
/*
|
|
* e107 website system
|
|
*
|
|
* Copyright (C) 2008-2012 e107 Inc (e107.org)
|
|
* Released under the terms and conditions of the
|
|
* GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
|
|
*
|
|
* Session handler
|
|
*
|
|
* $URL$
|
|
* $Id$
|
|
*/
|
|
|
|
if (!defined('e107_INIT'))
|
|
{
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* @package e107
|
|
* @subpackage e107_handlers
|
|
* @version $Id$
|
|
* @author SecretR
|
|
*
|
|
* Dependencies:
|
|
* - direct: language handler
|
|
* - indirect: system preferences (required by language handler)
|
|
*
|
|
* What could break it?
|
|
* If session is started before the first system session call (see class2.php
|
|
* 'Start: Set User Language' phase), session config will not be applied!
|
|
* This could happen if included $CLASS2_INCLUDE script (see class2.php)
|
|
* calls session_start(). However, sessions will not be broken, just not secured
|
|
* as per e_SECURITY_LEVEL setting.
|
|
*
|
|
* Security levels:
|
|
* - SECURITY_LEVEL_NONE [0]: security disabled - no token checks, all session validation settings dsiabled
|
|
* - SECURITY_LEVEL_BALANCED [5]: ValidateRemoteAddr, ValidateHttpXForwardedFor are on,
|
|
* session token is created/checked, but not regenerated on every page load
|
|
* - SECURITY_LEVEL_HIGH [7]: Same as above but ValidateHttpVia, ValidateHttpUserAgent are on.
|
|
* - SECURITY_LEVEL_PARANOID [9]: Same as SECURITY_LEVEL_HIGH except session token is regenerated on
|
|
* every page load. 'httponly' is on, which means JS is unable to retrieve session cookie, this may cause
|
|
* troubles with some browsers.
|
|
* - SECURITY_LEVEL_INSANE [10]: Same as SECURITY_LEVEL_HIGH plus session id is regenerated at the end
|
|
* of every page request.
|
|
*
|
|
* Session objects are created by namespace:
|
|
* $_SESSION['e107'] is default namesapce auto created with
|
|
* <code><?php e107::getSession();</code>
|
|
* Session handler is validating corresponding session COOKIE
|
|
* (named as current session name, keeping the session id)
|
|
* on regular basis (session lifetime/4). If validation
|
|
* fails, corresponding cookie is destroyed (not the session itself).
|
|
*
|
|
* Initial system Session is started after language detection (see class2.php) to
|
|
* ensure proper session handling for sites using language sub-domains (e.g. fr.site.com)
|
|
*
|
|
* Some important system session data will be kept outside of the object for now (e.g. user validation data)
|
|
*
|
|
*/
|
|
|
|
|
|
class e_session
|
|
{
|
|
/**
|
|
* No protection, label 'Looking for trouble'
|
|
* @var integer
|
|
*/
|
|
const SECURITY_LEVEL_NONE = 0;
|
|
|
|
|
|
const SECURITY_LEVEL_LOW = 3;
|
|
/**
|
|
* Default system protection, balanced for best user experience,
|
|
* label 'Safe mode - Balanced'
|
|
* @var integer
|
|
*/
|
|
const SECURITY_LEVEL_BALANCED = 5;
|
|
|
|
/**
|
|
* Adds more system security, but there is a chance (minimal) to break stuff,
|
|
* label 'High Security'
|
|
* @var integer
|
|
*/
|
|
const SECURITY_LEVEL_HIGH = 7;
|
|
|
|
/**
|
|
* High system protection, session id is regenerated on every page request,
|
|
* label 'Paranoid'
|
|
* @var integer
|
|
*/
|
|
const SECURITY_LEVEL_PARANOID = 9;
|
|
|
|
/**
|
|
* Highest system protection, session id and token values are regenerated on every page request,
|
|
* label 'Insane'
|
|
* @var int unknown_type
|
|
*/
|
|
const SECURITY_LEVEL_INSANE = 10;
|
|
|
|
/**
|
|
* Session save path
|
|
* @var string
|
|
*/
|
|
protected $_sessionSavePath = false;
|
|
|
|
/**
|
|
* Session save method
|
|
* @var string files|db
|
|
*/
|
|
protected $_sessionSaveMethod = 'files';//'files';
|
|
|
|
/**
|
|
* Session cache limiter, ignored if empty
|
|
* php.net/manual/en/function.session-cache-limiter.php
|
|
* @var string public|private_no_expire|private|nocache
|
|
*/
|
|
protected $_sessionCacheLimiter = '';
|
|
|
|
protected $_namespace;
|
|
protected $_name;
|
|
protected static $_sessionStarted = false; // Fixes lost $_SESSION value problem.
|
|
|
|
/**
|
|
* Validation options
|
|
* @var boolean
|
|
*/
|
|
protected $_sessionValidateRemoteAddr = true;
|
|
protected $_sessionValidateHttpVia = true;
|
|
protected $_sessionValidateHttpXForwardedFor = true;
|
|
protected $_sessionValidateHttpUserAgent = true;
|
|
|
|
/**
|
|
* Skip validation
|
|
* @var array
|
|
*/
|
|
protected $_sessionValidateRemoteAddrSkip = array();
|
|
protected $_sessionValidateHttpViaSkip = array();
|
|
protected $_sessionValidateHttpXForwardedForSkip = array();
|
|
protected $_sessionValidateHttpUserAgentSkip = array();
|
|
|
|
/**
|
|
* Default session options
|
|
* @var array
|
|
*/
|
|
protected $_options = array(
|
|
'lifetime' => 3600 , // 1 hour
|
|
'path' => '',
|
|
'domain' => '',
|
|
'secure' => false,
|
|
'httponly' => true,
|
|
);
|
|
|
|
/**
|
|
* Session data
|
|
* @var array
|
|
*/
|
|
protected $_data = array();
|
|
|
|
/**
|
|
* Set session options
|
|
* @param string $key
|
|
* @param mixed $value
|
|
* @return e_session
|
|
*/
|
|
public function setOption($key, $value)
|
|
{
|
|
$this->setOptions(array($key => $value));
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function getOptions()
|
|
{
|
|
return $this->_options;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Get session option
|
|
* @param string $key
|
|
* @param mixed $default
|
|
* @return mixed value
|
|
*/
|
|
public function getOption($key, $default = null)
|
|
{
|
|
return (isset($this->_options[$key]) ? $this->_options[$key] : $default);
|
|
}
|
|
|
|
/**
|
|
* Set default settings/options based on the current security level
|
|
* NOTE: new prefs 'session_save_path', 'session_save_method', 'session_lifetime' introduced,
|
|
* still not added to preference administration
|
|
* @return e_session
|
|
*/
|
|
public function setDefaultSystemConfig()
|
|
{
|
|
if ($this->getSessionId()) return $this;
|
|
|
|
$config = array(
|
|
'ValidateRemoteAddr' => (e_SECURITY_LEVEL >= self::SECURITY_LEVEL_BALANCED),
|
|
'ValidateHttpVia' => (e_SECURITY_LEVEL >= self::SECURITY_LEVEL_HIGH),
|
|
'ValidateHttpXForwardedFor' => (e_SECURITY_LEVEL >= self::SECURITY_LEVEL_LOW),
|
|
'ValidateHttpUserAgent' => (e_SECURITY_LEVEL >= self::SECURITY_LEVEL_HIGH),
|
|
);
|
|
|
|
$options = array(
|
|
// 'httponly' => (e_SECURITY_LEVEL >= self::SECURITY_LEVEL_PARANOID),
|
|
'httponly' => true,
|
|
);
|
|
|
|
if (!defined('E107_INSTALL'))
|
|
{
|
|
$systemSaveMethod = ini_get('session.save_handler');
|
|
|
|
$saveMethod = (!empty($systemSaveMethod)) ? $systemSaveMethod : 'files';
|
|
|
|
$config['SavePath'] = e107::getPref('session_save_path', false); // FIXME - new pref
|
|
$config['SaveMethod'] = e107::getPref('session_save_method', $saveMethod);
|
|
$options['lifetime'] = (int) e107::getPref('session_lifetime', 86400);
|
|
$options['path'] = e107::getPref('session_cookie_path', ''); // FIXME - new pref
|
|
$options['secure'] = e107::getPref('ssl_enabled', false); //
|
|
|
|
e107::getDebug()->log("Session Save Method: ".$config['SaveMethod']);
|
|
|
|
if (!empty($options['secure']))
|
|
{
|
|
ini_set('session.cookie_secure', 1);
|
|
}
|
|
|
|
ini_set('session.gc_maxlifetime', $options['lifetime']);
|
|
}
|
|
|
|
if (defined('SESSION_SAVE_PATH')) // safer than a pref.
|
|
{
|
|
$config['SavePath'] = e_BASE . SESSION_SAVE_PATH;
|
|
}
|
|
|
|
$hashes = hash_algos();
|
|
|
|
// if ((e_SECURITY_LEVEL >= self::SECURITY_LEVEL_BALANCED) && in_array('sha512', $hashes))
|
|
{
|
|
|
|
// ini_set('session.hash_function', 'sha512'); Removed in PHP 7.1
|
|
// ini_set('session.hash_bits_per_character', 5); Removed in PHP 7.1
|
|
}
|
|
|
|
$this->fixSessionFileGarbageCollection();
|
|
|
|
$this->setConfig($config)
|
|
->setOptions($options);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Modify PHP ini at runtime to enable session file garbage collection
|
|
*
|
|
* Takes no action if the garbage collector is already enabled.
|
|
*
|
|
* @see https://github.com/e107inc/e107/issues/4113
|
|
* @return void
|
|
*/
|
|
private function fixSessionFileGarbageCollection()
|
|
{
|
|
$gc_probability = ini_get('session.gc_probability');
|
|
if ($gc_probability > 0) return;
|
|
|
|
ini_set('session.gc_probability', 1);
|
|
ini_set('session.gc_divisor', 100);
|
|
}
|
|
|
|
/**
|
|
* Retrieve value from current session namespace
|
|
* Equals to $_SESSION[NAMESPACE][$key]
|
|
* @param string $key
|
|
* @param boolean $clear unset key
|
|
* @return mixed
|
|
*/
|
|
public function get($key, $clear = false)
|
|
{
|
|
$ret = isset($this->_data[$key]) ? $this->_data[$key] : null;
|
|
if($clear) $this->clear($key);
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Retrieve value from current session namespace
|
|
* If key is null, returns all current session namespace data
|
|
*
|
|
* @param string|null $key
|
|
* @param boolean $clear
|
|
* @return mixed
|
|
*/
|
|
public function getData($key = null, $clear = false)
|
|
{
|
|
if(null === $key)
|
|
{
|
|
$ret = $this->_data;
|
|
if($clear) $this->clearData();
|
|
return $ret;
|
|
}
|
|
return $this->get($key, $clear);
|
|
}
|
|
|
|
/**
|
|
* Set value in current session namespace
|
|
* Equals to $_SESSION[NAMESPACE][$key] = $value
|
|
* @param string $key Also accepts multi-dimensinal format. key1/key2
|
|
* @param mixed $value
|
|
* @return e_session
|
|
*/
|
|
public function set($key, $value)
|
|
{
|
|
if(strpos($key,'/') !== false) // multi-dimensional
|
|
{
|
|
$keyArr = explode('/',$key);
|
|
$count = count($keyArr);
|
|
|
|
if($count === 2)
|
|
{
|
|
list($k1, $k2) = $keyArr;
|
|
$this->_data[$k1][$k2] = $value;
|
|
}
|
|
elseif($count === 3)
|
|
{
|
|
list($k1, $k2, $k3) = $keyArr;
|
|
$this->_data[$k1][$k2][$k3] = $value;
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
$this->_data[$key] = $value;
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set value in current session namespace
|
|
* If $key is array, the whole namespace array will be replaced with it,
|
|
* $value will be ignored
|
|
* @param string|null $key
|
|
* @param mixed $value
|
|
* @return e_session
|
|
*/
|
|
public function setData($key, $value = null)
|
|
{
|
|
if(is_array($key))
|
|
{
|
|
$this->_data = $key;
|
|
return $this;
|
|
}
|
|
return $this->set($key, $value);
|
|
}
|
|
|
|
/**
|
|
* Check if given key is set in current session namespace
|
|
* Equals to isset($_SESSION[NAMESPACE][$key])
|
|
* @param string $key
|
|
* @return boolean
|
|
*/
|
|
public function is($key)
|
|
{
|
|
return isset($this->_data[$key]);
|
|
}
|
|
|
|
/**
|
|
* Check if given key is set and not empty in current session namespace
|
|
* Equals to !empty($_SESSION[NAMESPACE][$key]) check
|
|
* @param string $key
|
|
* @return boolean
|
|
*/
|
|
public function has($key)
|
|
{
|
|
return (isset($this->_data[$key]) && $this->_data[$key]);
|
|
}
|
|
|
|
/**
|
|
* Checks if current session namespace contains any data
|
|
* Equals to !empty($_SESSION[NAMESPACE]) check
|
|
* @return boolean
|
|
*/
|
|
public function hasData()
|
|
{
|
|
return !empty($this->_data);
|
|
}
|
|
|
|
/**
|
|
* Unset member of current session namespace array
|
|
* Equals to unset($_SESSION[NAMESPACE][$key])
|
|
* @param string $key
|
|
* @return e_session
|
|
*/
|
|
public function clear($key=null)
|
|
{
|
|
if($key == null) // clear all under this namespace.
|
|
{
|
|
$this->_data = array(); // must be set to array() not unset.
|
|
return $this;
|
|
}
|
|
|
|
if(strpos($key,'/') !== false) // multi-dimensional
|
|
{
|
|
$keyArr = explode('/',$key);
|
|
$count = count($keyArr);
|
|
|
|
if($count === 2)
|
|
{
|
|
list($k1, $k2) = $keyArr;
|
|
unset($this->_data[$k1][$k2]);
|
|
}
|
|
elseif($count === 3)
|
|
{
|
|
list($k1, $k2, $k3) = $keyArr;
|
|
unset($this->_data[$k1][$k2][$k3]);
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
unset($this->_data[$key]);
|
|
}
|
|
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Reset current session namespace to empty array
|
|
* @return e_session
|
|
*/
|
|
public function clearData()
|
|
{
|
|
$this->_data = array();
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set protected class vars, prefixed with _session
|
|
* @param array $config
|
|
* @return e_session
|
|
*/
|
|
public function setConfig($config)
|
|
{
|
|
foreach ($config as $k => $v)
|
|
{
|
|
$key = '_session'.$k;
|
|
if (isset($this->$key)) $this->$key = $v;
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get registered namespace key
|
|
* @return string
|
|
*/
|
|
public function getNamespaceKey()
|
|
{
|
|
return $this->_namespace;
|
|
}
|
|
|
|
/**
|
|
* Reset session options
|
|
* @param array $options
|
|
* @return e_session
|
|
*/
|
|
public function setOptions($options)
|
|
{
|
|
if (empty($options) || !is_array($options)) return $this;
|
|
foreach ($options as $k => $v)
|
|
{
|
|
switch ($k)
|
|
{
|
|
case 'lifetime':
|
|
$v = intval($v);
|
|
break;
|
|
|
|
case 'path':
|
|
case 'domain':
|
|
$v = (string) $v;
|
|
break;
|
|
|
|
case 'secure':
|
|
case 'httponly':
|
|
$v = $v ? true : false;
|
|
break;
|
|
|
|
default:
|
|
$v = null;
|
|
break;
|
|
}
|
|
|
|
if($v !== null)
|
|
{
|
|
$this->_options[$k] = $v;
|
|
}
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @param $namespace
|
|
* @param $sessionName
|
|
* @return void
|
|
*/
|
|
public function init($namespace, $sessionName = null)
|
|
{
|
|
$this->start($sessionName);
|
|
|
|
if (!isset($_SESSION[$namespace]))
|
|
{
|
|
$_SESSION[$namespace] = array();
|
|
}
|
|
$this->_data =& $_SESSION[$namespace];
|
|
$this->_namespace = $namespace;
|
|
|
|
$this->validate();
|
|
$this->validateSessionCookie();
|
|
}
|
|
|
|
/**
|
|
* Conigure and start session
|
|
*
|
|
* @param string $sessionName optional session name
|
|
* @return e_session
|
|
*/
|
|
public function start($sessionName = null)
|
|
{
|
|
|
|
if (isset($_SESSION) && (self::$_sessionStarted === true))
|
|
{
|
|
return $this;
|
|
}
|
|
|
|
if (false !== $this->_sessionSavePath && is_writable($this->_sessionSavePath))
|
|
{
|
|
session_save_path($this->_sessionSavePath);
|
|
}
|
|
|
|
switch ($this->_sessionSaveMethod)
|
|
{
|
|
case 'db':
|
|
// ini_set('session.save_handler', 'user');
|
|
|
|
$session = new e_session_db;
|
|
session_set_save_handler(
|
|
[$session, 'open'],
|
|
[$session, 'close'],
|
|
[$session, 'read'],
|
|
[$session, 'write'],
|
|
[$session, 'destroy'],
|
|
[$session, 'gc']
|
|
);
|
|
$session->setSaveHandler();
|
|
break;
|
|
|
|
default:
|
|
if(!isset($_SESSION))
|
|
{
|
|
session_module_name($this->_sessionSaveMethod);
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (empty($this->_options['domain']))
|
|
{
|
|
// MULTILANG_SUBDOMAIN set during initial language detection in language handler
|
|
$doma = ((deftrue('e_SUBDOMAIN') || deftrue('MULTILANG_SUBDOMAIN')) && e_DOMAIN != FALSE) ? ".".e_DOMAIN : FALSE; // from v1.x
|
|
$this->_options['domain'] = $doma;
|
|
}
|
|
|
|
if (empty($this->_options['path']))
|
|
{
|
|
if(defined('e_MULTISITE_MATCH')) // multisite support.
|
|
{
|
|
$this->_options['path'] = '/';
|
|
}
|
|
else
|
|
{
|
|
$this->_options['path'] = defined('e_HTTP') ? e_HTTP : '/';
|
|
}
|
|
}
|
|
|
|
// session name before options - problems reported on php.net
|
|
if (!empty($sessionName))
|
|
{
|
|
$this->setSessionName($sessionName);
|
|
}
|
|
|
|
// set session cookie params
|
|
session_set_cookie_params($this->_options['lifetime'],
|
|
$this->_options['path'],
|
|
$this->_options['domain'],
|
|
$this->_options['secure'],
|
|
$this->_options['httponly']);
|
|
|
|
if ($this->_sessionCacheLimiter)
|
|
{
|
|
session_cache_limiter((string) $this->_sessionCacheLimiter); //XXX Remove and have e_headers class handle it?
|
|
}
|
|
|
|
|
|
session_start();
|
|
self::$_sessionStarted = true;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set session ID
|
|
* @param string $sid
|
|
* @return e_session
|
|
*/
|
|
public function setSessionId($sid = null)
|
|
{
|
|
// comma and minus allowed since 5.0
|
|
if (!empty($sid) && preg_match('#^[0-9a-zA-Z,-]+$#', $sid))
|
|
{
|
|
session_id($sid);
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Retrieve current session id
|
|
* @return string
|
|
*/
|
|
public function getSessionId()
|
|
{
|
|
return session_id();
|
|
}
|
|
|
|
/**
|
|
* Retrieve current session save method.
|
|
* @return string
|
|
*/
|
|
public function getSaveMethod()
|
|
{
|
|
return $this->_sessionSaveMethod;
|
|
}
|
|
|
|
/**
|
|
* Set new session name
|
|
* @param string $name alphanumeric characters only
|
|
* @return false old session name or false on error
|
|
*/
|
|
public function setSessionName($name)
|
|
{
|
|
if (!empty($name) && preg_match('#^[0-9a-z_]+$#i', $name))
|
|
{
|
|
$this->_name = $name;
|
|
// return session_name($name);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Retrieve current session name
|
|
* @return string
|
|
*/
|
|
public function getSessionName()
|
|
{
|
|
return session_name();
|
|
}
|
|
|
|
/**
|
|
* Reset session cookie lifetime
|
|
* We reset session cookie on every (session_lifetime / 4) seconds
|
|
* It's done by all session handler instances, they all share
|
|
* one and the same '_cookie_session_validate' variable (global session namespace)
|
|
* @return e_session
|
|
*/
|
|
public function validateSessionCookie()
|
|
{
|
|
if (!$this->_options['lifetime'])
|
|
{
|
|
return $this;
|
|
}
|
|
|
|
if (empty($_SESSION['_cookie_session_validate']))
|
|
{
|
|
$time = time() + round($this->_options['lifetime'] / 4);
|
|
$_SESSION['_cookie_session_validate'] = $time;
|
|
}
|
|
elseif ($_SESSION['_cookie_session_validate'] < time())
|
|
{
|
|
if (!headers_sent())
|
|
{
|
|
cookie(session_name(), session_id(), time() + $this->_options['lifetime'], $this->_options['path'], $this->_options['domain'], $this->_options['secure']);
|
|
$time = time() + round($this->_options['lifetime'] / 4);
|
|
$_SESSION['_cookie_session_validate'] = $time;
|
|
}
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Delete session cookie
|
|
* @return e_session
|
|
*/
|
|
public function cookieDelete()
|
|
{
|
|
cookie(session_name(), null, null, $this->_options['path'], $this->_options['domain'], $this->_options['secure']);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Validate current session
|
|
* @return e_session
|
|
*/
|
|
public function validate()
|
|
{
|
|
if (!isset($this->_data['_session_validate_data']))
|
|
{
|
|
$this->_data['_session_validate_data'] = $this->getValidateData();
|
|
}
|
|
elseif (!$this->_validate())
|
|
{
|
|
$sessionData = $this->_data['_session_validate_data'];
|
|
$validateData = $this->getValidateData();
|
|
|
|
$details = 'USER INFORMATION: '.(isset($_COOKIE[e_COOKIE]) ? $_COOKIE[e_COOKIE] : (isset($_SESSION[e_COOKIE]) ? $_SESSION[e_COOKIE] : 'n/a'))."\n";
|
|
$details .= "HOST: ".$_SERVER['HTTP_HOST']."\n";
|
|
$details .= "REQUEST_URI: ".$_SERVER['REQUEST_URI']."\n";
|
|
$details .= "SESSION OPTIONS: ".print_r($this->_options, true)."\n";
|
|
$details .= "SESSION NAMESPACE: ".$this->_namespace."\n";
|
|
$details .= "SESSION VALIDATION DATA SAVED: ".print_r($sessionData, true)."\n";
|
|
$details .= "SESSION VALIDATION DATA CURRENT: ".print_r($validateData, true)."\n";
|
|
$details .= "CURRENT NAMESPACE SESSION DATA:\n";
|
|
$this->clear('_session_validate_data'); // already logged
|
|
$details .= print_r($this->_data, true);
|
|
$this->close(false);
|
|
$details .= "SESSION GLOBAL DATA:\n";
|
|
$details .= print_r($_SESSION, true);
|
|
|
|
// delete cookie, destroy session
|
|
$this->cookieDelete()->destroy();
|
|
|
|
// TODO event trigger
|
|
|
|
// e107::getAdminLog()->add('Session validation failed!', $details, E_LOG_FATAL);
|
|
// TODO session exception, handle it proper on live site
|
|
// throw new Exception('');
|
|
|
|
// just for now
|
|
$msg = 'Session validation failed! <a href="'.strip_tags($_SERVER['REQUEST_URI']).'">Go Back</a>';
|
|
// die($msg); //FIXME not functioning as intended.
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Validate current session based on config options
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function _validate()
|
|
{
|
|
$sessionData = $this->_data['_session_validate_data'];
|
|
$validateData = $this->getValidateData();
|
|
$keyvar = '_sessionValidate';
|
|
|
|
foreach ($validateData as $vkey => $value)
|
|
{
|
|
$var = $keyvar.$vkey;
|
|
$varskip = $var.'Skip';
|
|
if ($this->$var && $sessionData[$vkey] != $value && !in_array($value, $this->$varskip))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Retrieve data for validator
|
|
* @return array
|
|
*/
|
|
public function getValidateData()
|
|
{
|
|
$data = array(
|
|
'RemoteAddr' => '',
|
|
'HttpVia' => '',
|
|
'HttpXForwardedFor' => '',
|
|
'HttpUserAgent' => ''
|
|
);
|
|
|
|
// collect ip data
|
|
if (isset($_SERVER['REMOTE_ADDR']))
|
|
{
|
|
$data['RemoteAddr'] = (string) $_SERVER['REMOTE_ADDR'];
|
|
}
|
|
if (isset($_ENV['HTTP_VIA']))
|
|
{
|
|
$data['HttpVia'] = (string) $_ENV['HTTP_VIA'];
|
|
}
|
|
if (isset($_ENV['HTTP_X_FORWARDED_FOR']))
|
|
{
|
|
$data['HttpXForwardedFor'] = (string) $_ENV['HTTP_X_FORWARDED_FOR'];
|
|
}
|
|
|
|
// collect user agent data
|
|
if (isset($_SERVER['HTTP_USER_AGENT']))
|
|
{
|
|
$data['HttpUserAgent'] = (string) $_SERVER['HTTP_USER_AGENT'];
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Retrieve (create if doesn't exist) XSF protection token
|
|
* @param boolean $in_form if true (default) - value for forms, else raw session value
|
|
* @return string
|
|
*/
|
|
public function getFormToken($in_form = true)
|
|
{
|
|
if(!$this->has('__form_token') && !defined('e_TOKEN_DISABLE')) // TODO FIXME: SEF URL of Error page causes e-token refresh.
|
|
{
|
|
$this->set('__form_token', uniqid(md5(rand()), true));
|
|
if(deftrue('e_DEBUG_SESSION')) // XXX enable to troubleshoot "Unauthorized Access!" issues.
|
|
{
|
|
$message = date('r')."\t\t".e_REQUEST_URI."\n";
|
|
file_put_contents(__DIR__.'/session.log', $message, FILE_APPEND);
|
|
}
|
|
}
|
|
return ($in_form ? md5($this->get('__form_token')) : $this->get('__form_token'));
|
|
}
|
|
|
|
/**
|
|
* Regenerate form token value
|
|
* TODO - save old token
|
|
* @return e_session
|
|
*/
|
|
protected function _regenerateFormToken()
|
|
{
|
|
$this->set('__form_token', uniqid(md5(rand()), true));
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Do a check against passed token
|
|
* @param string $token
|
|
* @return boolean
|
|
*/
|
|
public function checkFormToken($token)
|
|
{
|
|
$utoken = $this->getFormToken(false);
|
|
return ($token === md5($utoken));
|
|
}
|
|
|
|
/**
|
|
* Clear and Unset current namespace, unregister session singleton
|
|
* e107::getSession('namespace') if needed.
|
|
* @param boolean $unregister if true (default) - unregister Singleton, destroy namespace,
|
|
* else alias of self::clearData()
|
|
* @return void
|
|
*/
|
|
public function close($unregister = true)
|
|
{
|
|
$this->clearData();
|
|
if($unregister)
|
|
{
|
|
unset($_SESSION[$this->_namespace]);
|
|
e107::setRegistry('core/e107/session/'.$this->_namespace, null);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save session data to disk, end session.
|
|
* Sessions can't be used after this point.
|
|
* Method should be called before every header redirect.
|
|
* @return void
|
|
*/
|
|
public function end()
|
|
{
|
|
session_write_close();
|
|
}
|
|
|
|
/**
|
|
* Destroy all session data
|
|
* @return e_session
|
|
*/
|
|
public function destroy()
|
|
{
|
|
$this->cookieDelete()->close();
|
|
//unset($_SESSION);
|
|
|
|
// cleanup
|
|
cookie(e_COOKIE, null, null); // remove user auth cookie
|
|
// unset($_SESSION['_cookie_session_validate']);
|
|
|
|
session_destroy();
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @return void
|
|
*/
|
|
public function replaceRegistry()
|
|
{
|
|
e107::setRegistry('core/e107/session/'.$this->_namespace, $this, true);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
*
|
|
*/
|
|
class e_core_session extends e_session
|
|
{
|
|
/**
|
|
* Constructor
|
|
* 3rd party code and/or other system areas are
|
|
* able to extend the base e_session class and
|
|
* add more or override the implemented functionality, has their own
|
|
* namespace, add more session security etc.
|
|
* @param array $data session config data
|
|
*/
|
|
public function __construct($data = array())
|
|
{
|
|
// default system configuration
|
|
$this->setDefaultSystemConfig();
|
|
|
|
$namespace = 'e107sess'; // Quick Fix for Fatal Error "Cannot use object of type e107 as array" on line 550
|
|
$name = (isset($data['name']) && !empty($data['name']) ? $data['name'] : deftrue('e_COOKIE', 'e107')).'SID';
|
|
if(isset($data['namespace']) && !empty($data['namespace'])) $namespace = $data['namespace'];
|
|
|
|
// create $_SESSION['e107'] namespace by default
|
|
$this->init($namespace, $name);
|
|
}
|
|
|
|
/**
|
|
* Session shutdown - called at the top of footer_default.php by default
|
|
* @return void
|
|
*/
|
|
public function shutdown()
|
|
{
|
|
if(!session_id()) // someone closed the session?
|
|
{
|
|
$this->init($this->_namespace, $this->_name); // restart
|
|
}
|
|
|
|
// give 3rd party code a way to prevent token re-generation
|
|
if(e_SECURITY_LEVEL >= e_session::SECURITY_LEVEL_PARANOID && !deftrue('e_TOKEN_FREEZE'))
|
|
{
|
|
if(e_SECURITY_LEVEL == e_session::SECURITY_LEVEL_INSANE)
|
|
{
|
|
// regenerate SID
|
|
$oldSID = session_id(); // old SID
|
|
$oldSData = $_SESSION; // old session data
|
|
session_regenerate_id(false); // true don't work on php4 - so time to move on people!
|
|
$newSID = session_id(); // new SID
|
|
|
|
// Clean
|
|
session_id($oldSID); // switch to the old session
|
|
session_destroy(); // destroy it
|
|
|
|
// set new ID, reopen the session, set saved data
|
|
session_id($newSID);
|
|
session_start();
|
|
$_SESSION = $oldSData;
|
|
}
|
|
$this->set('__form_token_regenerate', time()); // check() needs it to re-create token on the next request
|
|
}
|
|
// write session data
|
|
$this->end();
|
|
}
|
|
|
|
/**
|
|
* @param $status
|
|
* @param $type
|
|
* @return void|null
|
|
*/
|
|
private function log($status, $type=E_LOG_FATAL)
|
|
{
|
|
|
|
if(!deftrue('e_DEBUG_SESSION'))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
|
|
// $details = "USER: ".USERNAME."\n";
|
|
$details = "HOST: ".$_SERVER['HTTP_HOST']."\n";
|
|
$details .= "REQUEST_URI: ".$_SERVER['REQUEST_URI']."\n";
|
|
|
|
$details .= ($_POST['e-token']) ? "e-token (POST): ".$_POST['e-token']."\n" : "";
|
|
$details .= ($_GET['e-token']) ? "e-token (GET): ".$_GET['e-token']."\n" : "";
|
|
$details .= ($_POST['e_token']) ? "AJAX e_token (POST): ".$_POST['e_token']."\n" : "";
|
|
/*
|
|
$utoken = $this->getFormToken(false);
|
|
$details .= "raw token: ".$utoken."\n";
|
|
$details .= "checkFormToken (e-token should match this): ".md5($utoken)."\n";
|
|
$details .= "md5(e-token): ".md5($_POST['e-token'])."\n";*/
|
|
/*
|
|
$regenerate = $this->get('__form_token_regenerate');
|
|
$details .= "Regenerate after: ".date('r', $regenerate)." (".$regenerate.")\n";
|
|
*/
|
|
|
|
$details .= "has __form_token: ";
|
|
$hasToken = $this->has('__form_token');
|
|
$details .= empty($hasToken) ? 'false' : 'true';
|
|
$details .= "\n";
|
|
|
|
$details .= "_SESSION:\n";
|
|
$details .= print_r($_SESSION,true);
|
|
|
|
/* if($pref['plug_installed'])
|
|
{
|
|
$details .= "\nPlugins:\n";
|
|
$details .= print_r($pref['plug_installed'],true);
|
|
}*/
|
|
|
|
$details .= $status."\n\n---------------------------------\n\n";
|
|
|
|
$log = e107::getLog();
|
|
$log->addDebug($details);
|
|
|
|
if(deftrue('e_DEBUG_SESSION'))
|
|
{
|
|
$log->toFile('Unauthorized_access','Unauthorized access Log', true);
|
|
}
|
|
|
|
$log->add($status, $details, $type);
|
|
|
|
|
|
}
|
|
/**
|
|
* Core CSF protection, see class2.php
|
|
* Could be adopted by plugins for their own (different) protection logic
|
|
* @param boolean $die
|
|
* @return boolean
|
|
*/
|
|
public function check($die = true)
|
|
{
|
|
// define('e_TOKEN_NAME', 'e107_token_'.md5($_SERVER['HTTP_HOST'].e_HTTP));
|
|
// TODO e-token required for all system forms?
|
|
|
|
// only if not disabled and not in 'cli' mod
|
|
if(e_SECURITY_LEVEL < e_session::SECURITY_LEVEL_LOW || e107::getE107('cli')) return true;
|
|
|
|
if($this->getSessionId())
|
|
{
|
|
|
|
if((isset($_POST['e-token']) && !$this->checkFormToken($_POST['e-token']))
|
|
|| (isset($_GET['e-token']) && !$this->checkFormToken($_GET['e-token']))
|
|
|| (isset($_POST['e_token']) && !$this->checkFormToken($_POST['e_token']))) // '-' is not allowed in jquery. b
|
|
{
|
|
$this->log('Unauthorized access!');
|
|
// do not redirect, prevent dead loop, save server resources
|
|
if($die == true)
|
|
{
|
|
die('Unauthorized access!');
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
$this->log('Session Token Okay!', defset('E_LOG_NOTICE', 1));
|
|
|
|
}
|
|
|
|
if(!defined('e_TOKEN'))
|
|
{
|
|
// FREEZE token regeneration if minimal, ajax or iframe (ajax and iframe not implemented yet) request
|
|
$_toFreeze = (e107::getE107('minimal') || e107::getE107('ajax') || e107::getE107('iframe'));
|
|
if(!defined('e_TOKEN_FREEZE') && $_toFreeze)
|
|
{
|
|
define('e_TOKEN_FREEZE', true);
|
|
}
|
|
// __form_token_regenerate set in footer, so if footer is not called, token will be never regenerated!
|
|
if(e_SECURITY_LEVEL == e_session::SECURITY_LEVEL_INSANE && !deftrue('e_TOKEN_FREEZE') && $this->has('__form_token_regenerate'))
|
|
{
|
|
$this->_regenerateFormToken()
|
|
->clear('__form_token_regenerate');
|
|
}
|
|
define('e_TOKEN', $this->getFormToken());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Manually Reset the Token.
|
|
* @see e107forum::ajaxQuickReply();
|
|
*/
|
|
public function reset()
|
|
{
|
|
$this->_regenerateFormToken()->clear('__form_token_regenerate');
|
|
}
|
|
|
|
|
|
/**
|
|
* Make sure there is unique challenge string for CHAP login
|
|
* @see class2.php
|
|
* @return e_core_session
|
|
|
|
@TODO: Remove debug code
|
|
*/
|
|
public function challenge()
|
|
{
|
|
if (!$this->is('challenge')) // TODO: Eliminate need for this
|
|
{
|
|
$this->set('challenge', sha1(time().rand().$this->getSessionId())); // New challenge for next time
|
|
}
|
|
if ($this->is('challenge'))
|
|
{
|
|
$this->set('prevprevchallenge', $this->get('prevchallenge')); // Purely for debug
|
|
$this->set('prevchallenge', $this->get('challenge')); // Need to check user login against this
|
|
}
|
|
else
|
|
{
|
|
$this->set('prevchallenge', ''); // Dummy value
|
|
$this->set('prevprevchallenge', ''); // Dummy value
|
|
}
|
|
//$this->set('challenge', sha1(time().rand().$this->getSessionId())); // Temporarily disabled
|
|
// FIXME - session id will be regenerated if e_SECURITY_LEVEL is 'paranoid|insane' - generate (might be OK as long as values retained)
|
|
|
|
//$extra_text = 'C: '.$this->get('challenge').' PC: '.$this->get('prevchallenge').' PPC: '.$this->get('prevprevchallenge');
|
|
//$logfp = fopen(e_LOG.'authlog.txt', 'a+'); fwrite($logfp, strftime('%H:%M:%S').' CHAP start: '.$extra_text."\n"); fclose($logfp);
|
|
|
|
// could go, see _validate()
|
|
$user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
|
|
$ubrowser = md5('E107'.$user_agent);
|
|
if (!$this->is('ubrowser'))
|
|
{
|
|
$this->set('ubrowser', $ubrowser);
|
|
}
|
|
return $this;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Database session handler
|
|
*
|
|
* @todo PHP 8.1 support with {@see SessionHandlerInterface}
|
|
*/
|
|
class e_session_db #implements SessionHandlerInterface
|
|
{
|
|
/**
|
|
* @var e_db
|
|
*/
|
|
protected $_db = null;
|
|
|
|
/**
|
|
* Table name
|
|
* @var string
|
|
*/
|
|
protected $_table = 'session';
|
|
|
|
/**
|
|
* @var integer
|
|
*/
|
|
protected $_lifetime = null;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->_db = e107::getDb('session');
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
public function __destruct()
|
|
{
|
|
session_write_close();
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getTable()
|
|
{
|
|
return $this->_table;
|
|
}
|
|
|
|
/**
|
|
* @param string $table
|
|
* @return e_session_db
|
|
*/
|
|
public function setTable($table)
|
|
{
|
|
$this->_table = $table;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @return integer
|
|
*/
|
|
public function getLifetime()
|
|
{
|
|
if(null === $this->_lifetime)
|
|
{
|
|
$this->_lifetime = ini_get('session.gc_maxlifetime');
|
|
if(!$this->_lifetime)
|
|
{
|
|
$this->_lifetime = 3600;
|
|
}
|
|
}
|
|
return (integer) $this->_lifetime;
|
|
}
|
|
|
|
/**
|
|
* @param integer $seconds
|
|
* @return e_session_db
|
|
*/
|
|
public function setLifetime($seconds = null)
|
|
{
|
|
$this->_lifetime = $seconds;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set session save handler
|
|
* @return e_session_db
|
|
*/
|
|
public function setSaveHandler()
|
|
{
|
|
session_set_save_handler(
|
|
array($this, 'open'),
|
|
array($this, 'close'),
|
|
array($this, 'read'),
|
|
array($this, 'write'),
|
|
array($this, 'destroy'),
|
|
array($this, 'gc')
|
|
);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Open session, parameters are ignored (see e_session handler)
|
|
* @param string $save_path
|
|
* @param string $sess_name
|
|
* @return boolean
|
|
*/
|
|
public function open($save_path, $sess_name)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Close session
|
|
* @return boolean
|
|
*/
|
|
public function close()
|
|
{
|
|
$this->gc($this->getLifetime());
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get session data
|
|
* @param string $session_id
|
|
* @return string
|
|
*/
|
|
public function read($session_id)
|
|
{
|
|
$data = false;
|
|
$check = $this->_db->select($this->getTable(), 'session_data', "session_id='".$this->_sanitize($session_id)."' AND session_expires>".time());
|
|
if($check)
|
|
{
|
|
$tmp = $this->_db->fetch();
|
|
$data = base64_decode($tmp['session_data']);
|
|
}
|
|
elseif(false !== $check)
|
|
{
|
|
$data = '';
|
|
}
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Write session data
|
|
* @param string $session_id
|
|
* @param string $session_data
|
|
* @return boolean
|
|
*/
|
|
public function write($session_id, $session_data)
|
|
{
|
|
$data = array(
|
|
'data' => array(
|
|
'session_expires' => time() + $this->getLifetime(),
|
|
'session_data' => base64_encode($session_data),
|
|
'session_user' => defset('USERID'),
|
|
),
|
|
'_FIELD_TYPES' => array(
|
|
'session_id' => 'str',
|
|
'session_expires' => 'int',
|
|
'session_user' => 'int',
|
|
'session_data' => 'str'
|
|
),
|
|
'_DEFAULT' => 'str'
|
|
);
|
|
if(!($session_id = $this->_sanitize($session_id)))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
$check = $this->_db->select($this->getTable(), 'session_id', "`session_id`='{$session_id}'");
|
|
|
|
if($check)
|
|
{
|
|
$data['WHERE'] = "`session_id`='{$session_id}'";
|
|
if(false !== $this->_db->update($this->getTable(), $data))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
$data['data']['session_id'] = $session_id;
|
|
if($this->_db->insert($this->getTable(), $data))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Destroy session
|
|
* @param string $session_id
|
|
* @return boolean
|
|
*/
|
|
public function destroy($session_id)
|
|
{
|
|
$session_id = $this->_sanitize($session_id);
|
|
$this->_db->delete($this->getTable(), "`session_id`='{$session_id}'");
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Garbage collection
|
|
* @param integer $session_maxlf ignored - see write()
|
|
* @return boolean
|
|
*/
|
|
public function gc($session_maxlf)
|
|
{
|
|
$this->_db->delete($this->getTable(), '`session_expires`<'.time());
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Allow only well formed session id string
|
|
* @param string $session_id
|
|
* @return string
|
|
*/
|
|
protected function _sanitize($session_id)
|
|
{
|
|
return preg_replace('#[^0-9a-zA-Z,-]#', '', $session_id);
|
|
}
|
|
}
|