<?php /** * Part of the Joomla Framework Session Package * * @copyright Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved. * @license GNU General Public License version 2 or later; see LICENSE */ namespace Joomla\Session; use Joomla\Event\DispatcherInterface; use Joomla\Input\Input; /** * Class for managing HTTP sessions * * Provides access to session-state values as well as session-level * settings and lifetime management methods. * Based on the standard PHP session handling mechanism it provides * more advanced features such as expire timeouts. * * @since 1.0 */ class Session implements \IteratorAggregate { /** * Internal state. * One of 'inactive'|'active'|'expired'|'destroyed'|'error' * * @var string * @see getState() * @since 1.0 */ protected $state = 'inactive'; /** * Maximum age of unused session in minutes * * @var string * @since 1.0 */ protected $expire = 15; /** * The session store object. * * @var Storage * @since 1.0 */ protected $store; /** * Security policy. * List of checks that will be done. * * Default values: * - fix_browser * - fix_adress * * @var array * @since 1.0 */ protected $security = array('fix_browser'); /** * Force cookies to be SSL only * Default false * * @var boolean * @since 1.0 */ protected $force_ssl = false; /** * The domain to use when setting cookies. * * @var mixed * @since 1.0 * @deprecated 2.0 */ protected $cookie_domain; /** * The path to use when setting cookies. * * @var mixed * @since 1.0 * @deprecated 2.0 */ protected $cookie_path; /** * The configuration of the HttpOnly cookie. * * @var mixed * @since 1.5.0 * @deprecated 2.0 */ protected $cookie_httponly = true; /** * The configuration of the SameSite cookie. * * @var mixed * @since 1.5.0 * @deprecated 2.0 */ protected $cookie_samesite; /** * Session instances container. * * @var Session * @since 1.0 * @deprecated 2.0 */ protected static $instance; /** * The type of storage for the session. * * @var string * @since 1.0 * @deprecated 2.0 */ protected $storeName; /** * Holds the Input object * * @var Input * @since 1.0 */ private $input; /** * Holds the Dispatcher object * * @var DispatcherInterface * @since 1.0 */ private $dispatcher; /** * Constructor * * @param string $store The type of storage for the session. * @param array $options Optional parameters * * @since 1.0 */ public function __construct($store = 'none', array $options = array()) { // Need to destroy any existing sessions started with session.auto_start if (session_id()) { session_unset(); session_destroy(); } // Disable transparent sid support ini_set('session.use_trans_sid', '0'); // Only allow the session ID to come from cookies and nothing else. ini_set('session.use_only_cookies', '1'); // Create handler $this->store = Storage::getInstance($store, $options); $this->storeName = $store; // Set options $this->_setOptions($options); $this->_setCookieParams(); $this->setState('inactive'); } /** * Magic method to get read-only access to properties. * * @param string $name Name of property to retrieve * * @return mixed The value of the property * * @since 1.0 * @deprecated 2.0 Use get methods for non-deprecated properties */ public function __get($name) { if ($name === 'storeName' || $name === 'state' || $name === 'expire') { return $this->$name; } } /** * Returns the global Session object, only creating it * if it doesn't already exist. * * @param string $handler The type of session handler. * @param array $options An array of configuration options (for new sessions only). * * @return Session The Session object. * * @since 1.0 * @deprecated 2.0 A singleton object store will no longer be supported */ public static function getInstance($handler, array $options = array()) { if (!\is_object(self::$instance)) { self::$instance = new self($handler, $options); } return self::$instance; } /** * Get current state of session * * @return string The session state * * @since 1.0 */ public function getState() { return $this->state; } /** * Get expiration time in minutes * * @return integer The session expiration time in minutes * * @since 1.0 */ public function getExpire() { return $this->expire; } /** * Get a session token, if a token isn't set yet one will be generated. * * Tokens are used to secure forms from spamming attacks. Once a token * has been generated the system will check the post request to see if * it is present, if not it will invalidate the session. * * @param boolean $forceNew If true, force a new token to be created * * @return string The session token * * @since 1.0 */ public function getToken($forceNew = false) { $token = $this->get('session.token'); // Create a token if ($token === null || $forceNew) { $token = $this->_createToken(); $this->set('session.token', $token); } return $token; } /** * Method to determine if a token exists in the session. If not the * session will be set to expired * * @param string $tCheck Hashed token to be verified * @param boolean $forceExpire If true, expires the session * * @return boolean * * @since 1.0 */ public function hasToken($tCheck, $forceExpire = true) { // Check if a token exists in the session $tStored = $this->get('session.token'); // Check token if (($tStored !== $tCheck)) { if ($forceExpire) { $this->setState('expired'); } return false; } return true; } /** * Retrieve an external iterator. * * @return \ArrayIterator Return an ArrayIterator of $_SESSION. * * @since 1.0 */ public function getIterator() { return new \ArrayIterator($_SESSION); } /** * Get session name * * @return string The session name * * @since 1.0 */ public function getName() { if ($this->getState() === 'destroyed') { // @codingStandardsIgnoreLine return; } return session_name(); } /** * Get session id * * @return string The session name * * @since 1.0 */ public function getId() { if ($this->getState() === 'destroyed') { // @codingStandardsIgnoreLine return; } return session_id(); } /** * Get the session handlers * * @return array An array of available session handlers * * @since 1.0 * @deprecated 2.0 The Storage class chain will be removed */ public static function getStores() { $connectors = array(); // Get an iterator and loop trough the driver classes. $iterator = new \DirectoryIterator(__DIR__ . '/Storage'); foreach ($iterator as $file) { $fileName = $file->getFilename(); // Only load for php files. if (!$file->isFile() || $file->getExtension() != 'php') { continue; } // Derive the class name from the type. $class = str_ireplace('.php', '', '\\Joomla\\Session\\Storage\\' . ucfirst(trim($fileName))); // If the class doesn't exist we have nothing left to do but look at the next type. We did our best. if (!class_exists($class)) { continue; } // Sweet! Our class exists, so now we just need to know if it passes its test method. if ($class::isSupported()) { // Connector names should not have file extensions. $connectors[] = str_ireplace('.php', '', $fileName); } } return $connectors; } /** * Shorthand to check if the session is active * * @return boolean * * @since 1.0 */ public function isActive() { return (bool) ($this->getState() == 'active'); } /** * Check whether this session is currently created * * @return boolean True on success. * * @since 1.0 */ public function isNew() { $counter = $this->get('session.counter'); return (bool) ($counter === 1); } /** * Check whether this session is currently created * * @param Input $input Input object for the session to use. * @param DispatcherInterface $dispatcher Dispatcher object for the session to use. * * @return void * * @since 1.0 * @deprecated 2.0 In 2.0 the DispatcherInterface should be injected via the object constructor */ public function initialise(Input $input, DispatcherInterface $dispatcher = null) { $this->input = $input; $this->dispatcher = $dispatcher; } /** * Get data from the session store * * @param string $name Name of a variable * @param mixed $default Default value of a variable if not set * @param string $namespace Namespace to use, default to 'default' {@deprecated 2.0 Namespace support will be removed.} * * @return mixed Value of a variable * * @since 1.0 */ public function get($name, $default = null, $namespace = 'default') { // Add prefix to namespace to avoid collisions $namespace = '__' . $namespace; if ($this->getState() !== 'active' && $this->getState() !== 'expired') { return; } if (isset($_SESSION[$namespace][$name])) { return $_SESSION[$namespace][$name]; } return $default; } /** * Set data into the session store. * * @param string $name Name of a variable. * @param mixed $value Value of a variable. * @param string $namespace Namespace to use, default to 'default' {@deprecated 2.0 Namespace support will be removed.} * * @return mixed Old value of a variable. * * @since 1.0 */ public function set($name, $value = null, $namespace = 'default') { // Add prefix to namespace to avoid collisions $namespace = '__' . $namespace; if ($this->getState() !== 'active') { return; } $old = isset($_SESSION[$namespace][$name]) ? $_SESSION[$namespace][$name] : null; if ($value === null) { unset($_SESSION[$namespace][$name]); } else { $_SESSION[$namespace][$name] = $value; } return $old; } /** * Check whether data exists in the session store * * @param string $name Name of variable * @param string $namespace Namespace to use, default to 'default' {@deprecated 2.0 Namespace support will be removed.} * * @return boolean True if the variable exists * * @since 1.0 */ public function has($name, $namespace = 'default') { // Add prefix to namespace to avoid collisions. $namespace = '__' . $namespace; if ($this->getState() !== 'active') { // @codingStandardsIgnoreLine return; } return isset($_SESSION[$namespace][$name]); } /** * Unset data from the session store * * @param string $name Name of variable * @param string $namespace Namespace to use, default to 'default' {@deprecated 2.0 Namespace support will be removed.} * * @return mixed The value from session or NULL if not set * * @since 1.0 */ public function clear($name, $namespace = 'default') { // Add prefix to namespace to avoid collisions $namespace = '__' . $namespace; if ($this->getState() !== 'active') { // @TODO :: generated error here return; } $value = null; if (isset($_SESSION[$namespace][$name])) { $value = $_SESSION[$namespace][$name]; unset($_SESSION[$namespace][$name]); } return $value; } /** * Start a session. * * @return void * * @since 1.0 */ public function start() { if ($this->getState() === 'active') { return; } $this->_start(); $this->setState('active'); // Initialise the session $this->_setCounter(); $this->_setTimers(); // Perform security checks $this->_validate(); if ($this->dispatcher instanceof DispatcherInterface) { $this->dispatcher->triggerEvent('onAfterSessionStart'); } } /** * Start a session. * * Creates a session (or resumes the current one based on the state of the session) * * @return boolean true on success * * @since 1.0 * @deprecated 2.0 */ protected function _start() { // Start session if not started if ($this->getState() === 'restart') { session_regenerate_id(true); } else { $session_name = session_name(); // Get the Joomla\Input\Cookie object $cookie = $this->input->cookie; if ($cookie->get($session_name) === null) { $session_clean = $this->input->get($session_name, false, 'string'); if ($session_clean) { session_id($session_clean); $cookie->set($session_name, '', array('expires' => 1)); } } } /** * Write and Close handlers are called after destructing objects since PHP 5.0.5. * Thus destructors can use sessions but session handler can't use objects. * So we are moving session closure before destructing objects. * * Replace with session_register_shutdown() when dropping compatibility with PHP 5.3 */ register_shutdown_function('session_write_close'); session_cache_limiter('none'); session_start(); return true; } /** * Frees all session variables and destroys all data registered to a session * * This method resets the $_SESSION variable and destroys all of the data associated * with the current session in its storage (file or DB). It forces new session to be * started after this method is called. It does not unset the session cookie. * * @return boolean True on success * * @see session_destroy() * @see session_unset() * @since 1.0 */ public function destroy() { // Session was already destroyed if ($this->getState() === 'destroyed') { return true; } /* * In order to kill the session altogether, such as to log the user out, the session id * must also be unset. If a cookie is used to propagate the session id (default behavior), * then the session cookie must be deleted. */ $cookie = session_get_cookie_params(); $cookieOptions = array( 'expires' => 1, 'path' => $cookie['path'], 'domain' => $cookie['domain'], 'secure' => $cookie['secure'], 'httponly' => true, ); if (isset($cookie['samesite'])) { $cookieOptions['samesite'] = $cookie['samesite']; } $this->input->cookie->set($this->getName(), '', $cookieOptions); session_unset(); session_destroy(); $this->setState('destroyed'); return true; } /** * Restart an expired or locked session. * * @return boolean True on success * * @see destroy * @since 1.0 */ public function restart() { $this->destroy(); if ($this->getState() !== 'destroyed') { // @TODO :: generated error here return false; } // Re-register the session handler after a session has been destroyed, to avoid PHP bug $this->store->register(); $this->setState('restart'); // Regenerate session id session_regenerate_id(true); $this->_start(); $this->setState('active'); $this->_validate(); $this->_setCounter(); return true; } /** * Create a new session and copy variables from the old one * * @return boolean $result true on success * * @since 1.0 */ public function fork() { if ($this->getState() !== 'active') { // @TODO :: generated error here return false; } // Keep session config $cookie = session_get_cookie_params(); // Kill session session_destroy(); // Re-register the session store after a session has been destroyed, to avoid PHP bug $this->store->register(); // Restore config if (version_compare(PHP_VERSION, '7.3', '>=')) { session_set_cookie_params($cookie); } else { session_set_cookie_params($cookie['lifetime'], $cookie['path'], $cookie['domain'], $cookie['secure'], $cookie['httponly']); } // Restart session with new id session_regenerate_id(true); session_start(); return true; } /** * Writes session data and ends session * * Session data is usually stored after your script terminated without the need * to call JSession::close(), but as session data is locked to prevent concurrent * writes only one script may operate on a session at any time. When using * framesets together with sessions you will experience the frames loading one * by one due to this locking. You can reduce the time needed to load all the * frames by ending the session as soon as all changes to session variables are * done. * * @return void * * @see session_write_close() * @since 1.0 */ public function close() { session_write_close(); } /** * Set the session expiration * * @param integer $expire Maximum age of unused session in minutes * * @return $this * * @since 1.3.0 */ protected function setExpire($expire) { $this->expire = $expire; return $this; } /** * Set the session state * * @param string $state Internal state * * @return $this * * @since 1.3.0 */ protected function setState($state) { $this->state = $state; return $this; } /** * Set session cookie parameters * * @return void * * @since 1.0 * @deprecated 2.0 */ protected function _setCookieParams() { $cookie = session_get_cookie_params(); if ($this->force_ssl) { $cookie['secure'] = true; } if ($this->cookie_domain) { $cookie['domain'] = $this->cookie_domain; } if ($this->cookie_path) { $cookie['path'] = $this->cookie_path; } $cookie['httponly'] = $this->cookie_httponly; if ($this->cookie_samesite) { $cookie['samesite'] = $this->cookie_samesite; } if (version_compare(PHP_VERSION, '7.3', '>=')) { session_set_cookie_params($cookie); } else { session_set_cookie_params($cookie['lifetime'], $cookie['path'], $cookie['domain'], $cookie['secure'], $cookie['httponly']); } } /** * Create a token-string * * @param integer $length Length of string * * @return string Generated token * * @since 1.0 * @deprecated 2.0 Use createToken instead */ protected function _createToken($length = 32) { return $this->createToken($length); } /** * Create a token-string * * @param integer $length Length of string * * @return string Generated token * * @since 1.3.1 */ protected function createToken($length = 32) { return bin2hex(random_bytes($length)); } /** * Set counter of session usage * * @return boolean True on success * * @since 1.0 * @deprecated 2.0 Use setCounter instead */ protected function _setCounter() { return $this->setCounter(); } /** * Set counter of session usage * * @return boolean True on success * * @since 1.3.0 */ protected function setCounter() { $counter = $this->get('session.counter', 0); ++$counter; $this->set('session.counter', $counter); return true; } /** * Set the session timers * * @return boolean True on success * * @since 1.0 * @deprecated 2.0 Use setTimers instead */ protected function _setTimers() { return $this->setTimers(); } /** * Set the session timers * * @return boolean True on success * * @since 1.3.0 */ protected function setTimers() { if (!$this->has('session.timer.start')) { $start = time(); $this->set('session.timer.start', $start); $this->set('session.timer.last', $start); $this->set('session.timer.now', $start); } $this->set('session.timer.last', $this->get('session.timer.now')); $this->set('session.timer.now', time()); return true; } /** * Set additional session options * * @param array $options List of parameter * * @return boolean True on success * * @since 1.0 * @deprecated 2.0 Use setOptions instead */ protected function _setOptions(array $options) { return $this->setOptions($options); } /** * Set additional session options * * @param array $options List of parameter * * @return boolean True on success * * @since 1.3.0 */ protected function setOptions(array $options) { // Set name if (isset($options['name'])) { session_name(md5($options['name'])); } // Set id if (isset($options['id'])) { session_id($options['id']); } // Set expire time if (isset($options['expire'])) { $this->setExpire($options['expire']); } // Get security options if (isset($options['security'])) { $this->security = explode(',', $options['security']); } if (isset($options['force_ssl'])) { $this->force_ssl = (bool) $options['force_ssl']; } 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_httponly'])) { $this->cookie_httponly = (bool) $options['cookie_httponly']; } if (isset($options['cookie_samesite'])) { $this->cookie_samesite = $options['cookie_samesite']; } // Sync the session maxlifetime if (!headers_sent()) { ini_set('session.gc_maxlifetime', $this->getExpire()); } return true; } /** * Do some checks for security reason * * - timeout check (expire) * - ip-fixiation * - browser-fixiation * * If one check failed, session data has to be cleaned. * * @param boolean $restart Reactivate session * * @return boolean True on success * * @link http://shiflett.org/articles/the-truth-about-sessions * @since 1.0 * @deprecated 2.0 Use validate instead */ protected function _validate($restart = false) { return $this->validate($restart); } /** * Do some checks for security reason * * - timeout check (expire) * - ip-fixiation * - browser-fixiation * * If one check failed, session data has to be cleaned. * * @param boolean $restart Reactivate session * * @return boolean True on success * * @link http://shiflett.org/articles/the-truth-about-sessions * @since 1.3.0 */ protected function validate($restart = false) { // Allow to restart a session if ($restart) { $this->setState('active'); $this->set('session.client.address', null); $this->set('session.client.forwarded', null); $this->set('session.token', null); } // Check if session has expired if ($this->getExpire()) { $curTime = $this->get('session.timer.now', 0); $maxTime = $this->get('session.timer.last', 0) + $this->getExpire(); // Empty session variables if ($maxTime < $curTime) { $this->setState('expired'); return false; } } $remoteAddr = $this->input->server->getString('REMOTE_ADDR', ''); // Check for client address if (\in_array('fix_adress', $this->security) && !empty($remoteAddr) && filter_var($remoteAddr, FILTER_VALIDATE_IP) !== false) { $ip = $this->get('session.client.address'); if ($ip === null) { $this->set('session.client.address', $remoteAddr); } elseif ($remoteAddr !== $ip) { $this->setState('error'); return false; } } $xForwardedFor = $this->input->server->getString('HTTP_X_FORWARDED_FOR', ''); // Record proxy forwarded for in the session in case we need it later if (!empty($xForwardedFor) && filter_var($xForwardedFor, FILTER_VALIDATE_IP) !== false) { $this->set('session.client.forwarded', $xForwardedFor); } return true; } }