8000 [HttpFoundation] Allow dynamic session "ttl" when using a remote storage by nicolas-grekas · Pull Request #45873 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[HttpFoundation] Allow dynamic session "ttl" when using a remote storage #45873

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

Merged
merged 1 commit into from
Mar 30, 2022
Merged
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 @@ -77,7 +77,7 @@

->set('session.abstract_handler', AbstractSessionHandler::class)
->factory([SessionHandlerFactory::class, 'createHandler'])
->args([abstract_arg('A string or a connection object')])
->args([abstract_arg('A string or a connection object'), []])

->set('session_listener', SessionListener::class)
->args([
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/HttpFoundation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ CHANGELOG
---

* Add stale while revalidate and stale if error cache header
* Allow dynamic session "ttl" when using a remote storage

6.0
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class MemcachedSessionHandler extends AbstractSessionHandler
/**
* Time to live in seconds.
*/
private ?int $ttl;
private int|\Closure|null $ttl;

/**
* Key prefix for shared environments.
Expand Down Expand Up @@ -69,7 +69,8 @@ protected function doRead(string $sessionId): string

public function updateTimestamp(string $sessionId, string $data): bool
{
$this->memcached->touch($this->prefix.$sessionId, time() + (int) ($this->ttl ?? ini_get('session.gc_maxlifetime')));
$ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? ini_get('session.gc_maxlifetime');
$this->memcached->touch($this->prefix.$sessionId, time() + (int) $ttl);

return true;
}
Expand All @@ -79,7 +80,9 @@ public function updateTimestamp(string $sessionId, string $data): bool
*/
protected function doWrite(string $sessionId, string $data): bool
{
return $this->memcached->set($this->prefix.$sessionId, $data, time() + (int) ($this->ttl ?? ini_get('session.gc_maxlifetime')));
$ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? ini_get('session.gc_maxlifetime');

return $this->memcached->set($this->prefix.$sessionId, $data, time() + (int) $ttl);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class MongoDbSessionHandler extends AbstractSessionHandler
private Client $mongo;
private Collection $collection;
private array $options;
private int|\Closure|null $ttl;

/**
* Constructor.
Expand All @@ -39,7 +40,8 @@ class MongoDbSessionHandler extends AbstractSessionHandler
* * 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]
* * ttl: The time to live in seconds.
*
* It is strongly recommended to put an index on the `expiry_field` for
* garbage-collection. Alternatively it's possible to automatically expire
Expand Down Expand Up @@ -74,6 +76,7 @@ public function __construct(Client $mongo, array $options)
'time_field' => 'time',
'expiry_field' => 'expires_at',
], $options);
$this->ttl = $this->options['ttl'] ?? null;
}

public function close(): bool
Expand Down Expand Up @@ -105,7 +108,8 @@ public function gc(int $maxlifetime): int|false
*/
protected function doWrite(string $sessionId, string $data): bool
{
$expiry = new UTCDateTime((time() + (int) ini_get('session.gc_maxlifetime')) * 1000);
$ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? ini_get('session.gc_maxlifetime');
$expiry = new UTCDateTime((time() + (int) $ttl) * 1000);

$fields = [
$this->options['time_field'] => new UTCDateTime(),
Expand All @@ -124,7 +128,8 @@ protected function doWrite(string $sessionId, string $data): bool

public function updateTimestamp(string $sessionId, string $data): bool
{
$expiry = new UTCDateTime((time() + (int) ini_get('session.gc_maxlifetime')) * 1000);
$ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? ini_get('session.gc_maxlifetime');
$expiry = new UTCDateTime((time() + (int) $ttl) * 1000);

$this->getCollection()->updateOne(
[$this->options['id_field'] => $sessionId],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ class PdoSessionHandler extends AbstractSessionHandler
private string $lifetimeCol = 'sess_lifetime';
private string $timeCol = 'sess_time';

/**
* Time to live in seconds.
*/
private int|\Closure|null $ttl;

/**
* Username when lazy-connect.
*/
Expand Down Expand Up @@ -137,6 +142,7 @@ class PdoSessionHandler extends AbstractSessionHandler
* * db_password: The password when lazy-connect [default: '']
* * db_connection_options: An array of driver-specific connection options [default: []]
* * lock_mode: The strategy for locking, see constants [default: LOCK_TRANSACTIONAL]
* * ttl: The time to live in seconds.
*
* @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or URL string or null
*
Expand Down Expand Up @@ -166,6 +172,7 @@ public function __construct(\PDO|string $pdoOrDsn = null, array $options = [])
$this->password = $options['db_password'] ?? $this->password;
$this->connectionOptions = $options['db_connection_options'] ?? $this->connectionOptions;
$this->lockMode = $options['lock_mode'] ?? $this->lockMode;
$this->ttl = $options['ttl'] ?? null;
}

/**
Expand Down Expand Up @@ -275,7 +282,7 @@ protected function doDestroy(string $sessionId): bool
*/
protected function doWrite(string $sessionId, string $data): bool
{
$maxlifetime = (int) ini_get('session.gc_maxlifetime');
$maxlifetime = (int) (($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? ini_get('session.gc_maxlifetime'));

try {
// We use a single MERGE SQL query when supported by the database.
Expand Down Expand Up @@ -318,7 +325,7 @@ protected function doWrite(string $sessionId, string $data): bool

public function updateTimestamp(string $sessionId, string $data): bool
{
$expiry = time() + (int) ini_get('session.gc_maxlifetime');
$expiry = time() + (int) (($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? ini_get('session.gc_maxlifetime'));

try {
$updateStmt = $this->pdo->prepare(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class RedisSessionHandler extends AbstractSessionHandler
/**
* Time to live in seconds.
*/
private ?int $ttl;
private int|\Closure|null $ttl;

/**
* List of available options:
Expand Down Expand Up @@ -66,7 +66,8 @@ protected function doRead(string $sessionId): string
*/
protected function doWrite(string $sessionId, string $data): bool
{
$result = $this->redis->setEx($this->prefix.$sessionId, (int) ($this->ttl ?? ini_get('session.gc_maxlifetime')), $data);
$ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? ini_get('session.gc_maxlifetime');
$result = $this->redis->setEx($this->prefix.$sessionId, (int) $ttl, $data);

return $result && !$result instanceof ErrorInterface;
}
Expand Down Expand Up @@ -109,6 +110,8 @@ public function gc(int $maxlifetime): int|false

public function updateTimestamp(string $sessionId, string $data): bool
{
return $this->redis->expire($this->prefix.$sessionId, (int) ($this->ttl ?? ini_get('session.gc_maxlifetime')));
$ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? ini_get('session.gc_maxlifetime');

return $this->redis->expire($this->prefix.$sessionId, (int) $ttl);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,16 @@
*/
class SessionHandlerFactory
{
public static function createHandler(object|string $connection): AbstractSessionHandler
public static function createHandler(object|string $connection, array $options = []): AbstractSessionHandler
{
if ($options = \is_string($connection) ? parse_url($connection) : false) {
parse_str($options['query'] ?? '', $options);
if ($query = \is_string($connection) ? parse_url($connection) : false) {
parse_str($query['query'] ?? '', $query);

if (($options['ttl'] ?? null) instanceof \Closure) {
$query['ttl'] = $options['ttl'];
}
}
$options = ($query ?: []) + $options;

switch (true) {
case $connection instanceof \Redis:
Expand Down Expand Up @@ -58,7 +63,7 @@ public static function createHandler(object|string $connection): AbstractSession
$handlerClass = str_starts_with($connection, 'memcached:') ? MemcachedSessionHandler::class : RedisSessionHandler::class;
$connection = AbstractAdapter::createConnection($connection, ['lazy' => true]);

return new $handlerClass($connection, array_intersect_key($options ?: [], ['prefix' => 1, 'ttl' => 1]));
return new $handlerClass($connection, array_intersect_key($options, ['prefix' => 1, 'ttl' => 1]));

case str_starts_with($connection, 'pdo_oci://'):
if (!class_exists(DriverManager::class)) {
Expand All @@ -76,7 +81,7 @@ public static function createHandler(object|string $connection): AbstractSession
case str_starts_with($connection, 'sqlsrv://'):
case str_starts_with($connection, 'sqlite://'):
case str_starts_with($connection, 'sqlite3://'):
return new PdoSessionHandler($connection, $options ?: []);
return new PdoSessionHandler($connection, $options);
}

throw new \InvalidArgumentException(sprintf('Unsupported Connection: "%s".', $connection));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,19 @@ public function testUseTtlOption(int $ttl)

$this->assertLessThan($redisTtl, $ttl - 5);
$this->assertGreaterThan($redisTtl, $ttl + 5);

$options = [
'prefix' => self::PREFIX,
'ttl' => fn () => $ttl,
];

$handler = new RedisSessionHandler($this->redisClient, $options);
$handler->write('id', 'data');
$redisTtl = $this->redisClient->ttl(self::PREFIX.'id');

$this->assertLessThan($redisTtl, $ttl - 5);
$this->assertGreaterThan($redisTtl, $ttl + 5);

}

public function getTtlFixtures(): array
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ public function getOptionFixtures()
return [
[['prefix' => 'session'], true],
[['expiretime' => 100], true],
[['prefix' => 'session', 'expiretime' => 200], true],
[['prefix' => 'session', 'ttl' => 200], true],
[['expiretime' => 100, 'foo' => 'bar'], false],
];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,21 @@ public function provideUrlDsnPairs()
yield ['mssql://localhost:56/test', 'sqlsrv:server=localhost,56;Database=test'];
}

public function testTtl()
{
foreach ([60, fn () => 60] as $ttl) {
$pdo = $this->getMemorySqlitePdo();
$storage = new PdoSessionHandler($pdo, ['ttl' => $ttl]);

$storage->open('', 'sid');
$storage->read('id');
$storage->write('id', 'data');
$storage->close();

$this->assertEqualsWithDelta(time() + 60, $pdo->query('SELECT sess_lifetime FROM sessions')->fetchColumn(), 5);
}
}

/**
* @return resource
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,9 @@ public function testCreateRedisHandlerFromDsn()

$ttlProperty = $reflection->getProperty('ttl');
$this->assertSame(3600, $ttlProperty->getValue($handler));

$handler = SessionHandlerFactory::createHandler('redis://localhost?prefix=foo&ttl=3600&ignored=bar', ['ttl' => function () { return 123; }]);

$this->assertInstanceOf(\Closure::class, $reflection->getProperty('ttl')->getValue($handler));
}
}
0