[HttpFoundation] Make sessions secure and lazy

This commit is contained in:
Nicolas Grekas 2017-10-10 11:13:08 +02:00
parent 3f0a3f58a6
commit 347939c9b3
41 changed files with 1001 additions and 121 deletions

View File

@ -126,6 +126,8 @@ Form
FrameworkBundle
---------------
* The `session.use_strict_mode` option has been deprecated and is enabled by default.
* The `cache:clear` command doesn't clear "app" PSR-6 cache pools anymore,
but still clears "system" ones.
Use the `cache:pool:clear` command to clear "app" pools instead.
@ -235,18 +237,13 @@ HttpFoundation
* The `Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandler`
class has been deprecated and will be removed in 4.0. Use the `\SessionHandler` class instead.
* The `Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy` class has been
deprecated and will be removed in 4.0. Use your `\SessionHandlerInterface` implementation directly.
* The `Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler` class has been
deprecated and will be removed in 4.0. Implement `SessionUpdateTimestampHandlerInterface` or
extend `AbstractSessionHandler` instead.
* The `Symfony\Component\HttpFoundation\Session\Storage\Proxy\NativeProxy` class has been
deprecated and will be removed in 4.0. Use your `\SessionHandlerInterface` implementation directly.
* The `Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy` class has been
deprecated and will be removed in 4.0. Use your `\SessionHandlerInterface` implementation directly.
* `NativeSessionStorage::setSaveHandler()` now takes an instance of `\SessionHandlerInterface` as argument.
Not passing it is deprecated and will throw a `TypeError` in 4.0.
* Using `Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler` with the legacy mongo extension
has been deprecated and will be removed in 4.0. Use it with the mongodb/mongodb package and ext-mongodb instead.

View File

@ -329,6 +329,8 @@ Form
FrameworkBundle
---------------
* The `session.use_strict_mode` option has been removed and strict mode is always enabled.
* The `validator.mapping.cache.doctrine.apc` service has been removed.
* The "framework.trusted_proxies" configuration option and the corresponding "kernel.trusted_proxies" parameter have been removed. Use the `Request::setTrustedProxies()` method in your front controller instead.
@ -542,12 +544,11 @@ HttpFoundation
* The ability to check only for cacheable HTTP methods using `Request::isMethodSafe()` is
not supported anymore, use `Request::isMethodCacheable()` instead.
* The `Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandler`,
`Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy`,
`Symfony\Component\HttpFoundation\Session\Storage\Proxy\NativeProxy` and
`Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy` classes have been removed.
* The `Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler` class has been
removed. Implement `SessionUpdateTimestampHandlerInterface` or extend `AbstractSessionHandler` instead.
* `NativeSessionStorage::setSaveHandler()` now requires an instance of `\SessionHandlerInterface` as argument.
* The `Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandler` and
`Symfony\Component\HttpFoundation\Session\Storage\Proxy\NativeProxy` classes have been removed.
* The `Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler` does not work with the legacy
mongo extension anymore. It requires mongodb/mongodb package and ext-mongodb.

View File

@ -30,7 +30,7 @@
"symfony/polyfill-intl-icu": "~1.0",
"symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php56": "~1.0",
"symfony/polyfill-php70": "~1.0",
"symfony/polyfill-php70": "~1.6",
"symfony/polyfill-util": "~1.0"
},
"replace": {

View File

@ -4,6 +4,7 @@ CHANGELOG
3.4.0
-----
* Session `use_strict_mode` is now enabled by default and the corresponding option has been deprecated
* Made the `cache:clear` command to *not* clear "app" PSR-6 cache pools anymore,
but to still clear "system" ones; use the `cache:pool:clear` command to clear "app" pools instead
* Always register a minimalist logger that writes in `stderr`

View File

@ -462,11 +462,14 @@ class Configuration implements ConfigurationInterface
->scalarNode('gc_divisor')->end()
->scalarNode('gc_probability')->defaultValue(1)->end()
->scalarNode('gc_maxlifetime')->end()
->booleanNode('use_strict_mode')->end()
->booleanNode('use_strict_mode')
->defaultTrue()
->setDeprecated('The "%path%.%node%" option is enabled by default and deprecated since Symfony 3.4. It will be always enabled in 4.0.')
->end()
->scalarNode('save_path')->defaultValue('%kernel.cache_dir%/sessions')->end()
->integerNode('metadata_update_threshold')
->defaultValue('0')
->info('seconds to wait between 2 session metadata updates, it will also prevent the session handler to write if the session has not changed')
->info('seconds to wait between 2 session metadata updates')
->end()
->end()
->end()

View File

@ -916,14 +916,7 @@ class FrameworkExtension extends Extension
$container->getDefinition('session.storage.native')->replaceArgument(1, null);
$container->getDefinition('session.storage.php_bridge')->replaceArgument(0, null);
} else {
$handlerId = $config['handler_id'];
if ($config['metadata_update_threshold'] > 0) {
$container->getDefinition('session.handler.write_check')->addArgument(new Reference($handlerId));
$handlerId = 'session.handler.write_check';
}
$container->setAlias('session.handler', $handlerId)->setPrivate(true);
$container->setAlias('session.handler', $config['handler_id'])->setPrivate(true);
}
$container->setParameter('session.save_path', $config['save_path']);

View File

