635 lines
18 KiB
PHP
635 lines
18 KiB
PHP
<?php
|
|
/**
|
|
* Copyright 2010-2017 Horde LLC (http://www.horde.org/)
|
|
*
|
|
* See the enclosed file COPYING for license information (LGPL). If you
|
|
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
|
|
*
|
|
* @author Michael Slusarz <slusarz@horde.org>
|
|
* @category Horde
|
|
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
|
|
* @package Core
|
|
*/
|
|
|
|
/**
|
|
* The Horde_Session class provides a set of methods for handling the
|
|
* administration and contents of the Horde session variable.
|
|
*
|
|
* @author Michael Slusarz <slusarz@horde.org>
|
|
* @category Horde
|
|
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
|
|
* @package Core
|
|
*
|
|
* @property-read integer $begin The timestamp when this session began (0 if
|
|
* session is not active).
|
|
* @property-read boolean $regenerate_due True if session ID is due for
|
|
* regeneration (since 2.5.0).
|
|
* @property-read integer $regenerate_interval The regeneration interval
|
|
* (since 2.5.0).
|
|
* @property-write array $session_data Manually set session data (since
|
|
* 2.5.0).
|
|
*/
|
|
class Horde_Session
|
|
{
|
|
/* Class constants. */
|
|
const BEGIN = '_b';
|
|
const ENCRYPTED = '_e'; /* @since 2.7.0 */
|
|
const MODIFIED = '_m'; /* @deprecated */
|
|
const PRUNE = '_p';
|
|
const REGENERATE = '_r'; /* @since 2.5.0 */
|
|
|
|
const TYPE_ARRAY = 1;
|
|
const TYPE_OBJECT = 2;
|
|
const ENCRYPT = 4; /* @since 2.7.0 */
|
|
|
|
const NOT_SERIALIZED = "\0";
|
|
|
|
const NONCE_ID = 'session_nonce'; /* @since 2.11.0 */
|
|
const TOKEN_ID = 'session_token';
|
|
|
|
/**
|
|
* Maximum size of the pruneable data store.
|
|
*
|
|
* @var integer
|
|
*/
|
|
public $maxStore = 20;
|
|
|
|
/**
|
|
* The session handler object.
|
|
*
|
|
* @var Horde_SessionHandler
|
|
*/
|
|
public $sessionHandler = null;
|
|
|
|
/**
|
|
* Indicates that the session is active (read/write).
|
|
*
|
|
* @var boolean
|
|
*/
|
|
protected $_active = false;
|
|
|
|
/**
|
|
* Indicate that a new session ID has been generated for this page load.
|
|
*
|
|
* @var boolean
|
|
*/
|
|
protected $_cleansession = false;
|
|
|
|
/**
|
|
* Pointer to the session data.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_data;
|
|
|
|
/**
|
|
* Indicates that session data is read-only.
|
|
*
|
|
* @var boolean
|
|
*/
|
|
protected $_readonly = false;
|
|
|
|
/**
|
|
* On re-login, indicate whether we were previously authenticated.
|
|
*
|
|
* @var integer
|
|
*/
|
|
protected $_relogin = null;
|
|
|
|
/**
|
|
* Constructor.
|
|
*/
|
|
public function __construct()
|
|
{
|
|
/* Make sure global session variable is always initialized. */
|
|
$_SESSION = array();
|
|
$this->_data = &$_SESSION;
|
|
}
|
|
|
|
/**
|
|
*/
|
|
public function __get($name)
|
|
{
|
|
switch ($name) {
|
|
case 'begin':
|
|
return ($this->_active || $this->_relogin)
|
|
? $this->_data[self::BEGIN]
|
|
: 0;
|
|
|
|
case 'regenerate_due':
|
|
return (isset($this->_data[self::REGENERATE]) &&
|
|
(time() >= $this->_data[self::REGENERATE]));
|
|
|
|
case 'regenerate_interval':
|
|
// DEFAULT: 6 hours
|
|
return 21600;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*/
|
|
public function __set($name, $value)
|
|
{
|
|
switch ($name) {
|
|
case 'session_data':
|
|
$this->_data = &$value;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets a custom session handler up, if there is one.
|
|
*
|
|
* @param boolean $start Initiate the session?
|
|
* @param string $cache_limiter Override for the session cache limiter
|
|
* value.
|
|
* @param string $session_id The session ID to use.
|
|
*
|
|
* @throws Horde_Exception
|
|
*/
|
|
public function setup($start = true, $cache_limiter = null,
|
|
$session_id = null)
|
|
{
|
|
global $conf, $injector;
|
|
|
|
ini_set('url_rewriter.tags', 0);
|
|
if (empty($conf['session']['use_only_cookies'])) {
|
|
ini_set('session.use_only_cookies', 0);
|
|
} else {
|
|
ini_set('session.use_only_cookies', 1);
|
|
if (!empty($conf['cookie']['domain']) &&
|
|
(strpos($conf['server']['name'], '.') === false)) {
|
|
throw new Horde_Exception('Session cookies will not work without a FQDN and with a non-empty cookie domain. Either use a fully qualified domain name like "http://www.example.com" instead of "http://example" only, or set the cookie domain in the Horde configuration to an empty value, or enable non-cookie (url-based) sessions in the Horde configuration.');
|
|
}
|
|
}
|
|
|
|
if (!empty($conf['session']['timeout'])) {
|
|
ini_set('session.gc_maxlifetime', $conf['session']['timeout']);
|
|
}
|
|
|
|
session_set_cookie_params(
|
|
$conf['session']['timeout'],
|
|
$conf['cookie']['path'],
|
|
$conf['cookie']['domain'],
|
|
$conf['use_ssl'] == 1 ? 1 : 0,
|
|
true
|
|
);
|
|
session_cache_limiter(is_null($cache_limiter) ? $conf['session']['cache_limiter'] : $cache_limiter);
|
|
session_name(urlencode($conf['session']['name']));
|
|
if ($session_id) {
|
|
session_id($session_id);
|
|
}
|
|
|
|
/* We want to create an instance here, not get, since we may be
|
|
* destroying the previous instances in the page. */
|
|
$this->sessionHandler = $injector->createInstance('Horde_SessionHandler');
|
|
|
|
if ($start) {
|
|
$this->start();
|
|
$this->_start();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts the session.
|
|
*/
|
|
public function start()
|
|
{
|
|
/* Limit session ID to 32 bytes. Session IDs are NOT cryptographically
|
|
* secure hashes. Instead, they are nothing more than a way to
|
|
* generate random strings. */
|
|
ini_set('session.hash_function', 0);
|
|
ini_set('session.hash_bits_per_character', 5);
|
|
|
|
session_start();
|
|
$this->_active = true;
|
|
$this->_data = &$_SESSION;
|
|
|
|
/* We have reopened a session. Check to make sure that authentication
|
|
* status has not changed in the meantime. */
|
|
if (!$this->_readonly &&
|
|
!is_null($this->_relogin) &&
|
|
(($GLOBALS['registry']->getAuth() !== false) !== $this->_relogin)) {
|
|
Horde::log('Previous session attempted to be reopened after authentication status change. All session modifications will be ignored.', 'DEBUG');
|
|
$this->_readonly = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tasks to perform when starting a session.
|
|
*/
|
|
private function _start()
|
|
{
|
|
$curr_time = time();
|
|
|
|
/* Create internal data arrays. */
|
|
if (!isset($this->_data[self::BEGIN])) {
|
|
$this->_data[self::BEGIN] = $curr_time;
|
|
$this->_data[self::REGENERATE] = $curr_time + $this->regenerate_interval;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Regenerate the session ID.
|
|
*
|
|
* @since 2.5.0
|
|
*/
|
|
public function regenerate()
|
|
{
|
|
/* Load old encrypted data. */
|
|
$encrypted = array();
|
|
if (!empty($this->_data[self::ENCRYPTED])) {
|
|
foreach ($this->_data[self::ENCRYPTED] as $app => $val) {
|
|
foreach (array_keys($val) as $val2) {
|
|
$encrypted[$app][$val2] = $this->get($app, $val2);
|
|
}
|
|
}
|
|
}
|
|
|
|
session_regenerate_id(true);
|
|
$this->_data[self::REGENERATE] = time() + $this->regenerate_interval;
|
|
|
|
/* Store encrypted data, since secret key may have changed. */
|
|
foreach ($encrypted as $app => $val) {
|
|
foreach ($val as $key2 => $val2) {
|
|
$this->set($app, $key2, $val2, self::ENCRYPT);
|
|
}
|
|
}
|
|
|
|
$this->sessionHandler->changed = true;
|
|
}
|
|
|
|
/**
|
|
* Destroys any existing session on login and make sure to use a new
|
|
* session ID, to avoid session fixation issues. Should be called before
|
|
* checking a login.
|
|
*
|
|
* @return boolean True if the session was cleaned.
|
|
*/
|
|
public function clean()
|
|
{
|
|
if ($this->_cleansession) {
|
|
return false;
|
|
}
|
|
|
|
// Make sure to force a completely new session ID and clear all
|
|
// session data.
|
|
session_regenerate_id(true);
|
|
session_unset();
|
|
$this->_data = array();
|
|
$this->_start();
|
|
|
|
$GLOBALS['injector']->getInstance('Horde_Secret_Cbc')->setKey();
|
|
|
|
$this->_cleansession = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Close the current session.
|
|
*/
|
|
public function close()
|
|
{
|
|
$this->_active = false;
|
|
$this->_relogin = ($GLOBALS['registry']->getAuth() !== false);
|
|
session_write_close();
|
|
}
|
|
|
|
/**
|
|
* Destroy session data.
|
|
*/
|
|
public function destroy()
|
|
{
|
|
if (isset($_SESSION)) {
|
|
session_destroy();
|
|
}
|
|
// @suspicious shouldn't we empty $this->_data here too?
|
|
$this->_cleansession = true;
|
|
$GLOBALS['injector']->getInstance('Horde_Secret_Cbc')->clearKey();
|
|
}
|
|
|
|
/**
|
|
* Is the current session active (read/write)?
|
|
*
|
|
* @return boolean True if the current session is active.
|
|
*/
|
|
public function isActive()
|
|
{
|
|
return $this->_active;
|
|
}
|
|
|
|
/* Session variable access. */
|
|
|
|
/**
|
|
* Does the session variable exist?
|
|
*
|
|
* @param string $app Application name.
|
|
* @param string $name Session variable name.
|
|
*
|
|
* @return boolean True if session variable exists.
|
|
*/
|
|
public function exists($app, $name)
|
|
{
|
|
return isset($this->_data[$app][$name]);
|
|
}
|
|
|
|
/**
|
|
* Get the value of a session variable.
|
|
*
|
|
* @param string $app Application name.
|
|
* @param string $name Session variable name.
|
|
* @param integer $mask One of:
|
|
* - Horde_Session::TYPE_ARRAY - Return an array value.
|
|
* - Horde_Session::TYPE_OBJECT - Return an object value.
|
|
*
|
|
* @return mixed The value or null if the value doesn't exist.
|
|
*/
|
|
public function get($app, $name, $mask = 0)
|
|
{
|
|
global $injector;
|
|
|
|
if ($this->exists($app, $name)) {
|
|
$value = $this->_data[$app][$name];
|
|
if (!is_string($value) || strlen($value) === 0) {
|
|
return $value;
|
|
}
|
|
|
|
if (isset($this->_data[self::ENCRYPTED][$app][$name])) {
|
|
$secret = $injector->getInstance('Horde_Secret_Cbc');
|
|
$value = strval($secret->read($secret->getKey(), $value));
|
|
}
|
|
|
|
if ($value[0] === self::NOT_SERIALIZED) {
|
|
return substr($value, 1);
|
|
}
|
|
|
|
try {
|
|
return $injector->getInstance('Horde_Pack')->unpack($value);
|
|
} catch (Horde_Pack_Exception $e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if ($subkeys = $this->_subkeys($app, $name)) {
|
|
$ret = array();
|
|
foreach ($subkeys as $k => $v) {
|
|
$ret[$k] = $this->get($app, $v, $mask);
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
/* @todo Deprecated. */
|
|
if (strpos($name, self::DATA) === 0) {
|
|
return $this->retrieve($name);
|
|
}
|
|
|
|
switch ($mask) {
|
|
case self::TYPE_ARRAY:
|
|
return array();
|
|
|
|
case self::TYPE_OBJECT:
|
|
return new stdClass;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Sets the value of a session variable.
|
|
*
|
|
* @param string $app Application name.
|
|
* @param string $name Session variable name.
|
|
* @param mixed $value Session variable value.
|
|
* @param integer $mask One of:
|
|
* - Horde_Session::TYPE_ARRAY: Force save as an array value.
|
|
* - Horde_Session::TYPE_OBJECT: Force save as an object value.
|
|
* - Horde_Session::ENCRYPT: Encrypt the value. (since 2.7.0)
|
|
*/
|
|
public function set($app, $name, $value, $mask = 0)
|
|
{
|
|
global $injector;
|
|
|
|
if ($this->_readonly) {
|
|
return;
|
|
}
|
|
|
|
unset($this->_data[self::ENCRYPTED][$app][$name]);
|
|
|
|
/* Each particular piece of session data is generally not used on any
|
|
* given page load. Thus, for arrays and objects, it is beneficial to
|
|
* always convert to string representations so that the object/array
|
|
* does not need to be rebuilt every time the session is reloaded.
|
|
* For convenience, encrypted data is ALWAYS serialized, regardless
|
|
* of whether it is already a string. */
|
|
if (($mask & self::ENCRYPT) ||
|
|
is_object($value) || ($mask & self::TYPE_OBJECT) ||
|
|
is_array($value) || ($mask & self::TYPE_ARRAY)) {
|
|
$opts = array('compress' => 0);
|
|
if (is_object($value) || ($mask & self::TYPE_OBJECT)) {
|
|
$opts['phpob'] = true;
|
|
}
|
|
$value = $injector->getInstance('Horde_Pack')->pack($value, $opts);
|
|
|
|
if ($mask & self::ENCRYPT) {
|
|
$secret = $injector->getInstance('Horde_Secret_Cbc');
|
|
$value = $secret->write($secret->getKey(), $value);
|
|
$this->_data[self::ENCRYPTED][$app][$name] = true;
|
|
}
|
|
} elseif (is_string($value)) {
|
|
$value = self::NOT_SERIALIZED . $value;
|
|
}
|
|
|
|
if (!$this->exists($app, $name) ||
|
|
($this->_data[$app][$name] !== $value)) {
|
|
$this->_data[$app][$name] = $value;
|
|
$this->sessionHandler->changed = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove session key(s).
|
|
*
|
|
* @param string $app Application name.
|
|
* @param string $name Session variable name.
|
|
*/
|
|
public function remove($app, $name = null)
|
|
{
|
|
if ($this->_readonly || !isset($this->_data[$app])) {
|
|
return;
|
|
}
|
|
|
|
if (is_null($name)) {
|
|
unset($this->_data[$app]);
|
|
$this->sessionHandler->changed = true;
|
|
} elseif ($this->exists($app, $name)) {
|
|
unset(
|
|
$this->_data[$app][$name],
|
|
$this->_data[self::PRUNE][$this->_getKey($app, $name)]
|
|
);
|
|
$this->sessionHandler->changed = true;
|
|
} else {
|
|
foreach ($this->_subkeys($app, $name) as $val) {
|
|
$this->remove($app, $val);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates the unique storage key.
|
|
*
|
|
* @param string $app Application name.
|
|
* @param string $name Session variable name.
|
|
*
|
|
* @return string The unique storage key.
|
|
*/
|
|
private function _getKey($app, $name)
|
|
{
|
|
return $app . ':' . $name;
|
|
}
|
|
|
|
/**
|
|
* Return the list of subkeys for a master key.
|
|
*
|
|
* @param string $app Application name.
|
|
* @param string $name Session variable name.
|
|
*
|
|
* @return array Subkeyname (keys) and session variable name (values).
|
|
*/
|
|
private function _subkeys($app, $name)
|
|
{
|
|
$ret = array();
|
|
|
|
if ($name &&
|
|
isset($this->_data[$app]) &&
|
|
($name[strlen($name) - 1] == '/')) {
|
|
foreach (array_keys($this->_data[$app]) as $k) {
|
|
if (strpos($k, $name) === 0) {
|
|
$ret[substr($k, strlen($name))] = $k;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
/* Session tokens. */
|
|
|
|
/**
|
|
* Returns the session token.
|
|
*
|
|
* @return string Session token.
|
|
*/
|
|
public function getToken()
|
|
{
|
|
if ($token = $this->get('horde', self::TOKEN_ID)) {
|
|
return $token;
|
|
}
|
|
|
|
$token = strval(new Horde_Support_Randomid());
|
|
$this->set('horde', self::TOKEN_ID, $token);
|
|
|
|
return $token;
|
|
}
|
|
|
|
/**
|
|
* Checks the validity of the session token.
|
|
*
|
|
* @param string $token Token to check.
|
|
*
|
|
* @throws Horde_Exception
|
|
*/
|
|
public function checkToken($token)
|
|
{
|
|
if ($this->getToken() != $token) {
|
|
throw new Horde_Exception('Invalid token!');
|
|
}
|
|
}
|
|
|
|
/* Session nonces. */
|
|
|
|
/**
|
|
* Returns a single-use, session nonce.
|
|
*
|
|
* @since 2.11.0
|
|
*
|
|
* @return string Session nonce.
|
|
*/
|
|
public function getNonce()
|
|
{
|
|
$id = strval(new Horde_Support_Randomid());
|
|
|
|
$nonces = $this->get('horde', self::NONCE_ID, self::TYPE_ARRAY);
|
|
$nonces[] = $id;
|
|
$this->set('horde', self::NONCE_ID, array_values($nonces));
|
|
|
|
return $id;
|
|
}
|
|
|
|
/**
|
|
* Checks the validity of the session nonce.
|
|
*
|
|
* @since 2.11.0
|
|
*
|
|
* @param string $nonce Nonce to check.
|
|
*
|
|
* @throws Horde_Exception
|
|
*/
|
|
public function checkNonce($nonce)
|
|
{
|
|
$nonces = $this->get('horde', self::NONCE_ID, self::TYPE_ARRAY);
|
|
if (($pos = array_search($nonce, $nonces)) === false) {
|
|
throw new Horde_Exception('Invalid token!');
|
|
}
|
|
unset($nonces[$pos]);
|
|
$this->set('horde', self::NONCE_ID, array_values($nonces));
|
|
}
|
|
|
|
/* Session object storage. */
|
|
|
|
/** @deprecated */
|
|
const DATA = '_d';
|
|
|
|
/**
|
|
* @deprecated Use Horde_Core_Cache_SessionObjects instead.
|
|
*/
|
|
public function store($data, $prune = true, $id = null)
|
|
{
|
|
global $injector;
|
|
|
|
if (is_null($id)) {
|
|
$id = strval(new Horde_Support_Randomid());
|
|
}
|
|
|
|
$ob = new Horde_Core_Cache_SessionObjects();
|
|
$ob->set($id, $injector->getInstance('Horde_Pack')->pack($data));
|
|
|
|
return $id;
|
|
}
|
|
|
|
/**
|
|
* @deprecated Use Horde_Core_Cache_SessionObjects instead.
|
|
*/
|
|
public function retrieve($id)
|
|
{
|
|
global $injector;
|
|
|
|
$ob = new Horde_Core_Cache_SessionObjects();
|
|
try {
|
|
return $injector->getInstance('Horde_Pack')->unpack($ob->get($id));
|
|
} catch (Horde_Pack_Exception $e) {}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @deprecated Use Horde_Core_Cache_SessionObjects instead.
|
|
*/
|
|
public function purge($id)
|
|
{
|
|
$ob = new Horde_Core_Cache_SessionObjects();
|
|
return $ob->expire($id);
|
|
}
|
|
|
|
}
|