feature #24523 [HttpFoundation] Make sessions secure and lazy (nicolas-grekas)

This PR was merged into the 3.4 branch.

Discussion
----------

[HttpFoundation] Make sessions secure and lazy

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | yes
| Tests pass?   | not yet
| Fixed tickets | #6388, #6036, #12375, #12325
| License       | MIT
| Doc PR        | -

The `SessionUpdateTimestampHandlerInterface` (new to PHP 7.0) is mostly undocumented, and just not implemented anywhere. Yet, it's required to implement session fixation preventions and lazy write in userland session handlers (there is https://wiki.php.net/rfc/session-read_only-lazy_write which describes the behavior.)

By implementing it, we would make Symfony session handling much better and stronger. Meanwhile, doing some cookie headers management, this also gives the opportunity to fix the "don't start if session is only read issue".

So, here we are for the general idea. Now needs more (and green) tests, and review of course.

Commits
-------

347939c9b3 [HttpFoundation] Make sessions secure and lazy
This commit is contained in:
Fabien Potencier 2017-10-16 15:59:43 -07:00
commit 1f4025a0ae
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;
}