merged branch drak/sessionmeta (PR #3718)

Commits
-------

8a0e6d2 [HttpFoundation] Update changelog.
4fc04fa [HttpFoundation] Renamed MetaBag to MetadataBag
2f03b31 [HttpFoundation] Added the ability to change the session cookie lifetime on migrate().
39141e8 [HttpFoundation] Add ability to force the lifetime (allows update of session cookie expiry-time)
ec3f88f [HttpFoundation] Add methods to interface
402254c [HttpFoundation] Changed meta-data responsibility to SessionStorageInterface
d9fd14f [HttpFoundation] Refactored for moved tests location.
29bd787 [HttpFoundation] Added some basic meta-data to Session

Discussion
----------

[2.1][HttpFoundation] Added some basic meta-data to Session

Bug fix: no
Feature addition: yes
Backwards compatibility break: no
Symfony2 tests pass: yes
References the following tickets: #2171
Todo: -

Session data is stored as an encoded string against a single id.  If we want to store meta-data about the session, that data has to be stored as part of the session data to ensure the meta-data can persist using any session save handler.

This patch makes it much easier to determine the logic of session expiration.  In general a session expiry can be dealt with by the gc handlers, however, in some applications more specific expiry rules might be required.

Session expiry may also be more complex than a simple, session was idle for x seconds.  For example, in Zikula there are three security settings, Low, Medium and High.  The rules for session expiry are more complex as under the Medium setting, a session will expire after x minutes idle time, unless the rememberme option was ticked on login.  If so, the session will not idle.  This gives the user some control over their experience.  Under the high security setting, then there is no option, sessions will expire after the idle time is reached and login the UI has the rememberme checkbox removed.

The other advantage is that under this methodology, there can be a UI experience on expiry, like "Sorry, your session expired due to being idle for 10 minutes".

Keeping in the spirit of Symfony2 Components, I am seeking to make session handling flexible enough to accommodate these general requirements without specifically covering expiration rules. It would mean that it would be up to the implementing application to specifcally check and expire session after starting it.

Expiration might look something like this:

    $session->start();
    if (time() - $session->getMetadataBag()->getLastUpdate() > $maxIdleTime) {
        $session->invalidate();
        throw new SessionExpired();
    }

This commit also brings the ability to change the `cookie_lifetime` when migrating a session. This means one could move from a default of browser only session cookie to long-lived cookie when changing from a anonymous to a logged in user for example.

    $session->migrate($destroy, $lifetime);

---------------------------------------------------------------------------

by drak at 2012-03-30T18:18:43Z

@fabpot I have removed [WIP] status.

---------------------------------------------------------------------------

by drak at 2012-03-31T13:34:57Z

NB: This PR has been rebased and the tests relocated as per recent master changes.

---------------------------------------------------------------------------

by drak at 2012-04-03T02:16:43Z

@fabpot - ping
This commit is contained in:
Fabien Potencier 2012-04-03 11:40:07 +02:00
commit b9de0be349
10 changed files with 450 additions and 59 deletions

View File

@ -320,6 +320,9 @@ To get the diff between two versions, go to https://github.com/symfony/symfony/c
* Flash API can stores messages in an array so there may be multiple messages
per flash type. The old `Session` class API remains without BC break as it
will single messages as before.
* Added basic session meta-data to the session to record session create time,
last updated time, and the lifetime of the session cookie that was provided
to the client.
### HttpKernel

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\HttpFoundation\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag;
use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface;
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag;
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface;
@ -57,13 +58,13 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
{
$this->storage = $storage ?: new NativeSessionStorage();
$attributeBag = $attributes ?: new AttributeBag();
$this->attributeName = $attributeBag->getName();
$this->registerBag($attributeBag);
$attributes = $attributes ?: new AttributeBag();
$this->attributeName = $attributes->getName();
$this->registerBag($attributes);
$flashBag = $flashes ?: new FlashBag();
$this->flashName = $flashBag->getName();
$this->registerBag($flashBag);
$flashes = $flashes ?: new FlashBag();
$this->flashName = $flashes->getName();
$this->registerBag($flashes);
}
/**
@ -131,21 +132,41 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
}
/**
* {@inheritdoc}
* Returns an iterator for attributes.
*
* @return \ArrayIterator An \ArrayIterator instance
*/
public function invalidate()
public function getIterator()
{
$this->storage->clear();
return new \ArrayIterator($this->storage->getBag($this->attributeName)->all());
}
return $this->storage->regenerate(true);
/**
* Returns the number of attributes.
*
* @return int The number of attributes
*/
public function count()
{
return count($this->storage->getBag($this->attributeName)->all());
}
/**
* {@inheritdoc}
*/
public function migrate($destroy = false)
public function invalidate($lifetime = null)
{
return $this->storage->regenerate($destroy);
$this->storage->clear();
return $this->migrate(true, $lifetime);
}
/**
* {@inheritdoc}
*/
public function migrate($destroy = false, $lifetime = null)
{
return $this->storage->regenerate($destroy, $lifetime);
}
/**
@ -189,9 +210,15 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
}
/**
* Registers a SessionBagInterface with the session.
*
* @param SessionBagInterface $bag
* {@iheritdoc}
*/
public function getMetadataBag()
{
return $this->storage->getMetadataBag();
}
/**
* {@iheritdoc}
*/
public function registerBag(SessionBagInterface $bag)
{
@ -199,11 +226,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
}
/**
* Get's a bag instance.
*
* @param string $name
*
* @return SessionBagInterface
* {@iheritdoc}
*/
public function getBag($name)
{
@ -310,24 +333,4 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
{
return $this->getBag($this->flashName)->clear();
}
/**
* Returns an iterator for attributes.
*
* @return \ArrayIterator An \ArrayIterator instance
*/
public function getIterator()
{
return new \ArrayIterator($this->storage->getBag('attributes')->all());
}
/**
* Returns the number of attributes.
*
* @return int The number of attributes
*/
public function count()
{
return count($this->storage->getBag('attributes')->all());
}
}

View File

@ -71,23 +71,32 @@ interface SessionInterface
* Clears all session attributes and flashes and regenerates the
* session and deletes the old session from persistence.
*
* @param integer $lifetime Sets the cookie lifetime for the session cookie. A null value
* will leave the system settings unchanged, 0 sets the cookie
* to expire with browser session. Time is in seconds, and is
* not a Unix timestamp.
*
* @return Boolean True if session invalidated, false if error.
*
* @api
*/
function invalidate();
function invalidate($lifetime = null);
/**
* Migrates the current session to a new session id while maintaining all
* session attributes.
*
* @param Boolean $destroy Whether to delete the old session or leave it to garbage collection.
* @param Boolean $destroy Whether to delete the old session or leave it to garbage collection.
* @param integer $lifetime Sets the cookie lifetime for the session cookie. A null value
* will leave the system settings unchanged, 0 sets the cookie
* to expire with browser session. Time is in seconds, and is
* not a Unix timestamp.
*
* @return Boolean True if session migrated, false if error.
*
* @api
*/
function migrate($destroy = false);
function migrate($destroy = false, $lifetime = null);
/**
* Force the session to be saved and closed.
@ -164,4 +173,27 @@ interface SessionInterface
* @api
*/
function clear();
/**
* Registers a SessionBagInterface with the session.
*
* @param SessionBagInterface $bag
*/
public function registerBag(SessionBagInterface $bag);
/**
* Gets a bag instance by name.
*
* @param string $name
*
* @return SessionBagInterface
*/
public function getBag($name);
/**
* Gets session meta.
*
* @return MetadataBag
*/
public function getMetadataBag();
}

