<?php /** * Zend Framework (http://framework.zend.com/) * * @link http://github.com/zendframework/zf2 for the canonical source repository * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ namespace Zend\Session; use Zend\EventManager\EventManagerInterface; /** * Session ManagerInterface implementation utilizing ext/session */ class SessionManager extends AbstractManager { /** * Default options when a call to {@link destroy()} is made * - send_expire_cookie: whether or not to send a cookie expiring the current session cookie * - clear_storage: whether or not to empty the storage object of any stored values * @var array */ protected $defaultDestroyOptions = array( 'send_expire_cookie' => true, 'clear_storage' => false, ); /** * @var string value returned by session_name() */ protected $name; /** * @var EventManagerInterface Validation chain to determine if session is valid */ protected $validatorChain; /** * Constructor * * @param Config\ConfigInterface|null $config * @param Storage\StorageInterface|null $storage * @param SaveHandler\SaveHandlerInterface|null $saveHandler * @throws Exception\RuntimeException */ public function __construct(Config\ConfigInterface $config = null, Storage\StorageInterface $storage = null, SaveHandler\SaveHandlerInterface $saveHandler = null) { parent::__construct($config, $storage, $saveHandler); register_shutdown_function(array($this, 'writeClose')); } /** * Does a session exist and is it currently active? * * @return bool */ public function sessionExists() { $sid = defined('SID') ? constant('SID') : false; if ($sid !== false && $this->getId()) { return true; } if (headers_sent()) { return true; } return false; } /** * Start session * * if No session currently exists, attempt to start it. Calls * {@link isValid()} once session_start() is called, and raises an * exception if validation fails. * * @param bool $preserveStorage If set to true, current session storage will not be overwritten by the * contents of $_SESSION. * @return void * @throws Exception\RuntimeException */ public function start($preserveStorage = false) { if ($this->sessionExists()) { return; } $saveHandler = $this->getSaveHandler(); if ($saveHandler instanceof SaveHandler\SaveHandlerInterface) { // register the session handler with ext/session $this->registerSaveHandler($saveHandler); } session_start(); $storage = $this->getStorage(); // Since session is starting, we need to potentially repopulate our // session storage if ($storage instanceof Storage\SessionStorage && $_SESSION !== $storage) { if (!$preserveStorage) { $storage->fromArray($_SESSION); } $_SESSION = $storage; } elseif ($storage instanceof Storage\StorageInitializationInterface) { $storage->init($_SESSION); } if (!$this->isValid()) { throw new Exception\RuntimeException('Session validation failed'); } } /** * Destroy/end a session * * @param array $options See {@link $defaultDestroyOptions} * @return void */ public function destroy(array $options = null) { if (!$this->sessionExists()) { return; } if (null === $options) { $options = $this->defaultDestroyOptions; } else { $options = array_merge($this->defaultDestroyOptions, $options); } session_destroy(); if ($options['send_expire_cookie']) { $this->expireSessionCookie(); } if ($options['clear_storage']) { $this->getStorage()->clear(); } } /** * Write session to save handler and close * * Once done, the Storage object will be marked as isImmutable. * * @return void */ public function writeClose() { // The assumption is that we're using PHP's ext/session. // session_write_close() will actually overwrite $_SESSION with an // empty array on completion -- which leads to a mismatch between what // is in the storage object and $_SESSION. To get around this, we // temporarily reset $_SESSION to an array, and then re-link it to // the storage object. // // Additionally, while you _can_ write to $_SESSION following a // session_write_close() operation, no changes made to it will be // flushed to the session handler. As such, we now mark the storage // object isImmutable. $storage = $this->getStorage(); if (!$storage->isImmutable()) { $_SESSION = $storage->toArray(true); session_write_close(); $storage->fromArray($_SESSION); $storage->markImmutable(); } } /** * Attempt to set the session name * * If the session has already been started, or if the name provided fails * validation, an exception will be raised. * * @param string $name * @return SessionManager * @throws Exception\InvalidArgumentException */ public function setName($name) { if ($this->sessionExists()) { throw new Exception\InvalidArgumentException( 'Cannot set session name after a session has already started' ); } if (!preg_match('/^[a-zA-Z0-9]+$/', $name)) { throw new Exception\InvalidArgumentException( 'Name provided contains invalid characters; must be alphanumeric only' ); } $this->name = $name; session_name($name); return $this; } /** * Get session name * * Proxies to {@link session_name()}. * * @return string */ public function getName() { if (null === $this->name) { // If we're grabbing via session_name(), we don't need our // validation routine; additionally, calling setName() after // session_start() can lead to issues, and often we just need the name // in order to do things such as setting cookies. $this->name = session_name(); } return $this->name; } /** * Set session ID * * Can safely be called in the middle of a session. * * @param string $id * @return SessionManager */ public function setId($id) { if ($this->sessionExists()) { throw new Exception\RuntimeException('Session has already been started, to change the session ID call regenerateId()'); } session_id($id); return $this; } /** * Get session ID * * Proxies to {@link session_id()} * * @return string */ public function getId() { return session_id(); } /** * Regenerate id * * Regenerate the session ID, using session save handler's * native ID generation Can safely be called in the middle of a session. * * @param bool $deleteOldSession * @return SessionManager */ public function regenerateId($deleteOldSession = true) { session_regenerate_id((bool) $deleteOldSession); return $this; } /** * Set the TTL (in seconds) for the session cookie expiry * * Can safely be called in the middle of a session. * * @param null|int $ttl * @return SessionManager */ public function rememberMe($ttl = null) { if (null === $ttl) { $ttl = $this->getConfig()->getRememberMeSeconds(); } $this->setSessionCookieLifetime($ttl); return $this; } /** * Set a 0s TTL for the session cookie * * Can safely be called in the middle of a session. * * @return SessionManager */ public function forgetMe() { $this->setSessionCookieLifetime(0); return $this; } /** * Set the validator chain to use when validating a session * * In most cases, you should use an instance of {@link ValidatorChain}. * * @param EventManagerInterface $chain * @return SessionManager */ public function setValidatorChain(EventManagerInterface $chain) { $this->validatorChain = $chain; return $this; } /** * Get the validator chain to use when validating a session * * By default, uses an instance of {@link ValidatorChain}. * * @return EventManagerInterface */ public function getValidatorChain() { if (null === $this->validatorChain) { $this->setValidatorChain(new ValidatorChain($this->getStorage())); } return $this->validatorChain; } /** * Is this session valid? * * Notifies the Validator Chain until either all validators have returned * true or one has failed. * * @return bool */ public function isValid() { $validator = $this->getValidatorChain(); $responses = $validator->triggerUntil('session.validate', $this, array($this), function ($test) { return !$test; }); if ($responses->stopped()) { // If execution was halted, validation failed return false; } // Otherwise, we're good to go return true; } /** * Expire the session cookie * * Sends a session cookie with no value, and with an expiry in the past. * * @return void */ public function expireSessionCookie() { $config = $this->getConfig(); if (!$config->getUseCookies()) { return; } setcookie( $this->getName(), // session name '', // value $_SERVER['REQUEST_TIME'] - 42000, // TTL for cookie $config->getCookiePath(), $config->getCookieDomain(), $config->getCookieSecure(), $config->getCookieHttpOnly() ); } /** * Set the session cookie lifetime * * If a session already exists, destroys it (without sending an expiration * cookie), regenerates the session ID, and restarts the session. * * @param int $ttl * @return void */ protected function setSessionCookieLifetime($ttl) { $config = $this->getConfig(); if (!$config->getUseCookies()) { return; } // Set new cookie TTL $config->setCookieLifetime($ttl); if ($this->sessionExists()) { // There is a running session so we'll regenerate id to send a new cookie $this->regenerateId(); } } /** * Register Save Handler with ext/session * * Since ext/session is coupled to this particular session manager * register the save handler with ext/session. * * @param SaveHandler\SaveHandlerInterface $saveHandler * @return bool */ protected function registerSaveHandler(SaveHandler\SaveHandlerInterface $saveHandler) { return session_set_save_handler( array($saveHandler, 'open'), array($saveHandler, 'close'), array($saveHandler, 'read'), array($saveHandler, 'write'), array($saveHandler, 'destroy'), array($saveHandler, 'gc') ); } }