[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 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, * The `cache:clear` command doesn't clear "app" PSR-6 cache pools anymore,
but still clears "system" ones. but still clears "system" ones.
Use the `cache:pool:clear` command to clear "app" pools instead. Use the `cache:pool:clear` command to clear "app" pools instead.
@ -235,18 +237,13 @@ HttpFoundation
* The `Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandler` * The `Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandler`
class has been deprecated and will be removed in 4.0. Use the `\SessionHandler` class instead. 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 * The `Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler` class has been
deprecated and will be removed in 4.0. Use your `\SessionHandlerInterface` implementation directly. 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 * The `Symfony\Component\HttpFoundation\Session\Storage\Proxy\NativeProxy` class has been
deprecated and will be removed in 4.0. Use your `\SessionHandlerInterface` implementation directly. 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 * 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. 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 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 `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. * 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 * The ability to check only for cacheable HTTP methods using `Request::isMethodSafe()` is
not supported anymore, use `Request::isMethodCacheable()` instead. not supported anymore, use `Request::isMethodCacheable()` instead.
* The `Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandler`, * The `Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler` class has been
`Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy`, removed. Implement `SessionUpdateTimestampHandlerInterface` or extend `AbstractSessionHandler` instead.
`Symfony\Component\HttpFoundation\Session\Storage\Proxy\NativeProxy` and
`Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy` classes have been removed.
* `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 * 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. 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-intl-icu": "~1.0",
"symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php56": "~1.0", "symfony/polyfill-php56": "~1.0",
"symfony/polyfill-php70": "~1.0", "symfony/polyfill-php70": "~1.6",
"symfony/polyfill-util": "~1.0" "symfony/polyfill-util": "~1.0"
}, },
"replace": { "replace": {

View File

@ -4,6 +4,7 @@ CHANGELOG
3.4.0 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, * 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 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` * 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_divisor')->end()
->scalarNode('gc_probability')->defaultValue(1)->end() ->scalarNode('gc_probability')->defaultValue(1)->end()
->scalarNode('gc_maxlifetime')->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() ->scalarNode('save_path')->defaultValue('%kernel.cache_dir%/sessions')->end()
->integerNode('metadata_update_threshold') ->integerNode('metadata_update_threshold')
->defaultValue('0') ->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() ->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.native')->replaceArgument(1, null);
$container->getDefinition('session.storage.php_bridge')->replaceArgument(0, null); $container->getDefinition('session.storage.php_bridge')->replaceArgument(0, null);
} else { } else {
$handlerId = $config['handler_id']; $container->setAlias('session.handler', $config['handler_id'])->setPrivate(true);
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->setParameter('session.save_path', $config['save_path']); $container->setParameter('session.save_path', $config['save_path']);

View File

@ -48,11 +48,17 @@
<argument type="service" id="session.storage.metadata_bag" /> <argument type="service" id="session.storage.metadata_bag" />
</service> </service>
<service id="session.handler.native_file" class="Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler"> <service id="session.handler.native_file" class="Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler">
<argument>%session.save_path%</argument> <argument type="service">
<service class="Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler">
<argument>%session.save_path%</argument>
</service>
</argument>
</service> </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"> <service id="session_listener" class="Symfony\Component\HttpKernel\EventListener\SessionListener">
<tag name="kernel.event_subscriber" /> <tag name="kernel.event_subscriber" />

View File

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

View File

@ -4,8 +4,9 @@ CHANGELOG
3.4.0 3.4.0
----- -----
* deprecated the `NativeSessionHandler` class, * implemented PHP 7.0's `SessionUpdateTimestampHandlerInterface` with a new
* deprecated the `AbstractProxy`, `NativeProxy` and `SessionHandlerProxy` classes, `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 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 using `MongoDbSessionHandler` with the legacy mongo extension; use it with the mongodb/mongodb package and ext-mongodb instead
* deprecated `MemcacheSessionHandler`; use `MemcachedSessionHandler` 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> * @author Drak <drak@zikula.org>
*/ */
class MemcachedSessionHandler implements \SessionHandlerInterface class MemcachedSessionHandler extends AbstractSessionHandler
{ {
/** /**
* @var \Memcached Memcached driver * @var \Memcached Memcached driver
@ -39,7 +39,7 @@ class MemcachedSessionHandler implements \SessionHandlerInterface
/** /**
* List of available options: * List of available options:
* * prefix: The prefix to use for the memcached keys in order to avoid collision * * 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 \Memcached $memcached A \Memcached instance
* @param array $options An associative array of Memcached options * @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'; $this->prefix = isset($options['prefix']) ? $options['prefix'] : 'sf2s';
} }
/**
* {@inheritdoc}
*/
public function open($savePath, $sessionName)
{
return true;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@ -79,7 +71,7 @@ class MemcachedSessionHandler implements \SessionHandlerInterface
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function read($sessionId) protected function doRead($sessionId)
{ {
return $this->memcached->get($this->prefix.$sessionId) ?: ''; return $this->memcached->get($this->prefix.$sessionId) ?: '';
} }
@ -87,7 +79,15 @@ class MemcachedSessionHandler implements \SessionHandlerInterface
/** /**
* {@inheritdoc} * {@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); return $this->memcached->set($this->prefix.$sessionId, $data, time() + $this->ttl);
} }
@ -95,7 +95,7 @@ class MemcachedSessionHandler implements \SessionHandlerInterface
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function destroy($sessionId) protected function doDestroy($sessionId)
{ {
$result = $this->memcached->delete($this->prefix.$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 https://packagist.org/packages/mongodb/mongodb
* @see http://php.net/manual/en/set.mongodb.php * @see http://php.net/manual/en/set.mongodb.php
*/ */
class MongoDbSessionHandler implements \SessionHandlerInterface class MongoDbSessionHandler extends AbstractSessionHandler
{ {
/** /**
* @var \Mongo|\MongoClient|\MongoDB\Client * @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] * * id_field: The field name for storing the session id [default: _id]
* * data_field: The field name for storing the session data [default: data] * * data_field: The field name for storing the session data [default: data]
* * time_field: The field name for storing the timestamp [default: time] * * 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 * It is strongly recommended to put an index on the `expiry_field` for
* garbage-collection. Alternatively it's possible to automatically expire * garbage-collection. Alternatively it's possible to automatically expire
@ -92,14 +92,6 @@ class MongoDbSessionHandler implements \SessionHandlerInterface
), $options); ), $options);
} }
/**
* {@inheritdoc}
*/
public function open($savePath, $sessionName)
{
return true;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@ -111,7 +103,7 @@ class MongoDbSessionHandler implements \SessionHandlerInterface
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function destroy($sessionId) protected function doDestroy($sessionId)
{ {
$methodName = $this->mongo instanceof \MongoDB\Client ? 'deleteOne' : 'remove'; $methodName = $this->mongo instanceof \MongoDB\Client ? 'deleteOne' : 'remove';
@ -139,7 +131,7 @@ class MongoDbSessionHandler implements \SessionHandlerInterface
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function write($sessionId, $data) protected function doWrite($sessionId, $data)
{ {
$expiry = $this->createDateTime(time() + (int) ini_get('session.gc_maxlifetime')); $expiry = $this->createDateTime(time() + (int) ini_get('session.gc_maxlifetime'));
@ -171,7 +163,34 @@ class MongoDbSessionHandler implements \SessionHandlerInterface
/** /**
* {@inheritdoc} * {@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( $dbData = $this->getCollection()->findOne(array(
$this->options['id_field'] => $sessionId, $this->options['id_field'] => $sessionId,

View File

@ -11,12 +11,14 @@
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; 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. * @deprecated since version 3.4, to be removed in 4.0. Use \SessionHandler instead.
* @see http://php.net/sessionhandler * @see http://php.net/sessionhandler
*/ */
class NativeSessionHandler extends \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> * @author Drak <drak@zikula.org>
*/ */
class NullSessionHandler implements \SessionHandlerInterface class NullSessionHandler extends AbstractSessionHandler
{ {
/**
* {@inheritdoc}
*/
public function open($savePath, $sessionName)
{
return true;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@ -37,15 +29,7 @@ class NullSessionHandler implements \SessionHandlerInterface
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function read($sessionId) public function validateId($sessionId)
{
return '';
}
/**
* {@inheritdoc}
*/
public function write($sessionId, $data)
{ {
return true; return true;
} }
@ -53,7 +37,31 @@ class NullSessionHandler implements \SessionHandlerInterface
/** /**
* {@inheritdoc} * {@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; return true;
} }

View File

@ -38,7 +38,7 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
* @author Michael Williams <michael.williams@funsational.com> * @author Michael Williams <michael.williams@funsational.com>
* @author Tobias Schultze <http://tobion.de> * @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 * 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) public function open($savePath, $sessionName)
{ {
$this->sessionExpired = false;
if (null === $this->pdo) { if (null === $this->pdo) {
$this->connect($this->dsn ?: $savePath); $this->connect($this->dsn ?: $savePath);
} }
return true; return parent::open($savePath, $sessionName);
} }
/** /**
@ -273,7 +275,7 @@ class PdoSessionHandler implements \SessionHandlerInterface
public function read($sessionId) public function read($sessionId)
{ {
try { try {
return $this->doRead($sessionId); return parent::read($sessionId);
} catch (\PDOException $e) { } catch (\PDOException $e) {
$this->rollback(); $this->rollback();
@ -296,7 +298,7 @@ class PdoSessionHandler implements \SessionHandlerInterface
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function destroy($sessionId) protected function doDestroy($sessionId)
{ {
// delete the record associated with this id // delete the record associated with this id
$sql = "DELETE FROM $this->table WHERE $this->idCol = :id"; $sql = "DELETE FROM $this->table WHERE $this->idCol = :id";
@ -317,7 +319,7 @@ class PdoSessionHandler implements \SessionHandlerInterface
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function write($sessionId, $data) protected function doWrite($sessionId, $data)
{ {
$maxlifetime = (int) ini_get('session.gc_maxlifetime'); $maxlifetime = (int) ini_get('session.gc_maxlifetime');
@ -372,6 +374,30 @@ class PdoSessionHandler implements \SessionHandlerInterface
return true; 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} * {@inheritdoc}
*/ */
@ -491,10 +517,8 @@ class PdoSessionHandler implements \SessionHandlerInterface
* *
* @return string The session data * @return string The session data
*/ */
private function doRead($sessionId) protected function doRead($sessionId)
{ {
$this->sessionExpired = false;
if (self::LOCK_ADVISORY === $this->lockMode) { if (self::LOCK_ADVISORY === $this->lockMode) {
$this->unlockStatements[] = $this->doAdvisoryLock($sessionId); $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]; 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 // 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. // until other connections to the session are committed.
try { 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; 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. * Wraps another SessionHandlerInterface to only write the session when it has been modified.
* *
* @author Adrien Brault <adrien.brault@gmail.com> * @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 class WriteCheckSessionHandler implements \SessionHandlerInterface
{ {

View File

@ -11,8 +11,8 @@
namespace Symfony\Component\HttpFoundation\Session\Storage; namespace Symfony\Component\HttpFoundation\Session\Storage;
use Symfony\Component\Debug\Exception\ContextErrorException;
use Symfony\Component\HttpFoundation\Session\SessionBagInterface; 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\AbstractProxy;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy;
@ -26,7 +26,7 @@ class NativeSessionStorage implements SessionStorageInterface
/** /**
* @var SessionBagInterface[] * @var SessionBagInterface[]
*/ */
protected $bags; protected $bags = array();
/** /**
* @var bool * @var bool
@ -100,9 +100,9 @@ class NativeSessionStorage implements SessionStorageInterface
public function __construct(array $options = array(), $handler = null, MetadataBag $metaBag = null) public function __construct(array $options = array(), $handler = null, MetadataBag $metaBag = null)
{ {
$options += array( $options += array(
// disable by default because it's managed by HeaderBag (if used) 'cache_limiter' => 'private_no_expire',
'cache_limiter' => '',
'use_cookies' => 1, 'use_cookies' => 1,
'lazy_write' => 1,
); );
session_register_shutdown(); session_register_shutdown();
@ -217,15 +217,31 @@ class NativeSessionStorage implements SessionStorageInterface
*/ */
public function save() 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 // Register custom error handler to catch a possible failure warning during session write
set_error_handler(function ($errno, $errstr, $errfile, $errline, $errcontext) { set_error_handler(function ($errno, $errstr, $errfile, $errline) {
throw new ContextErrorException($errstr, $errno, E_WARNING, $errfile, $errline, $errcontext); throw new \ErrorException($errstr, $errno, E_WARNING, $errfile, $errline);
}, E_WARNING); }, E_WARNING);
try { try {
$e = null;
session_write_close(); session_write_close();
} catch (\ErrorException $e) {
} finally {
restore_error_handler(); 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. // 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 // Therefore, we catch this error and trigger a warning with a better error message
$handler = $this->getSaveHandler(); $handler = $this->getSaveHandler();
@ -233,7 +249,6 @@ class NativeSessionStorage implements SessionStorageInterface
$handler = $handler->getHandler(); $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); 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.'); 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)) { 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)); 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) { if (!$saveHandler instanceof AbstractProxy && $saveHandler instanceof \SessionHandlerInterface) {
$saveHandler = new SessionHandlerProxy($saveHandler); $saveHandler = new SessionHandlerProxy($saveHandler);
} elseif (!$saveHandler instanceof AbstractProxy) { } elseif (!$saveHandler instanceof AbstractProxy) {
$saveHandler = new SessionHandlerProxy(new \SessionHandler()); $saveHandler = new SessionHandlerProxy(new StrictSessionHandler(new \SessionHandler()));
} }
$this->saveHandler = $saveHandler; $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); session_set_save_handler($this->saveHandler, false);
} }
} }

View File

@ -11,11 +11,7 @@
namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy; 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> * @author Drak <drak@zikula.org>
*/ */
abstract class AbstractProxy abstract class AbstractProxy

View File

@ -11,11 +11,7 @@
namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy; 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> * @author Drak <drak@zikula.org>
*/ */
class SessionHandlerProxy extends AbstractProxy implements \SessionHandlerInterface 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 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() public function testConstruct()
{ {
$handler = new NativeSessionHandler(); $handler = new NativeSessionHandler();

View File

@ -160,6 +160,9 @@ class PdoSessionHandlerTest extends TestCase
if (defined('HHVM_VERSION')) { 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'); $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'); $pdo = new MockPdo('pgsql');
$selectStmt = $this->getMockBuilder('PDOStatement')->getMock(); $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> * @author Adrien Brault <adrien.brault@gmail.com>
*
* @group legacy
*/ */
class WriteCheckSessionHandlerTest extends TestCase class WriteCheckSessionHandlerTest extends TestCase
{ {

View File

@ -14,7 +14,7 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag; use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; 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\Handler\NullSessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy;
@ -152,7 +152,7 @@ class NativeSessionStorageTest extends TestCase
$this->iniSet('session.cache_limiter', 'nocache'); $this->iniSet('session.cache_limiter', 'nocache');
$storage = new NativeSessionStorage(); $storage = new NativeSessionStorage();
$this->assertEquals('', ini_get('session.cache_limiter')); $this->assertEquals('private_no_expire', ini_get('session.cache_limiter'));
} }
public function testExplicitSessionCacheLimiter() public function testExplicitSessionCacheLimiter()
@ -201,9 +201,9 @@ class NativeSessionStorageTest extends TestCase
$this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy', $storage->getSaveHandler()); $this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy', $storage->getSaveHandler());
$storage->setSaveHandler(null); $storage->setSaveHandler(null);
$this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy', $storage->getSaveHandler()); $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()); $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()); $this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy', $storage->getSaveHandler());
$storage->setSaveHandler(new SessionHandlerProxy(new NullSessionHandler())); $storage->setSaveHandler(new SessionHandlerProxy(new NullSessionHandler()));
$this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy', $storage->getSaveHandler()); $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. * Test class for AbstractProxy.
* *
* @group legacy
*
* @author Drak <drak@zikula.org> * @author Drak <drak@zikula.org>
*/ */
class AbstractProxyTest extends TestCase class AbstractProxyTest extends TestCase

View File

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

View File

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

View File

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