View File

@ -0,0 +1,160 @@
<?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;
use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
/**
* Metadata container.
*
* Adds metadata to the session.
*
* @author Drak <drak@zikula.org>
*/
class MetadataBag implements SessionBagInterface
{
const CREATED = 'c';
const UPDATED = 'u';
const LIFETIME = 'l';
/**
* @var string
*/
private $name = '__metadata';
/**
* @var string
*/
private $storageKey;
/**
* @var array
*/
protected $meta = array();
/**
* Unix timestamp.
*
* @var integer
*/
private $lastUsed;
/**
* Constructor.
*
* @param string $storageKey The key used to store bag in the session.
*/
public function __construct($storageKey = '_sf2_meta')
{
$this->storageKey = $storageKey;
$this->meta = array(self::CREATED => 0, self::UPDATED => 0, self::LIFETIME => 0);
}
/**
* {@inheritdoc}
*/
public function initialize(array &$array)
{
$this->meta = &$array;
if (isset($array[self::CREATED])) {
$this->lastUsed = $this->meta[self::UPDATED];
$this->meta[self::UPDATED] = time();
} else {
$this->stampCreated();
}
}
/**
* Gets the lifetime that the session cookie was set with.
*
* @return integer
*/
public function getLifetime()
{
return $this->meta[self::LIFETIME];
}
/**
* Stamps a new session's metadata.
*
* @param integer $lifetime Sets the cookie lifetime for the session cookie. A null value
* will leave the system settings unchanged, 0 sets the cookie
* to expire with browser session. Time is in seconds, and is
* not a Unix timestamp.
*/
public function stampNew($lifetime = null)
{
$this->stampCreated($lifetime);
}
/**
* {@inheritdoc}
*/
public function getStorageKey()
{
return $this->storageKey;
}
/**
* Gets the created timestamp metadata.
*
* @return integer Unix timestamp
*/
public function getCreated()
{
return $this->meta[self::CREATED];
}
/**
* Gets the last used metadata.
*
* @return integer Unix timestamp
*/
public function getLastUsed()
{
return $this->lastUsed;
}
/**
* {@inheritdoc}
*/
public function clear()
{
// nothing to do
}
/**
* {@inheritdoc}
*/
public function getName()
{
return $this->name;
}
/**
* Sets name.
*
* @param string $name
*/
public function setName($name)
{
$this->name = $name;
}
private function stampCreated($lifetime = null)
{
$timeStamp = time();
$this->meta[self::CREATED] = $this->meta[self::UPDATED] = $this->lastUsed = $timeStamp;
$this->meta[self::LIFETIME] = (null === $lifetime) ? ini_get('session.cookie_lifetime') : $lifetime;
}
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\HttpFoundation\Session\Storage;
use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag;
/**
* MockArraySessionStorage mocks the session for unit tests.
@ -52,14 +53,21 @@ class MockArraySessionStorage implements SessionStorageInterface
*/
protected $data = array();
/**
* @var MetadataBag
*/
protected $metadataBag;
/**
* Constructor.
*
* @param string $name Session name
* @param string $name Session name
* @param MetadataBag $metaBag MetadataBag instance.
*/
public function __construct($name = 'MOCKSESSID')
public function __construct($name = 'MOCKSESSID', MetadataBag $metaBag = null)
{
$this->name = $name;
$this->setMetadataBag($metaBag);
}
/**
@ -90,16 +98,16 @@ class MockArraySessionStorage implements SessionStorageInterface
return true;
}
/**
* {@inheritdoc}
*/
public function regenerate($destroy = false)
public function regenerate($destroy = false, $lifetime = null)
{
if (!$this->started) {
$this->start();
}
$this->metadataBag->stampNew($lifetime);
$this->id = $this->generateId();
return true;
@ -191,6 +199,30 @@ class MockArraySessionStorage implements SessionStorageInterface
return $this->bags[$name];
}
/**
* Sets the MetadataBag.
*
* @param MetadataBag $bag
*/
public function setMetadataBag(MetadataBag $bag = null)
{
if (null === $bag) {
$bag = new MetadataBag();
}
$this->metadataBag = $bag;
}
/**
* Gets the MetadataBag.
*
* @return MetadataBag
*/
public function getMetadataBag()
{
return $this->metadataBag;
}
/**
* Generates a session ID.
*
@ -206,7 +238,9 @@ class MockArraySessionStorage implements SessionStorageInterface
protected function loadSession()
{
foreach ($this->bags as $bag) {
$bags = array_merge($this->bags, array($this->metadataBag));
foreach ($bags as $bag) {
$key = $bag->getStorageKey();
$this->data[$key] = isset($this->data[$key]) ? $this->data[$key] : array();
$bag->initialize($this->data[$key]);

View File

@ -73,15 +73,17 @@ class MockFileSessionStorage extends MockArraySessionStorage
/**
* {@inheritdoc}
*/
public function regenerate($destroy = false)
public function regenerate($destroy = false, $lifetime = null)
{
if (!$this->started) {
$this->start();
}
if ($destroy) {
$this->destroy();
}
$this->id = $this->generateId();
return true;
return parent::regenerate($destroy, $lifetime);
}
/**

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\HttpFoundation\Session\Storage;
use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\NativeProxy;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy;
@ -45,6 +46,11 @@ class NativeSessionStorage implements SessionStorageInterface
*/
protected $saveHandler;
/**
* @var MetadataBag
*/
protected $metadataBag;
/**
* Constructor.
*
@ -83,10 +89,11 @@ class NativeSessionStorage implements SessionStorageInterface
* upload_progress.min-freq, "1"
* url_rewriter.tags, "a=href,area=href,frame=src,form=,fieldset="
*
* @param array $options Session configuration options.
* @param object $handler SessionHandlerInterface.
* @param array $options Session configuration options.
* @param object $handler SessionHandlerInterface.
* @param MetadataBag $handler MetadataBag.
*/
public function __construct(array $options = array(), $handler = null)
public function __construct(array $options = array(), $handler = null, MetadataBag $metaBag = null)
{
// sensible defaults
ini_set('session.auto_start', 0); // by default we prefer to explicitly start the session using the class.
@ -99,6 +106,7 @@ class NativeSessionStorage implements SessionStorageInterface
register_shutdown_function('session_write_close');
}
$this->setMetadataBag($metaBag);
$this->setOptions($options);
$this->setSaveHandler($handler);
}
@ -187,8 +195,16 @@ class NativeSessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function regenerate($destroy = false)
public function regenerate($destroy = false, $lifetime = null)
{
if (null !== $lifetime) {
ini_set('session.cookie_lifetime', $lifetime);
}
if ($destroy) {
$this->metadataBag->stampNew();
}
return session_regenerate_id($destroy);
}
@ -249,6 +265,30 @@ class NativeSessionStorage implements SessionStorageInterface
return $this->bags[$name];
}
/**
* Sets the MetadataBag.
*
* @param MetadataBag $metaBag
*/
public function setMetadataBag(MetadataBag $metaBag = null)
{
if (null === $metaBag) {
$metaBag = new MetadataBag();
}
$this->metadataBag = $metaBag;
}
/**
* Gets the MetadataBag.
*
* @return MetadataBag
*/
public function getMetadataBag()
{
return $this->metadataBag;
}
/**
* Sets session.* ini variables.
*
@ -338,7 +378,9 @@ class NativeSessionStorage implements SessionStorageInterface
$session = &$_SESSION;
}
foreach ($this->bags as $bag) {
$bags = array_merge($this->bags, array($this->metadataBag));
foreach ($bags as $bag) {
$key = $bag->getStorageKey();
$session[$key] = isset($session[$key]) ? $session[$key] : array();
$bag->initialize($session[$key]);

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\HttpFoundation\Session\Storage;
use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag;
/**
* StorageInterface.
@ -81,7 +82,11 @@ interface SessionStorageInterface
* Note regenerate+destroy should not clear the session data in memory
* only delete the session data from persistent storage.
*
* @param Boolean $destroy Destroy session when regenerating?
* @param Boolean $destroy Destroy session when regenerating?
* @param integer $lifetime Sets the cookie lifetime for the session cookie. A null value
* will leave the system settings unchanged, 0 sets the cookie
* to expire with browser session. Time is in seconds, and is
* not a Unix timestamp.
*
* @return Boolean True if session regenerated, false if error
*
@ -89,7 +94,7 @@ interface SessionStorageInterface
*
* @api
*/
function regenerate($destroy = false);
function regenerate($destroy = false, $lifetime = null);
/**
* Force the session to be saved and closed.
@ -123,4 +128,9 @@ interface SessionStorageInterface
* @param SessionBagInterface $bag
*/
function registerBag(SessionBagInterface $bag);
/**
* @return MetadataBag
*/
function getMetadataBag();
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\HttpFoundation\Tests\Session;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\MetadataBag;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBag;
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
@ -253,4 +254,9 @@ class SessionTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(2, count($this->session));
}
public function testGetMeta()
{
$this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\MetadataBag', $this->session->getMetadataBag());
}
}

