Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[HttpFoundation] MongoDbSessionHandler::read() now checks for valid session age #12516

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,24 @@ 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]
*
* It is strongly recommended to put an index on the `expiry_field` for
* garbage-collection. Alternatively it's possible to automatically expire
* the sessions in the database as described below:
*
* A TTL collections can be used on MongoDB 2.2+ to cleanup expired sessions
* automatically. Such an index can for example look like this:
*
* db.<session-collection>.ensureIndex(
* { "<expiry-field>": 1 },
* { "expireAfterSeconds": 0 }
* )
*
* More details on: http://docs.mongodb.org/manual/tutorial/expire-data/
*
* If you use such an index, you can drop `gc_probability` to 0 since
* no garbage-collection is required.
*
* @param \Mongo|\MongoClient $mongo A MongoClient or Mongo instance
* @param array $options An associative array of field options
Expand All @@ -65,7 +83,7 @@ public function __construct($mongo, array $options)
'id_field' => '_id',
'data_field' => 'data',
'time_field' => 'time',
'expiry_field' => false,
'expiry_field' => 'expires_at',
), $options);
}

Expand Down Expand Up @@ -102,21 +120,8 @@ public function destroy($sessionId)
*/
public function gc($maxlifetime)
{
/* Note: MongoDB 2.2+ supports TTL collections, which may be used in
* place of this method by indexing the "time_field" field with an
* "expireAfterSeconds" option. Regardless of whether TTL collections
* are used, consider indexing this field to make the remove query more
* efficient.
*
* See: http://docs.mongodb.org/manual/tutorial/expire-data/
*/
if (false !== $this->options['expiry_field']) {
return true;
}
$time = new \MongoDate(time() - $maxlifetime);

$this->getCollection()->remove(array(
$this->options['time_field'] => array('$lt' => $time),
$this->options['expiry_field'] => array('$lt' => new \MongoDate()),
));

return true;
Expand All @@ -127,24 +132,14 @@ public function gc($maxlifetime)
*/
public function write($sessionId, $data)
{
$expiry = new \MongoDate(time() + (int) ini_get('session.gc_maxlifetime'));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should try to inject time() also. correct me if i am wrong, but this assumes that your webserver and your database server are sharing the same timezome?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's been this way. I think this should be a separate issue?

Edit: MongoDate is not timezone aware anyway. So this is a non-issue. It's always based on PHP's current default-timezone. It's the driver's responsibility to "translate"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction, it's based on the timezone of PHP's execution context, see: https://gist.github.com/bzikarsky/c309a55fefbaf9bdaa5d

Anyhow: ext-mongo uses the system's timezone to create correct Isodate-instances in the database. The same is valid for reading.

Ignoring PHP's timezone is probably a bug. But that is located in ext-mongo.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i looked at the other session handlers, this issue is related to most of them. to solve this issue symfony should provide a service that is able to return date / time informations.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignoring PHP's timezone is probably a bug. But that is located in ext-mongo.

I would disagree. The BSON date type is specified as:

UTC datetime - The int64 is UTC milliseconds since the Unix epoch.

In your gist, strtotime() happens to respect PHP's timezone, as does date('c') when you print out the dates later; however, when printing the date on the way out of the database, you're ignoring the original timezone from the tz field. PHP will simply use the UNIX timestamp from the MongoDate and render it in the default timezone. This is why the time offsets of the three records are still staggered (0, +2, and +1 respectively). Does that make sense?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to avoid any more confusion: I'm fine with the output of my exmaple-script. This is what I expected. My comment about there being a bug in ext-mongo was related to me falling back to the usage of TZ=/usr/share/zoneinfo.... But I must have tricked myself somewhere.

I was under the impression that ext-mongo converts the UNIX timestamps to Isodates (in UTC) based on the TZ process context, and ignores PHP's internal date.timezone. That's the reason why I went for a bash-script instead of a PHP script in the beginning.

But I ran a 2nd test today morning, and this time it worked flawlessly. Strange. Modified Gist at https://gist.github.com/bzikarsky/4044d1fa37e32e5c6996

Anyhow I think everything is fine - no bugs anywhere to be seen. 😉


$fields = array(
$this->options['data_field'] => new \MongoBinData($data, \MongoBinData::BYTE_ARRAY),
$this->options['time_field'] => new \MongoDate(),
$this->options['expiry_field'] => $expiry,
);

/* Note: As discussed in the gc method of this class. You can utilise
* TTL collections in MongoDB 2.2+
* We are setting the "expiry_field" as part of the write operation here
* You will need to create the index on your collection that expires documents
* at that time
* e.g.
* db.MySessionCollection.ensureIndex( { "expireAt": 1 }, { expireAfterSeconds: 0 } )
*/
if (false !== $this->options['expiry_field']) {
$expiry = new \MongoDate(time() + (int) ini_get('session.gc_maxlifetime'));
$fields[$this->options['expiry_field']] = $expiry;
}

$this->getCollection()->update(
array($this->options['id_field'] => $sessionId),
array('$set' => $fields),
Expand All @@ -160,7 +155,8 @@ public function write($sessionId, $data)
public function read($sessionId)
{
$dbData = $this->getCollection()->findOne(array(
$this->options['id_field'] => $sessionId,
$this->options['id_field'] => $sessionId,
$this->options['expiry_field'] => array('$gte' => new \MongoDate()),
));

return null === $dbData ? '' : $dbData[$this->options['data_field']]->bin;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ protected function setUp()
'id_field' => '_id',
'data_field' => 'data',
'time_field' => 'time',
'expiry_field' => 'expires_at',
'database' => 'sf2-test',
'collection' => 'session-test',
);
Expand Down Expand Up @@ -73,7 +74,7 @@ public function testCloseMethodAlwaysReturnTrue()
$this->assertTrue($this->storage->close(), 'The "close" method should always return true');
}

public function testWrite()
public function testRead()
{
$collection = $this->createMongoCollectionMock();

Expand All @@ -83,36 +84,34 @@ public function testWrite()
->will($this->returnValue($collection));

$that = $this;
$data = array();

$collection->expects($this->once())
->method('update')
->will($this->returnCallback(function ($criteria, $updateData, $options) use ($that, &$data) {
$that->assertEquals(array($that->options['id_field'] => 'foo'), $criteria);
$that->assertEquals(array('upsert' => true, 'multiple' => false), $options);
// defining the timeout before the actual method call
// allows to test for "greater than" values in the $criteria
$testTimeout = time();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just one more reason to "mock" it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests don't talk to the actual database-layer. So everything is relative to PHP's timezone, see above.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

time() + (int) ini_get('session.gc_maxlifetime') is just untestable. but this is related to a bunch of handlers so it shouldn't keep us off to merge the PR.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, this is caused by PHP's not-very-elegant session API. The only proper way around this would be a custom session system, ignoring both native API and configuration for the session-module.


$data = $updateData['$set'];
$collection->expects($this->once())
->method('findOne')
->will($this->returnCallback(function ($criteria) use ($that, $testTimeout) {
$that->assertArrayHasKey($that->options['id_field'], $criteria);
$that->assertEquals($criteria[$that->options['id_field']], 'foo');

$that->assertArrayHasKey($that->options['expiry_field'], $criteria);
$that->assertArrayHasKey('$gte', $criteria[$that->options['expiry_field']]);
$that->assertInstanceOf('MongoDate', $criteria[$that->options['expiry_field']]['$gte']);
$that->assertGreaterThanOrEqual($criteria[$that->options['expiry_field']]['$gte']->sec, $testTimeout);

return array(
$that->options['id_field'] => 'foo',
$that->options['data_field'] => new \MongoBinData('bar', \MongoBinData::BYTE_ARRAY),
$that->options['id_field'] => new \MongoDate(),
);
}));

$this->assertTrue($this->storage->write('foo', 'bar'));

$this->assertEquals('bar', $data[$this->options['data_field']]->bin);
$that->assertInstanceOf('MongoDate', $data[$this->options['time_field']]);
$this->assertEquals('bar', $this->storage->read('foo'));
}

public function testWriteWhenUsingExpiresField()
public function testWrite()
{
$this->options = array(
'id_field' => '_id',
'data_field' => 'data',
'time_field' => 'time',
'database' => 'sf2-test',
'collection' => 'session-test',
'expiry_field' => 'expiresAt',
);

$this->storage = new MongoDbSessionHandler($this->mongo, $this->options);

$collection = $this->createMongoCollectionMock();

$this->mongo->expects($this->once())
Expand All @@ -132,11 +131,13 @@ public function testWriteWhenUsingExpiresField()
$data = $updateData['$set'];
}));

$expectedExpiry = time() + (int) ini_get('session.gc_maxlifetime');
$this->assertTrue($this->storage->write('foo', 'bar'));

$this->assertEquals('bar', $data[$this->options['data_field']]->bin);
$that->assertInstanceOf('MongoDate', $data[$this->options['time_field']]);
$that->assertInstanceOf('MongoDate', $data[$this->options['expiry_field']]);
$this->assertInstanceOf('MongoDate', $data[$this->options['expiry_field']]);
$this->assertGreaterThanOrEqual($expectedExpiry, $data[$this->options['expiry_field']]->sec);
}

public function testReplaceSessionData()
Expand Down Expand Up @@ -192,39 +193,13 @@ public function testGc()
$collection->expects($this->once())
->method('remove')
->will($this->returnCallback(function ($criteria) use ($that) {
$that->assertInstanceOf('MongoDate', $criteria[$that->options['time_field']]['$lt']);
$that->assertGreaterThanOrEqual(time() - 1, $criteria[$that->options['time_field']]['$lt']->sec);
$that->assertInstanceOf('MongoDate', $criteria[$that->options['expiry_field']]['$lt']);
$that->assertGreaterThanOrEqual(time() - 1, $criteria[$that->options['expiry_field']]['$lt']->sec);
}));

$this->assertTrue($this->storage->gc(1));
}

public function testGcWhenUsingExpiresField()
{
$this->options = array(
'id_field' => '_id',
'data_field' => 'data',
'time_field' => 'time',
'database' => 'sf2-test',
'collection' => 'session-test',
'expiry_field' => 'expiresAt',
);

$this->storage = new MongoDbSessionHandler($this->mongo, $this->options);

$collection = $this->createMongoCollectionMock();

$this->mongo->expects($this->never())
->method('selectCollection');

$that = $this;

$collection->expects($this->never())
->method('remove');

$this->assertTrue($this->storage->gc(1));
}

public function testGetConnection()
{
$method = new \ReflectionMethod($this->storage, 'getMongo');
Expand Down
Morty Proxy This is a proxified and sanitized view of the page, visit original site.