/libraries/joomla/session/session.php
PHP | 1012 lines | 448 code | 125 blank | 439 comment | 59 complexity | 1efc68f36b327f8463dea5a7f6eab174 MD5 | raw file
Possible License(s): LGPL-2.1
- <?php
- /**
- * @package Joomla.Platform
- * @subpackage Session
- *
- * @copyright Copyright (C) 2005 - 2012 Open Source Matters, Inc. All rights reserved.
- * @license GNU General Public License version 2 or later; see LICENSE
- */
- defined('JPATH_PLATFORM') or die;
- /**
- * 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.
- *
- * @package Joomla.Platform
- * @subpackage Session
- * @since 11.1
- */
- class JSession implements IteratorAggregate
- {
- /**
- * Internal state.
- * One of 'inactive'|'active'|'expired'|'destroyed'|'error'
- *
- * @var string
- * @see getState()
- * @since 11.1
- */
- protected $_state = 'inactive';
- /**
- * Maximum age of unused session in minutes
- *
- * @var string
- * @since 11.1
- */
- protected $_expire = 15;
- /**
- * The session store object.
- *
- * @var JSessionStorage
- * @since 11.1
- */
- protected $_store = null;
- /**
- * Security policy.
- * List of checks that will be done.
- *
- * Default values:
- * - fix_browser
- * - fix_adress
- *
- * @var array
- * @since 11.1
- */
- protected $_security = array('fix_browser');
- /**
- * Force cookies to be SSL only
- * Default false
- *
- * @var boolean
- * @since 11.1
- */
- protected $_force_ssl = false;
- /**
- * @var JSession JSession instances container.
- * @since 11.3
- */
- protected static $instance;
- /**
- * @var string
- * @since 12.2
- */
- protected $storeName;
- /**
- * Holds the JInput object
- *
- * @var JInput
- * @since 12.2
- */
- private $_input = null;
- /**
- * Holds the event dispatcher object
- *
- * @var JEventDispatcher
- * @since 12.2
- */
- private $_dispatcher = null;
- /**
- * Constructor
- *
- * @param string $store The type of storage for the session.
- * @param array $options Optional parameters
- *
- * @since 11.1
- */
- 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 = JSessionStorage::getInstance($store, $options);
- $this->storeName = $store;
- // Set options
- $this->_setOptions($options);
- $this->_setCookieParams();
- $this->_state = '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 12.2
- */
- public function __get($name)
- {
- if ($name === 'storeName')
- {
- return $this->$name;
- }
- if ($name === 'state' || $name === 'expire')
- {
- $property = '_' . $name;
- return $this->$property;
- }
- }
- /**
- * 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.
- *
- * @return JSession The Session object.
- *
- * @since 11.1
- */
- public static function getInstance($handler, $options)
- {
- if (!is_object(self::$instance))
- {
- self::$instance = new JSession($handler, $options);
- }
- return self::$instance;
- }
- /**
- * Get current state of session
- *
- * @return string The session state
- *
- * @since 11.1
- */
- public function getState()
- {
- return $this->_state;
- }
- /**
- * Get expiration time in minutes
- *
- * @return integer The session expiration time in minutes
- *
- * @since 11.1
- */
- 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 11.1
- */
- public function getToken($forceNew = false)
- {
- $token = $this->get('session.token');
- // Create a token
- if ($token === null || $forceNew)
- {
- $token = $this->_createToken(12);
- $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 11.1
- */
- 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->_state = 'expired';
- }
- return false;
- }
- return true;
- }
- /**
- * Method to determine a hash for anti-spoofing variable names
- *
- * @param boolean $forceNew If true, force a new token to be created
- *
- * @return string Hashed var name
- *
- * @since 11.1
- */
- public static function getFormToken($forceNew = false)
- {
- $user = JFactory::getUser();
- $session = JFactory::getSession();
- // TODO: Decouple from legacy JApplication class.
- if (is_callable(array('JApplication', 'getHash')))
- {
- $hash = JApplication::getHash($user->get('id', 0) . $session->getToken($forceNew));
- }
- else
- {
- $hash = md5(JFactory::getApplication()->get('secret') . $user->get('id', 0) . $session->getToken($forceNew));
- }
- return $hash;
- }
- /**
- * Retrieve an external iterator.
- *
- * @return ArrayIterator Return an ArrayIterator of $_SESSION.
- *
- * @since 12.2
- */
- public function getIterator()
- {
- return new ArrayIterator($_SESSION);
- }
- /**
- * Checks for a form token in the request.
- *
- * Use in conjunction with JHtml::_('form.token') or JSession::getFormToken.
- *
- * @param string $method The request method in which to look for the token key.
- *
- * @return boolean True if found and valid, false otherwise.
- *
- * @since 12.1
- */
- public static function checkToken($method = 'post')
- {
- $token = self::getFormToken();
- $app = JFactory::getApplication();
- if (!$app->input->$method->get($token, '', 'alnum'))
- {
- $session = JFactory::getSession();
- if ($session->isNew())
- {
- // Redirect to login screen.
- $app->redirect(JRoute::_('index.php'), JText::_('JLIB_ENVIRONMENT_SESSION_EXPIRED'));
- $app->close();
- }
- else
- {
- return false;
- }
- }
- else
- {
- return true;
- }
- }
- /**
- * Get session name
- *
- * @return string The session name
- *
- * @since 11.1
- */
- public function getName()
- {
- if ($this->_state === 'destroyed')
- {
- // @TODO : raise error
- return null;
- }
- return session_name();
- }
- /**
- * Get session id
- *
- * @return string The session name
- *
- * @since 11.1
- */
- public function getId()
- {
- if ($this->_state === 'destroyed')
- {
- // @TODO : raise error
- return null;
- }
- return session_id();
- }
- /**
- * Get the session handlers
- *
- * @return array An array of available session handlers
- *
- * @since 11.1
- */
- 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.
- // Note: DirectoryIterator::getExtension only available PHP >= 5.3.6
- if (!$file->isFile() || substr($fileName, strrpos($fileName, '.') + 1) != 'php')
- {
- continue;
- }
- // Derive the class name from the type.
- $class = str_ireplace('.php', '', 'JSessionStorage' . 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 12.2
- */
- public function isActive()
- {
- return (bool) ($this->_state == 'active');
- }
- /**
- * Check whether this session is currently created
- *
- * @return boolean True on success.
- *
- * @since 11.1
- */
- public function isNew()
- {
- $counter = $this->get('session.counter');
- return (bool) ($counter === 1);
- }
- /**
- * Check whether this session is currently created
- *
- * @param JInput $input JInput object for the session to use.
- * @param JEventDispatcher $dispatcher Dispatcher object for the session to use.
- *
- * @return void.
- *
- * @since 12.2
- */
- public function initialise(JInput $input, JEventDispatcher $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'
- *
- * @return mixed Value of a variable
- *
- * @since 11.1
- */
- public function get($name, $default = null, $namespace = 'default')
- {
- // Add prefix to namespace to avoid collisions
- $namespace = '__' . $namespace;
- if ($this->_state !== 'active' && $this->_state !== 'expired')
- {
- // @TODO :: generated error here
- $error = null;
- return $error;
- }
- 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'.
- *
- * @return mixed Old value of a variable.
- *
- * @since 11.1
- */
- public function set($name, $value = null, $namespace = 'default')
- {
- // Add prefix to namespace to avoid collisions
- $namespace = '__' . $namespace;
- if ($this->_state !== 'active')
- {
- // @TODO :: generated error here
- return null;
- }
- $old = isset($_SESSION[$namespace][$name]) ? $_SESSION[$namespace][$name] : null;
- if (null === $value)
- {
- 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'
- *
- * @return boolean True if the variable exists
- *
- * @since 11.1
- */
- public function has($name, $namespace = 'default')
- {
- // Add prefix to namespace to avoid collisions.
- $namespace = '__' . $namespace;
- if ($this->_state !== 'active')
- {
- // @TODO :: generated error here
- return null;
- }
- 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'
- *
- * @return mixed The value from session or NULL if not set
- *
- * @since 11.1
- */
- public function clear($name, $namespace = 'default')
- {
- // Add prefix to namespace to avoid collisions
- $namespace = '__' . $namespace;
- if ($this->_state !== 'active')
- {
- // @TODO :: generated error here
- return null;
- }
- $value = null;
- if (isset($_SESSION[$namespace][$name]))
- {
- $value = $_SESSION[$namespace][$name];
- unset($_SESSION[$namespace][$name]);
- }
- return $value;
- }
- /**
- * Start a session.
- *
- * @return void
- *
- * @since 12.2
- */
- public function start()
- {
- if ($this->_state === 'active')
- {
- return;
- }
- $this->_start();
- $this->_state = 'active';
- // Initialise the session
- $this->_setCounter();
- $this->_setTimers();
- // Perform security checks
- $this->_validate();
- if ($this->_dispatcher instanceof JEventDispatcher)
- {
- $this->_dispatcher->trigger('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 11.1
- */
- protected function _start()
- {
- // Start session if not started
- if ($this->_state === 'restart')
- {
- session_regenerate_id(true);
- }
- else
- {
- $session_name = session_name();
- // Get the JInputCookie object
- $cookie = $this->_input->cookie;
- if (is_null($cookie->get($session_name)))
- {
- $session_clean = $this->_input->get($session_name, false, 'string');
- if ($session_clean)
- {
- session_id($session_clean);
- $cookie->set($session_name, '', time() - 3600);
- }
- }
- }
- /**
- * 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 11.1
- */
- public function destroy()
- {
- // Session was already destroyed
- if ($this->_state === '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.
- */
- if (isset($_COOKIE[session_name()]))
- {
- $config = JFactory::getConfig();
- $cookie_domain = $config->get('cookie_domain', '');
- $cookie_path = $config->get('cookie_path', '/');
- setcookie(session_name(), '', time() - 42000, $cookie_path, $cookie_domain);
- }
- session_unset();
- session_destroy();
- $this->_state = 'destroyed';
- return true;
- }
- /**
- * Restart an expired or locked session.
- *
- * @return boolean True on success
- *
- * @see destroy
- * @since 11.1
- */
- public function restart()
- {
- $this->destroy();
- if ($this->_state !== '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->_state = 'restart';
- // Regenerate session id
- session_regenerate_id(true);
- $this->_start();
- $this->_state = '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 11.1
- */
- public function fork()
- {
- if ($this->_state !== 'active')
- {
- // @TODO :: generated error here
- return false;
- }
- // Save values
- $values = $_SESSION;
- // 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
- session_set_cookie_params($cookie['lifetime'], $cookie['path'], $cookie['domain'], $cookie['secure'], true);
- // 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 11.1
- */
- public function close()
- {
- session_write_close();
- }
- /**
- * Set session cookie parameters
- *
- * @return void
- *
- * @since 11.1
- */
- protected function _setCookieParams()
- {
- $cookie = session_get_cookie_params();
- if ($this->_force_ssl)
- {
- $cookie['secure'] = true;
- }
- $config = JFactory::getConfig();
- if ($config->get('cookie_domain', '') != '')
- {
- $cookie['domain'] = $config->get('cookie_domain');
- }
- if ($config->get('cookie_path', '') != '')
- {
- $cookie['path'] = $config->get('cookie_path');
- }
- session_set_cookie_params($cookie['lifetime'], $cookie['path'], $cookie['domain'], $cookie['secure'], true);
- }
- /**
- * Create a token-string
- *
- * @param integer $length Length of string
- *
- * @return string Generated token
- *
- * @since 11.1
- */
- protected function _createToken($length = 32)
- {
- static $chars = '0123456789abcdef';
- $max = strlen($chars) - 1;
- $token = '';
- $name = session_name();
- for ($i = 0; $i < $length; ++$i)
- {
- $token .= $chars[(rand(0, $max))];
- }
- return md5($token . $name);
- }
- /**
- * Set counter of session usage
- *
- * @return boolean True on success
- *
- * @since 11.1
- */
- 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 11.1
- */
- 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 11.1
- */
- 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->_expire = $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'];
- }
- // Sync the session maxlifetime
- ini_set('session.gc_maxlifetime', $this->_expire);
- 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
- *
- * @see http://shiflett.org/articles/the-truth-about-sessions
- * @since 11.1
- */
- protected function _validate($restart = false)
- {
- // Allow to restart a session
- if ($restart)
- {
- $this->_state = 'active';
- $this->set('session.client.address', null);
- $this->set('session.client.forwarded', null);
- $this->set('session.client.browser', null);
- $this->set('session.token', null);
- }
- // Check if session has expired
- if ($this->_expire)
- {
- $curTime = $this->get('session.timer.now', 0);
- $maxTime = $this->get('session.timer.last', 0) + $this->_expire;
- // Empty session variables
- if ($maxTime < $curTime)
- {
- $this->_state = 'expired';
- return false;
- }
- }
- // Record proxy forwarded for in the session in case we need it later
- if (isset($_SERVER['HTTP_X_FORWARDED_FOR']))
- {
- $this->set('session.client.forwarded', $_SERVER['HTTP_X_FORWARDED_FOR']);
- }
- // Check for client address
- if (in_array('fix_adress', $this->_security) && isset($_SERVER['REMOTE_ADDR']))
- {
- $ip = $this->get('session.client.address');
- if ($ip === null)
- {
- $this->set('session.client.address', $_SERVER['REMOTE_ADDR']);
- }
- elseif ($_SERVER['REMOTE_ADDR'] !== $ip)
- {
- $this->_state = 'error';
- return false;
- }
- }
- // Check for clients browser
- if (in_array('fix_browser', $this->_security) && isset($_SERVER['HTTP_USER_AGENT']))
- {
- $browser = $this->get('session.client.browser');
- if ($browser === null)
- {
- $this->set('session.client.browser', $_SERVER['HTTP_USER_AGENT']);
- }
- elseif ($_SERVER['HTTP_USER_AGENT'] !== $browser)
- {
- // @todo remove code: $this->_state = 'error';
- // @todo remove code: return false;
- }
- }
- return true;
- }
- }