View File

@ -0,0 +1,99 @@
<?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;
use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag;
/**
* Test class for MetadataBag.
*/
class MetadataBagTest extends \PHPUnit_Framework_TestCase
{
/**
* @var MetadataBag
*/
protected $bag;
/**
* @var array
*/
protected $array = array();
protected function setUp()
{
$this->bag = new MetadataBag();
$this->array = array(MetadataBag::CREATED => 1234567, MetadataBag::UPDATED => 12345678, MetadataBag::LIFETIME => 0);
$this->bag->initialize($this->array);
}
protected function tearDown()
{
$this->array = array();
$this->bag = null;
}
public function testInitialize()
{
$p = new \ReflectionProperty('Symfony\Component\HttpFoundation\Session\Storage\MetadataBag', 'meta');
$p->setAccessible(true);
$bag1 = new MetadataBag();
$array = array();
$bag1->initialize($array);
$this->assertGreaterThanOrEqual(time(), $bag1->getCreated());
$this->assertEquals($bag1->getCreated(), $bag1->getLastUsed());
sleep(1);
$bag2 = new MetadataBag();
$array2 = $p->getValue($bag1);
$bag2->initialize($array2);
$this->assertEquals($bag1->getCreated(), $bag2->getCreated());
$this->assertEquals($bag1->getLastUsed(), $bag2->getLastUsed());
$this->assertEquals($bag2->getCreated(), $bag2->getLastUsed());
sleep(1);
$bag3 = new MetadataBag();
$array3 = $p->getValue($bag2);
$bag3->initialize($array3);
$this->assertEquals($bag1->getCreated(), $bag3->getCreated());
$this->assertGreaterThan($bag2->getLastUsed(), $bag3->getLastUsed());
$this->assertNotEquals($bag3->getCreated(), $bag3->getLastUsed());
}
public function testGetSetName()
{
$this->assertEquals('__metadata', $this->bag->getName());
$this->bag->setName('foo');
$this->assertEquals('foo', $this->bag->getName());
}
public function testGetStorageKey()
{
$this->assertEquals('_sf2_meta', $this->bag->getStorageKey());
}
public function testGetCreated()
{
$this->assertEquals(1234567, $this->bag->getCreated());
}
public function testGetLastUsed()
{
$this->assertLessThanOrEqual(time(), $this->bag->getLastUsed());
}
public function testClear()
{
$this->bag->clear();
}
}