Files
server/usr/share/psa-pear/pear/php/Horde/Session.php
2026-01-07 20:52:11 +01:00

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);
}
}