@ -48,11 +48,17 @@
<argument type="service" id="session.storage.metadata_bag" />
</service>
<service id="session.handler.native_file" class="Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler">
<argument>%session.save_path%</argument>
<service id="session.handler.native_file" class="Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler">
<argument type="service">
<service class="Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler">
<argument>%session.save_path%</argument>
</service>
</argument>
</service>
<service id="session.handler.write_check" class="Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler" />
<service id="session.handler.write_check" class="Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler">
<deprecated>The "%service_id%" service is deprecated since Symfony 3.4 and will be removed in 4.0. Use the `session.lazy_write` ini setting instead.</deprecated>
</service>
<service id="session_listener" class="Symfony\Component\HttpKernel\EventListener\SessionListener">
<tag name="kernel.event_subscriber" />

View File

@ -301,6 +301,7 @@ class ConfigurationTest extends TestCase
'gc_probability' => 1,
'save_path' => '%kernel.cache_dir%/sessions',
'metadata_update_threshold' => '0',
'use_strict_mode' => true,
),
'request' => array(
'enabled' => false,

View File

@ -4,8 +4,9 @@ CHANGELOG
3.4.0
-----
* deprecated the `NativeSessionHandler` class,
* deprecated the `AbstractProxy`, `NativeProxy` and `SessionHandlerProxy` classes,
* implemented PHP 7.0's `SessionUpdateTimestampHandlerInterface` with a new
`AbstractSessionHandler` base class and a new `StrictSessionHandler` wrapper
* deprecated the `WriteCheckSessionHandler`, `NativeSessionHandler` and `NativeProxy` classes
* deprecated setting session save handlers that do not implement `\SessionHandlerInterface` in `NativeSessionStorage::setSaveHandler()`
* deprecated using `MongoDbSessionHandler` with the legacy mongo extension; use it with the mongodb/mongodb package and ext-mongodb instead
* deprecated `MemcacheSessionHandler`; use `MemcachedSessionHandler` instead

View File

@ -0,0 +1,165 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
/**
* This abstract session handler provides a generic implementation
* of the PHP 7.0 SessionUpdateTimestampHandlerInterface,
* enabling strict and lazy session handling.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
abstract class AbstractSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface
{
private $sessionName;
private $prefetchId;
private $prefetchData;
private $newSessionId;
private $igbinaryEmptyData;
/**
* {@inheritdoc}
*/
public function open($savePath, $sessionName)
{
$this->sessionName = $sessionName;
return true;
}
/**
* @param string $sessionId
*
* @return string
*/
abstract protected function doRead($sessionId);
/**
* @param string $sessionId
* @param string $data
*
* @return bool
*/
abstract protected function doWrite($sessionId, $data);
/**
* @param string $sessionId
*
* @return bool
*/
abstract protected function doDestroy($sessionId);
/**
* {@inheritdoc}
*/
public function validateId($sessionId)
{
$this->prefetchData = $this->read($sessionId);
$this->prefetchId = $sessionId;
return '' !== $this->prefetchData;
}
/**
* {@inheritdoc}
*/
public function read($sessionId)
{
if (null !== $this->prefetchId) {
$prefetchId = $this->prefetchId;
$prefetchData = $this->prefetchData;
$this->prefetchId = $this->prefetchData = null;
if ($prefetchId === $sessionId || '' === $prefetchData) {
$this->newSessionId = '' === $prefetchData ? $sessionId : null;
return $prefetchData;
}
}
$data = $this->doRead($sessionId);
$this->newSessionId = '' === $data ? $sessionId : null;
if (\PHP_VERSION_ID < 70000) {
$this->prefetchData = $data;
}
return $data;
}
/**
* {@inheritdoc}
*/
public function write($sessionId, $data)
{
if (\PHP_VERSION_ID < 70000 && $this->prefetchData) {
$readData = $this->prefetchData;
$this->prefetchData = null;
if ($readData === $data) {
return $this->updateTimestamp($sessionId, $data);
}
}
if (null === $this->igbinaryEmptyData) {
// see https://github.com/igbinary/igbinary/issues/146
$this->igbinaryEmptyData = \function_exists('igbinary_serialize') ? igbinary_serialize(array()) : '';
}
if ('' === $data || $this->igbinaryEmptyData === $data) {
return $this->destroy($sessionId);
}
$this->newSessionId = null;
return $this->doWrite($sessionId, $data);
}
/**
* {@inheritdoc}
*/
public function destroy($sessionId)
{
if (\PHP_VERSION_ID < 70000) {
$this->prefetchData = null;
}
if (!headers_sent() && ini_get('session.use_cookies')) {
if (!$this->sessionName) {
throw new \LogicException(sprintf('Session name cannot be empty, did you forget to call "parent::open()" in "%s"?.', get_class($this)));
}
$sessionCookie = sprintf(' %s=', urlencode($this->sessionName));
$sessionCookieWithId = sprintf('%s%s;', $sessionCookie, urlencode($sessionId));
$sessionCookieFound = false;
$otherCookies = array();
foreach (headers_list() as $h) {
if (0 !== stripos($h, 'Set-Cookie:')) {
continue;
}
if (11 === strpos($h, $sessionCookie, 11)) {
$sessionCookieFound = true;
if (11 !== strpos($h, $sessionCookieWithId, 11)) {
$otherCookies[] = $h;
}
} else {
$otherCookies[] = $h;
}
}
if ($sessionCookieFound) {
header_remove('Set-Cookie');
foreach ($otherCookies as $h) {
header('Set-Cookie:'.$h, false);
}
} else {
setcookie($this->sessionName, '', 0, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure'), ini_get('session.cookie_httponly'));
}
}
return $this->newSessionId === $sessionId || $this->doDestroy($sessionId);
}
}

View File

@ -19,7 +19,7 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
*
* @author Drak <drak@zikula.org>
*/
class MemcachedSessionHandler implements \SessionHandlerInterface
class MemcachedSessionHandler extends AbstractSessionHandler
{
/**
* @var \Memcached Memcached driver
@ -39,7 +39,7 @@ class MemcachedSessionHandler implements \SessionHandlerInterface
/**
* List of available options:
* * prefix: The prefix to use for the memcached keys in order to avoid collision
* * expiretime: The time to live in seconds
* * expiretime: The time to live in seconds.
*
* @param \Memcached $memcached A \Memcached instance
* @param array $options An associative array of Memcached options
@ -60,14 +60,6 @@ class MemcachedSessionHandler implements \SessionHandlerInterface
$this->prefix = isset($options['prefix']) ? $options['prefix'] : 'sf2s';
}
/**
* {@inheritdoc}
*/
public function open($savePath, $sessionName)
{
return true;
}
/**
* {@inheritdoc}
*/
@ -79,7 +71,7 @@ class MemcachedSessionHandler implements \SessionHandlerInterface
/**
* {@inheritdoc}
*/
public function read($sessionId)
protected function doRead($sessionId)
{
return $this->memcached->get($this->prefix.$sessionId) ?: '';
}
@ -87,7 +79,15 @@ class MemcachedSessionHandler implements \SessionHandlerInterface
/**
* {@inheritdoc}
*/
public function write($sessionId, $data)
public function updateTimestamp($sessionId, $data)
{
return $this->memcached->touch($this->prefix.$sessionId, time() + $this->ttl);
}
/**
* {@inheritdoc}
*/
protected function doWrite($sessionId, $data)
{
return $this->memcached->set($this->prefix.$sessionId, $data, time() + $this->ttl);
}
@ -95,7 +95,7 @@ class MemcachedSessionHandler implements \SessionHandlerInterface
/**
* {@inheritdoc}
*/
public function destroy($sessionId)
protected function doDestroy($sessionId)
{
$result = $this->memcached->delete($this->prefix.$sessionId);

View File

@ -19,7 +19,7 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
* @see https://packagist.org/packages/mongodb/mongodb
* @see http://php.net/manual/en/set.mongodb.php
*/
class MongoDbSessionHandler implements \SessionHandlerInterface
class MongoDbSessionHandler extends AbstractSessionHandler
{
/**
* @var \Mongo|\MongoClient|\MongoDB\Client
@ -43,7 +43,7 @@ class MongoDbSessionHandler implements \SessionHandlerInterface
* * id_field: The field name for storing the session id [default: _id]
* * data_field: The field name for storing the session data [default: data]
* * time_field: The field name for storing the timestamp [default: time]
* * expiry_field: The field name for storing the expiry-timestamp [default: expires_at]
* * expiry_field: The field name for storing the expiry-timestamp [default: expires_at].
*
* It is strongly recommended to put an index on the `expiry_field` for
* garbage-collection. Alternatively it's possible to automatically expire
@ -92,14 +92,6 @@ class MongoDbSessionHandler implements \SessionHandlerInterface
), $options);
}
/**
* {@inheritdoc}
*/
public function open($savePath, $sessionName)
{
return true;
}
/**
* {@inheritdoc}
*/
@ -111,7 +103,7 @@ class MongoDbSessionHandler implements \SessionHandlerInterface
/**
* {@inheritdoc}
*/
public function destroy($sessionId)
protected function doDestroy($sessionId)
{
$methodName = $this->mongo instanceof \MongoDB\Client ? 'deleteOne' : 'remove';
@ -139,7 +131,7 @@ class MongoDbSessionHandler implements \SessionHandlerInterface
/**
* {@inheritdoc}
*/
public function write($sessionId, $data)
protected function doWrite($sessionId, $data)
{
$expiry = $this->createDateTime(time() + (int) ini_get('session.gc_maxlifetime'));
@ -171,7 +163,34 @@ class MongoDbSessionHandler implements \SessionHandlerInterface
/**
* {@inheritdoc}
*/
public function read($sessionId)
public function updateTimestamp($sessionId, $data)
{
$expiry = $this->createDateTime(time() + (int) ini_get('session.gc_maxlifetime'));
if ($this->mongo instanceof \MongoDB\Client) {
$methodName = 'updateOne';
$options = array();
} else {
$methodName = 'update';
$options = array('multiple' => false);
}
$this->getCollection()->$methodName(
array($this->options['id_field'] => $sessionId),
array('$set' => array(
$this->options['time_field'] => $this->createDateTime(),
$this->options['expiry_field'] => $expiry,
)),
$options
);
return true;
}
/**
* {@inheritdoc}
*/
protected function doRead($sessionId)
{
$dbData = $this->getCollection()->findOne(array(
$this->options['id_field'] => $sessionId,

View File

@ -11,12 +11,14 @@
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
@trigger_error('The '.__NAMESPACE__.'\NativeSessionHandler class is deprecated since version 3.4 and will be removed in 4.0. Use the \SessionHandler class instead.', E_USER_DEPRECATED);
/**
* @deprecated since version 3.4, to be removed in 4.0. Use \SessionHandler instead.
* @see http://php.net/sessionhandler
*/
class NativeSessionHandler extends \SessionHandler
{
public function __construct()
{
@trigger_error('The '.__NAMESPACE__.'\NativeSessionHandler class is deprecated since version 3.4 and will be removed in 4.0. Use the \SessionHandler class instead.', E_USER_DEPRECATED);
}
}

View File

@ -16,16 +16,8 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
*
* @author Drak <drak@zikula.org>
*/
class NullSessionHandler implements \SessionHandlerInterface
class NullSessionHandler extends AbstractSessionHandler
{
/**
* {@inheritdoc}
*/
public function open($savePath, $sessionName)
{
return true;
}
/**
* {@inheritdoc}
*/
@ -37,15 +29,7 @@ class NullSessionHandler implements \SessionHandlerInterface
/**
* {@inheritdoc}
*/
public function read($sessionId)
{
return '';
}
/**
* {@inheritdoc}
*/
public function write($sessionId, $data)
public function validateId($sessionId)
{
return true;
}
@ -53,7 +37,31 @@ class NullSessionHandler implements \SessionHandlerInterface
/**
* {@inheritdoc}
*/
public function destroy($sessionId)
protected function doRead($sessionId)
{
return '';
}
/**
* {@inheritdoc}
*/
public function updateTimestamp($sessionId, $data)
{
return true;
}
/**
* {@inheritdoc}
*/
protected function doWrite($sessionId, $data)
{
return true;
}
/**
* {@inheritdoc}
*/
protected function doDestroy($sessionId)
{
return true;
}

View File

@ -38,7 +38,7 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
* @author Michael Williams <michael.williams@funsational.com>
* @author Tobias Schultze <http://tobion.de>
*/
class PdoSessionHandler implements \SessionHandlerInterface
class PdoSessionHandler extends AbstractSessionHandler
{
/**
* No locking is done. This means sessions are prone to loss of data due to
@ -260,11 +260,13 @@ class PdoSessionHandler implements \SessionHandlerInterface
*/
public function open($savePath, $sessionName)
{
$this->sessionExpired = false;
if (null === $this->pdo) {
$this->connect($this->dsn ?: $savePath);
}
return true;
return parent::open($savePath, $sessionName);
}
/**
@ -273,7 +275,7 @@ class PdoSessionHandler implements \SessionHandlerInterface
public function read($sessionId)
{
try {
return $this->doRead($sessionId);
return parent::read($sessionId);
} catch (\PDOException $e) {
$this->rollback();
@ -296,7 +298,7 @@ class PdoSessionHandler implements \SessionHandlerInterface
/**
* {@inheritdoc}
*/
public function destroy($sessionId)
protected function doDestroy($sessionId)
{
// delete the record associated with this id
$sql = "DELETE FROM $this->table WHERE $this->idCol = :id";
@ -317,7 +319,7 @@ class PdoSessionHandler implements \SessionHandlerInterface
/**
* {@inheritdoc}
*/
public function write($sessionId, $data)
protected function doWrite($sessionId, $data)
{
$maxlifetime = (int) ini_get('session.gc_maxlifetime');
@ -372,6 +374,30 @@ class PdoSessionHandler implements \SessionHandlerInterface
return true;
}
/**
* {@inheritdoc}
*/
public function updateTimestamp($sessionId, $data)
{
$maxlifetime = (int) ini_get('session.gc_maxlifetime');
try {
$updateStmt = $this->pdo->prepare(
"UPDATE $this->table SET $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id"
);
$updateStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
$updateStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT);
$updateStmt->bindValue(':time', time(), \PDO::PARAM_INT);
$updateStmt->execute();
} catch (\PDOException $e) {
$this->rollback();
throw $e;
}
return true;
}
/**
* {@inheritdoc}
*/
@ -491,10 +517,8 @@ class PdoSessionHandler implements \SessionHandlerInterface
*
* @return string The session data
*/
private function doRead($sessionId)
protected function doRead($sessionId)
{
$this->sessionExpired = false;
if (self::LOCK_ADVISORY === $this->lockMode) {
$this->unlockStatements[] = $this->doAdvisoryLock($sessionId);
}
@ -517,7 +541,9 @@ class PdoSessionHandler implements \SessionHandlerInterface
return is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0];
}
if (self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver) {
if (!ini_get('session.use_strict_mode') && self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver) {
// In strict mode, session fixation is not possible: new sessions always start with a unique
// random id, so that concurrency is not possible and this code path can be skipped.
// Exclusive-reading of non-existent rows does not block, so we need to do an insert to block
// until other connections to the session are committed.
try {

View File

@ -0,0 +1,89 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
/**
* Adds basic `SessionUpdateTimestampHandlerInterface` behaviors to another `SessionHandlerInterface`.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class StrictSessionHandler extends AbstractSessionHandler
{
private $handler;
public function __construct(\SessionHandlerInterface $handler)
{
if ($handler instanceof \SessionUpdateTimestampHandlerInterface) {
throw new \LogicException(sprintf('"%s" is already an instance of "SessionUpdateTimestampHandlerInterface", you cannot wrap it with "%s".', get_class($handler), self::class));
}
$this->handler = $handler;
}
/**
* {@inheritdoc}
*/
public function open($savePath, $sessionName)
{
parent::open($savePath, $sessionName);
return $this->handler->open($savePath, $sessionName);
}
/**
* {@inheritdoc}
*/
protected function doRead($sessionId)
{
return $this->handler->read($sessionId);
}
/**
* {@inheritdoc}
*/
public function updateTimestamp($sessionId, $data)
{
return $this->write($sessionId, $data);
}
/**
* {@inheritdoc}
*/
protected function doWrite($sessionId, $data)
{
return $this->handler->write($sessionId, $data);
}
/**
* {@inheritdoc}
*/
protected function doDestroy($sessionId)
{
return $this->handler->destroy($sessionId);
}
/**
* {@inheritdoc}
*/
public function close()
{
return $this->handler->close();
}
/**
* {@inheritdoc}
*/
public function gc($maxlifetime)
{
return $this->handler->gc($maxlifetime);
}
}

View File

@ -11,10 +11,14 @@
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
@trigger_error(sprintf('The %s class is deprecated since version 3.4 and will be removed in 4.0. Implement `SessionUpdateTimestampHandlerInterface` or extend `AbstractSessionHandler` instead.', WriteCheckSessionHandler::class), E_USER_DEPRECATED);
/**
* Wraps another SessionHandlerInterface to only write the session when it has been modified.
*
* @author Adrien Brault <adrien.brault@gmail.com>
*
* @deprecated since version 3.4, to be removed in 4.0. Implement `SessionUpdateTimestampHandlerInterface` or extend `AbstractSessionHandler` instead.
*/
class WriteCheckSessionHandler implements \SessionHandlerInterface
{

View File

@ -11,8 +11,8 @@
namespace Symfony\Component\HttpFoundation\Session\Storage;
use Symfony\Component\Debug\Exception\ContextErrorException;
use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy;
@ -26,7 +26,7 @@ class NativeSessionStorage implements SessionStorageInterface
/**
* @var SessionBagInterface[]
*/
protected $bags;
protected $bags = array();
/**
* @var bool
@ -100,9 +100,9 @@ class NativeSessionStorage implements SessionStorageInterface
public function __construct(array $options = array(), $handler = null, MetadataBag $metaBag = null)
{
$options += array(
// disable by default because it's managed by HeaderBag (if used)
'cache_limiter' => '',
'cache_limiter' => 'private_no_expire',
'use_cookies' => 1,
'lazy_write' => 1,
);
session_register_shutdown();
@ -217,15 +217,31 @@ class NativeSessionStorage implements SessionStorageInterface
*/
public function save()
{
$session = $_SESSION;
foreach ($this->bags as $bag) {
if (empty($_SESSION[$key = $bag->getStorageKey()])) {
unset($_SESSION[$key]);
}
}
if (array($key = $this->metadataBag->getStorageKey()) === array_keys($_SESSION)) {
unset($_SESSION[$key]);
}
// Register custom error handler to catch a possible failure warning during session write
set_error_handler(function ($errno, $errstr, $errfile, $errline, $errcontext) {
throw new ContextErrorException($errstr, $errno, E_WARNING, $errfile, $errline, $errcontext);
set_error_handler(function ($errno, $errstr, $errfile, $errline) {
throw new \ErrorException($errstr, $errno, E_WARNING, $errfile, $errline);
}, E_WARNING);
try {
$e = null;
session_write_close();
} catch (\ErrorException $e) {
} finally {
restore_error_handler();
} catch (ContextErrorException $e) {
$_SESSION = $session;
}
if (null !== $e) {
// The default PHP error message is not very helpful, as it does not give any information on the current save handler.
// Therefore, we catch this error and trigger a warning with a better error message
$handler = $this->getSaveHandler();
@ -233,7 +249,6 @@ class NativeSessionStorage implements SessionStorageInterface
$handler = $handler->getHandler();
}
restore_error_handler();
trigger_error(sprintf('session_write_close(): Failed to write session data with %s handler', get_class($handler)), E_USER_WARNING);
}
@ -386,13 +401,6 @@ class NativeSessionStorage implements SessionStorageInterface
throw new \InvalidArgumentException('Must be instance of AbstractProxy; implement \SessionHandlerInterface; or be null.');
}
if ($saveHandler instanceof AbstractProxy) {
@trigger_error(
'Using session save handlers that are instances of AbstractProxy is deprecated since version 3.4 and will be removed in 4.0.',
E_USER_DEPRECATED
);
}
if (headers_sent($file, $line)) {
throw new \RuntimeException(sprintf('Failed to set the session handler because headers have already been sent by "%s" at line %d.', $file, $line));
}
@ -401,11 +409,13 @@ class NativeSessionStorage implements SessionStorageInterface
if (!$saveHandler instanceof AbstractProxy && $saveHandler instanceof \SessionHandlerInterface) {
$saveHandler = new SessionHandlerProxy($saveHandler);
} elseif (!$saveHandler instanceof AbstractProxy) {
$saveHandler = new SessionHandlerProxy(new \SessionHandler());
$saveHandler = new SessionHandlerProxy(new StrictSessionHandler(new \SessionHandler()));
}
$this->saveHandler = $saveHandler;
if ($this->saveHandler instanceof \SessionHandlerInterface) {
if ($this->saveHandler instanceof SessionHandlerProxy) {
session_set_save_handler($this->saveHandler->getHandler(), false);
} elseif ($this->saveHandler instanceof \SessionHandlerInterface) {
session_set_save_handler($this->saveHandler, false);
}
}

View File

@ -11,11 +11,7 @@
namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy;
@trigger_error('The '.__NAMESPACE__.'\AbstractProxy class is deprecated since version 3.4 and will be removed in 4.0. Use your session handler implementation directly.', E_USER_DEPRECATED);
/**
* @deprecated since version 3.4, to be removed in 4.0. Use your session handler implementation directly.
*
* @author Drak <drak@zikula.org>
*/
abstract class AbstractProxy

View File

@ -11,11 +11,7 @@
namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy;
@trigger_error('The '.__NAMESPACE__.'\SessionHandlerProxy class is deprecated since version 3.4 and will be removed in 4.0. Use your session handler implementation directly.', E_USER_DEPRECATED);
/**
* @deprecated since version 3.4, to be removed in 4.0. Use your session handler implementation directly.
*
* @author Drak <drak@zikula.org>
*/
class SessionHandlerProxy extends AbstractProxy implements \SessionHandlerInterface

View File

@ -0,0 +1,61 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler;
use PHPUnit\Framework\TestCase;
/**
* @requires PHP 7.0
*/
class AbstractSessionHandlerTest extends TestCase
{
private static $server;
public static function setUpBeforeClass()
{
$spec = array(
1 => array('file', '/dev/null', 'w'),
2 => array('file', '/dev/null', 'w'),
);
if (!self::$server = @proc_open('exec php -S localhost:8053', $spec, $pipes, __DIR__.'/Fixtures')) {
self::markTestSkipped('PHP server unable to start.');
}
sleep(1);
}
public static function tearDownAfterClass()
{
if (self::$server) {
proc_terminate(self::$server);
proc_close(self::$server);
}
}
/**
* @dataProvider provideSession
*/
public function testSession($fixture)
{
$context = array('http' => array('header' => "Cookie: sid=123abc\r\n"));
$context = stream_context_create($context);
$result = file_get_contents(sprintf('http://localhost:8053/%s.php', $fixture), false, $context);
$this->assertStringEqualsFile(__DIR__.sprintf('/Fixtures/%s.expected', $fixture), $result);
}
public function provideSession()
{
foreach (glob(__DIR__.'/Fixtures/*.php') as $file) {
yield array(pathinfo($file, PATHINFO_FILENAME));
}
}
}

View File

@ -0,0 +1,152 @@
<?php
use Symfony\Component\HttpFoundation\Session\Storage\Handler\AbstractSessionHandler;
$parent = __DIR__;
while (!@file_exists($parent.'/vendor/autoload.php')) {
if (!@file_exists($parent)) {
// open_basedir restriction in effect
break;
}
if ($parent === dirname($parent)) {
echo "vendor/autoload.php not found\n";
exit(1);
}
$parent = dirname($parent);
}
require $parent.'/vendor/autoload.php';
error_reporting(-1);
ini_set('html_errors', 0);
ini_set('display_errors', 1);
ini_set('session.gc_probability', 0);
ini_set('session.serialize_handler', 'php');
ini_set('session.cookie_lifetime', 0);
ini_set('session.cookie_domain', '');
ini_set('session.cookie_secure', '');
ini_set('session.cookie_httponly', '');
ini_set('session.use_cookies', 1);
ini_set('session.use_only_cookies', 1);
ini_set('session.cache_expire', 180);
ini_set('session.cookie_path', '/');
ini_set('session.cookie_domain', '');
ini_set('session.cookie_secure', 1);
ini_set('session.cookie_httponly', 1);
ini_set('session.use_strict_mode', 1);
ini_set('session.lazy_write', 1);
ini_set('session.name', 'sid');
ini_set('session.save_path', __DIR__);
ini_set('session.cache_limiter', 'private_no_expire');
header_remove('X-Powered-By');
header('Content-Type: text/plain; charset=utf-8');
register_shutdown_function(function () {
echo "\n";
header_remove('Last-Modified');
session_write_close();
print_r(headers_list());
echo "shutdown\n";
});
ob_start();
class TestSessionHandler extends AbstractSessionHandler
{
private $data;
public function __construct($data = '')
{
$this->data = $data;
}
public function open($path, $name)
{
echo __FUNCTION__, "\n";
return parent::open($path, $name);
}
public function validateId($sessionId)
{
echo __FUNCTION__, "\n";
return parent::validateId($sessionId);
}
/**
* {@inheritdoc}
*/
public function read($sessionId)
{
echo __FUNCTION__, "\n";
return parent::read($sessionId);
}
/**
* {@inheritdoc}
*/
public function updateTimestamp($sessionId, $data)
{
echo __FUNCTION__, "\n";
return true;
}
/**
* {@inheritdoc}
*/
public function write($sessionId, $data)
{
echo __FUNCTION__, "\n";
return parent::write($sessionId, $data);
}
/**
* {@inheritdoc}
*/
public function destroy($sessionId)
{
echo __FUNCTION__, "\n";
return parent::destroy($sessionId);
}
public function close()
{
echo __FUNCTION__, "\n";
return true;
}
public function gc($maxLifetime)
{
echo __FUNCTION__, "\n";
return true;
}
protected function doRead($sessionId)
{
echo __FUNCTION__.': ', $this->data, "\n";
return $this->data;
}
protected function doWrite($sessionId, $data)
{
echo __FUNCTION__.': ', $data, "\n";
return true;
}
protected function doDestroy($sessionId)
{
echo __FUNCTION__, "\n";
return true;
}
}

View File

@ -0,0 +1,17 @@
open
validateId
read
doRead: abc|i:123;
read
write
destroy
doDestroy
close
Array
(
[0] => Content-Type: text/plain; charset=utf-8
[1] => Cache-Control: private, max-age=10800
[2] => Set-Cookie: sid=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly
)
shutdown

View File

@ -0,0 +1,8 @@
<?php
require __DIR__.'/common.inc';
session_set_save_handler(new TestSessionHandler('abc|i:123;'), false);
session_start();
unset($_SESSION['abc']);

View File

@ -0,0 +1,14 @@
open
validateId
read
doRead: abc|i:123;
read
123
updateTimestamp
close
Array
(
[0] => Content-Type: text/plain; charset=utf-8
[1] => Cache-Control: private, max-age=10800
)
shutdown

View File

@ -0,0 +1,8 @@
<?php
require __DIR__.'/common.inc';
session_set_save_handler(new TestSessionHandler('abc|i:123;'), false);
session_start();
echo $_SESSION['abc'];

View File

@ -0,0 +1,24 @@
open
validateId
read
doRead: abc|i:123;
read
destroy
doDestroy
close
open
validateId
read
doRead: abc|i:123;
read
write
doWrite: abc|i:123;
close
Array
(
[0] => Content-Type: text/plain; charset=utf-8
[1] => Cache-Control: private, max-age=10800
[2] => Set-Cookie: sid=random_session_id; path=/; secure; HttpOnly
)
shutdown

View File

@ -0,0 +1,10 @@
<?php
require __DIR__.'/common.inc';
session_set_save_handler(new TestSessionHandler('abc|i:123;'), false);
session_start();
session_regenerate_id(true);
ob_start(function ($buffer) { return str_replace(session_id(), 'random_session_id', $buffer); });

View File

@ -0,0 +1,20 @@
open
validateId
read
doRead:
read
Array
(
[0] => bar
)
$_SESSION is not empty
write
destroy
close
$_SESSION is not empty
Array
(
[0] => Content-Type: text/plain; charset=utf-8
[1] => Cache-Control: private, max-age=10800
)
shutdown

View File

@ -0,0 +1,24 @@
<?php
require __DIR__.'/common.inc';
use Symfony\Component\HttpFoundation\Session\Flash\FlashBag;
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
$storage = new NativeSessionStorage();
$storage->setSaveHandler(new TestSessionHandler());
$flash = new FlashBag();
$storage->registerBag($flash);
$storage->start();
$flash->add('foo', 'bar');
print_r($flash->get('foo'));
echo empty($_SESSION) ? '$_SESSION is empty' : '$_SESSION is not empty';
echo "\n";
$storage->save();
echo empty($_SESSION) ? '$_SESSION is empty' : '$_SESSION is not empty';
ob_start(function ($buffer) { return str_replace(session_id(), 'random_session_id', $buffer); });

View File

@ -0,0 +1,15 @@
open
validateId
read
doRead: abc|i:123;
read
updateTimestamp
close
Array
(
[0] => Content-Type: text/plain; charset=utf-8
[1] => Cache-Control: private, max-age=10800
[2] => Set-Cookie: abc=def
)
shutdown

View File

@ -0,0 +1,8 @@
<?php
require __DIR__.'/common.inc';
session_set_save_handler(new TestSessionHandler('abc|i:123;'), false);
session_start();
setcookie('abc', 'def');

View File

@ -25,6 +25,9 @@ use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandle
*/
class NativeSessionHandlerTest extends TestCase
{
/**
* @expectedDeprecation The Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandler class is deprecated since version 3.4 and will be removed in 4.0. Use the \SessionHandler class instead.
*/
public function testConstruct()
{
$handler = new NativeSessionHandler();

View File

@ -160,6 +160,9 @@ class PdoSessionHandlerTest extends TestCase
if (defined('HHVM_VERSION')) {
$this->markTestSkipped('PHPUnit_MockObject cannot mock the PDOStatement class on HHVM. See https://github.com/sebastianbergmann/phpunit-mock-objects/pull/289');
}
if (ini_get('session.use_strict_mode')) {
$this->markTestSkipped('Strict mode needs no locking for new sessions.');
}
$pdo = new MockPdo('pgsql');
$selectStmt = $this->getMockBuilder('PDOStatement')->getMock();

View File

@ -0,0 +1,189 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\AbstractSessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler;
class StrictSessionHandlerTest extends TestCase
{
public function testOpen()
{
$handler = $this->getMockBuilder('SessionHandlerInterface')->getMock();
$handler->expects($this->once())->method('open')
->with('path', 'name')->willReturn(true);
$proxy = new StrictSessionHandler($handler);
$this->assertInstanceof('SessionUpdateTimestampHandlerInterface', $proxy);
$this->assertInstanceof(AbstractSessionHandler::class, $proxy);
$this->assertTrue($proxy->open('path', 'name'));
}
public function testCloseSession()
{
$handler = $this->getMockBuilder('SessionHandlerInterface')->getMock();
$handler->expects($this->once())->method('close')
->willReturn(true);
$proxy = new StrictSessionHandler($handler);
$this->assertTrue($proxy->close());
}
public function testValidateIdOK()
{
$handler = $this->getMockBuilder('SessionHandlerInterface')->getMock();
$handler->expects($this->once())->method('read')
->with('id')->willReturn('data');
$proxy = new StrictSessionHandler($handler);
$this->assertTrue($proxy->validateId('id'));
}
public function testValidateIdKO()
{
$handler = $this->getMockBuilder('SessionHandlerInterface')->getMock();
$handler->expects($this->once())->method('read')
->with('id')->willReturn('');
$proxy = new StrictSessionHandler($handler);
$this->assertFalse($proxy->validateId('id'));
}
public function testRead()
{
$handler = $this->getMockBuilder('SessionHandlerInterface')->getMock();
$handler->expects($this->once())->method('read')
->with('id')->willReturn('data');
$proxy = new StrictSessionHandler($handler);
$this->assertSame('data', $proxy->read('id'));
}
public function testReadWithValidateIdOK()
{
$handler = $this->getMockBuilder('SessionHandlerInterface')->getMock();
$handler->expects($this->once())->method('read')
->with('id')->willReturn('data');
$proxy = new StrictSessionHandler($handler);
$this->assertTrue($proxy->validateId('id'));
$this->assertSame('data', $proxy->read('id'));
}
public function testReadWithValidateIdMismatch()
{
$handler = $this->getMockBuilder('SessionHandlerInterface')->getMock();
$handler->expects($this->exactly(2))->method('read')
->withConsecutive(array('id1'), array('id2'))
->will($this->onConsecutiveCalls('data1', 'data2'));
$proxy = new StrictSessionHandler($handler);
$this->assertTrue($proxy->validateId('id1'));
$this->assertSame('data2', $proxy->read('id2'));
}
public function testUpdateTimestamp()
{
$handler = $this->getMockBuilder('SessionHandlerInterface')->getMock();
$handler->expects($this->once())->method('write')
->with('id', 'data')->willReturn(true);
$proxy = new StrictSessionHandler($handler);
$this->assertTrue($proxy->updateTimestamp('id', 'data'));
}
public function testWrite()
{
$handler = $this->getMockBuilder('SessionHandlerInterface')->getMock();
$handler->expects($this->once())->method('write')
->with('id', 'data')->willReturn(true);
$proxy = new StrictSessionHandler($handler);
$this->assertTrue($proxy->write('id', 'data'));
}
public function testWriteEmptyNewSession()
{
$handler = $this->getMockBuilder('SessionHandlerInterface')->getMock();
$handler->expects($this->once())->method('read')
->with('id')->willReturn('');
$handler->expects($this->never())->method('write');
$handler->expects($this->never())->method('destroy');
$proxy = new StrictSessionHandler($handler);
$this->assertFalse($proxy->validateId('id'));
$this->assertSame('', $proxy->read('id'));
$this->assertTrue($proxy->write('id', ''));
}
public function testWriteEmptyExistingSession()
{
$handler = $this->getMockBuilder('SessionHandlerInterface')->getMock();
$handler->expects($this->once())->method('read')
->with('id')->willReturn('data');
$handler->expects($this->never())->method('write');
$handler->expects($this->once())->method('destroy')->willReturn(true);
$proxy = new StrictSessionHandler($handler);
$this->assertSame('data', $proxy->read('id'));
$this->assertTrue($proxy->write('id', ''));
}
public function testDestroy()
{
$handler = $this->getMockBuilder('SessionHandlerInterface')->getMock();
$handler->expects($this->once())->method('destroy')
->with('id')->willReturn(true);
$proxy = new StrictSessionHandler($handler);
$this->assertTrue($proxy->destroy('id'));
}
public function testDestroyNewSession()
{
$handler = $this->getMockBuilder('SessionHandlerInterface')->getMock();
$handler->expects($this->once())->method('read')
->with('id')->willReturn('');
$handler->expects($this->never())->method('destroy');
$proxy = new StrictSessionHandler($handler);
$this->assertSame('', $proxy->read('id'));
$this->assertTrue($proxy->destroy('id'));
}
public function testDestroyNonEmptyNewSession()
{
$handler = $this->getMockBuilder('SessionHandlerInterface')->getMock();
$handler->expects($this->once())->method('read')
->with('id')->willReturn('');
$handler->expects($this->once())->method('write')
->with('id', 'data')->willReturn(true);
$handler->expects($this->once())->method('destroy')
->with('id')->willReturn(true);
$proxy = new StrictSessionHandler($handler);
$this->assertSame('', $proxy->read('id'));
$this->assertTrue($proxy->write('id', 'data'));
$this->assertTrue($proxy->destroy('id'));
}
public function testGc()
{
$handler = $this->getMockBuilder('SessionHandlerInterface')->getMock();
$handler->expects($this->once())->method('gc')
->with(123)->willReturn(true);
$proxy = new StrictSessionHandler($handler);
$this->assertTrue($proxy->gc(123));
}
}

View File

@ -16,6 +16,8 @@ use Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHa
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*
* @group legacy
*/
class WriteCheckSessionHandlerTest extends TestCase
{

View File

@ -14,7 +14,7 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBag;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\NullSessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy;
@ -152,7 +152,7 @@ class NativeSessionStorageTest extends TestCase
$this->iniSet('session.cache_limiter', 'nocache');
$storage = new NativeSessionStorage();
$this->assertEquals('', ini_get('session.cache_limiter'));
$this->assertEquals('private_no_expire', ini_get('session.cache_limiter'));
}
public function testExplicitSessionCacheLimiter()
@ -201,9 +201,9 @@ class NativeSessionStorageTest extends TestCase
$this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy', $storage->getSaveHandler());
$storage->setSaveHandler(null);
$this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy', $storage->getSaveHandler());
$storage->setSaveHandler(new SessionHandlerProxy(new NativeSessionHandler()));
$storage->setSaveHandler(new SessionHandlerProxy(new NativeFileSessionHandler()));
$this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy', $storage->getSaveHandler());
$storage->setSaveHandler(new NativeSessionHandler());
$storage->setSaveHandler(new NativeFileSessionHandler());
$this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy', $storage->getSaveHandler());
$storage->setSaveHandler(new SessionHandlerProxy(new NullSessionHandler()));
$this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy', $storage->getSaveHandler());

View File

@ -18,8 +18,6 @@ use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy;
/**
* Test class for AbstractProxy.
*
* @group legacy
*
* @author Drak <drak@zikula.org>
*/
class AbstractProxyTest extends TestCase

View File

@ -21,7 +21,6 @@ use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy;
*
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @group legacy
*/
class SessionHandlerProxyTest extends TestCase
{

View File

@ -17,7 +17,8 @@
],
"require": {
"php": "^5.5.9|>=7.0.8",
"symfony/polyfill-mbstring": "~1.1"
"symfony/polyfill-mbstring": "~1.1",
"symfony/polyfill-php70": "~1.6"
},
"require-dev": {
"symfony/expression-language": "~2.8|~3.0|~4.0"

View File

@ -97,12 +97,18 @@ class NativeSessionTokenStorage implements TokenStorageInterface
$this->startSession();
}
$token = isset($_SESSION[$this->namespace][$tokenId])
? (string) $_SESSION[$this->namespace][$tokenId]
: null;
if (!isset($_SESSION[$this->namespace][$tokenId])) {
return;
}
$token = (string) $_SESSION[$this->namespace][$tokenId];
unset($_SESSION[$this->namespace][$tokenId]);
if (!$_SESSION[$this->namespace]) {
unset($_SESSION[$this->namespace]);
}
return $token;
}