Issue #3428565 by catch, phenaproxima, alexpott: Implement lazy database creation for sessions
(cherry picked from commit da2e2ce2ae
)
merge-requests/7287/head
parent
2071823a8b
commit
9feabd3759
|
@ -73,7 +73,7 @@ trait InstallerRedirectTrait {
|
|||
// Redirect if the database is empty.
|
||||
if ($connection) {
|
||||
try {
|
||||
return !$connection->schema()->tableExists('sessions');
|
||||
return !$connection->schema()->tableExists('sequences');
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
// If we still have an exception at this point, we need to be careful
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace Drupal\Core\Session;
|
|||
use Drupal\Component\Datetime\TimeInterface;
|
||||
use Drupal\Component\Utility\Crypt;
|
||||
use Drupal\Core\Database\Connection;
|
||||
use Drupal\Core\Database\DatabaseException;
|
||||
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy;
|
||||
|
@ -62,11 +63,16 @@ class SessionHandler extends AbstractProxy implements \SessionHandlerInterface {
|
|||
public function read(#[\SensitiveParameter] string $sid): string|false {
|
||||
$data = '';
|
||||
if (!empty($sid)) {
|
||||
try {
|
||||
// Read the session data from the database.
|
||||
$query = $this->connection
|
||||
->queryRange('SELECT [session] FROM {sessions} WHERE [sid] = :sid', 0, 1, [':sid' => Crypt::hashBase64($sid)]);
|
||||
$data = (string) $query->fetchField();
|
||||
}
|
||||
// Swallow the error if the table hasn't been created yet.
|
||||
catch (\Exception) {
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
@ -74,6 +80,7 @@ class SessionHandler extends AbstractProxy implements \SessionHandlerInterface {
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function write(#[\SensitiveParameter] string $sid, string $value): bool {
|
||||
$try_again = FALSE;
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
$fields = [
|
||||
'uid' => $request->getSession()->get('uid', 0),
|
||||
|
@ -81,10 +88,27 @@ class SessionHandler extends AbstractProxy implements \SessionHandlerInterface {
|
|||
'session' => $value,
|
||||
'timestamp' => $this->time->getRequestTime(),
|
||||
];
|
||||
$doWrite = fn() =>
|
||||
$this->connection->merge('sessions')
|
||||
->keys(['sid' => Crypt::hashBase64($sid)])
|
||||
->fields($fields)
|
||||
->execute();
|
||||
try {
|
||||
$doWrite();
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
// If there was an exception, try to create the table.
|
||||
if (!$try_again = $this->ensureTableExists()) {
|
||||
// If the exception happened for other reason than the missing
|
||||
// table, propagate the exception.
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
// Now that the bin has been created, try again if necessary.
|
||||
if ($try_again) {
|
||||
$doWrite();
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
|
@ -99,10 +123,15 @@ class SessionHandler extends AbstractProxy implements \SessionHandlerInterface {
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function destroy(#[\SensitiveParameter] string $sid): bool {
|
||||
try {
|
||||
// Delete session data.
|
||||
$this->connection->delete('sessions')
|
||||
->condition('sid', Crypt::hashBase64($sid))
|
||||
->execute();
|
||||
}
|
||||
// Swallow the error if the table hasn't been created yet.
|
||||
catch (\Exception) {
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
@ -116,9 +145,98 @@ class SessionHandler extends AbstractProxy implements \SessionHandlerInterface {
|
|||
// for three weeks before deleting them, you need to set gc_maxlifetime
|
||||
// to '1814400'. At that value, only after a user doesn't log in after
|
||||
// three weeks (1814400 seconds) will their session be removed.
|
||||
try {
|
||||
return $this->connection->delete('sessions')
|
||||
->condition('timestamp', $this->time->getRequestTime() - $lifetime, '<')
|
||||
->execute();
|
||||
}
|
||||
// Swallow the error if the table hasn't been created yet.
|
||||
catch (\Exception) {
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the schema for the session table.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function schemaDefinition(): array {
|
||||
$schema = [
|
||||
'description' => "Drupal's session handlers read and write into the sessions table. Each record represents a user session, either anonymous or authenticated.",
|
||||
'fields' => [
|
||||
'uid' => [
|
||||
'description' => 'The {users}.uid corresponding to a session, or 0 for anonymous user.',
|
||||
'type' => 'int',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
],
|
||||
'sid' => [
|
||||
'description' => "A session ID (hashed). The value is generated by Drupal's session handlers.",
|
||||
'type' => 'varchar_ascii',
|
||||
'length' => 128,
|
||||
'not null' => TRUE,
|
||||
],
|
||||
'hostname' => [
|
||||
'description' => 'The IP address that last used this session ID (sid).',
|
||||
'type' => 'varchar_ascii',
|
||||
'length' => 128,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
],
|
||||
'timestamp' => [
|
||||
'description' => 'The Unix timestamp when this session last requested a page. Old records are purged by PHP automatically.',
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'size' => 'big',
|
||||
],
|
||||
'session' => [
|
||||
'description' => 'The serialized contents of the user\'s session, an array of name/value pairs that persists across page requests by this session ID. Drupal loads the user\'s session from here at the start of each request and saves it at the end.',
|
||||
'type' => 'blob',
|
||||
'not null' => FALSE,
|
||||
'size' => 'big',
|
||||
],
|
||||
],
|
||||
'primary key' => [
|
||||
'sid',
|
||||
],
|
||||
'indexes' => [
|
||||
'timestamp' => ['timestamp'],
|
||||
'uid' => ['uid'],
|
||||
],
|
||||
'foreign keys' => [
|
||||
'session_user' => [
|
||||
'table' => 'users',
|
||||
'columns' => ['uid' => 'uid'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the session table exists and create it if not.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the table already exists or was created, FALSE if creation fails.
|
||||
*/
|
||||
protected function ensureTableExists(): bool {
|
||||
try {
|
||||
$database_schema = $this->connection->schema();
|
||||
$schema_definition = $this->schemaDefinition();
|
||||
$database_schema->createTable('sessions', $schema_definition);
|
||||
}
|
||||
// If another process has already created the table, attempting to create
|
||||
// it will throw an exception. In this case just catch the exception and do
|
||||
// nothing.
|
||||
catch (DatabaseException $e) {
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -234,10 +234,15 @@ class SessionManager extends NativeSessionStorage implements SessionManagerInter
|
|||
if (!$this->writeSafeHandler->isSessionWritable() || $this->isCli()) {
|
||||
return;
|
||||
}
|
||||
// The sessions table may not have been created yet.
|
||||
try {
|
||||
$this->connection->delete('sessions')
|
||||
->condition('uid', $uid)
|
||||
->execute();
|
||||
}
|
||||
catch (\Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
|
|
|
@ -26,6 +26,8 @@ class DbDumpTest extends DriverSpecificKernelTestBase {
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
// @todo system can be removed from this test once
|
||||
// https://www.drupal.org/project/drupal/issues/2851705 is committed.
|
||||
'system',
|
||||
'config',
|
||||
'dblog',
|
||||
|
@ -87,7 +89,6 @@ class DbDumpTest extends DriverSpecificKernelTestBase {
|
|||
parent::setUp();
|
||||
|
||||
// Create some schemas so our export contains tables.
|
||||
$this->installSchema('system', ['sessions']);
|
||||
$this->installSchema('dblog', ['watchdog']);
|
||||
$this->installEntitySchema('block_content');
|
||||
$this->installEntitySchema('user');
|
||||
|
@ -131,7 +132,6 @@ class DbDumpTest extends DriverSpecificKernelTestBase {
|
|||
'menu_link_content_data',
|
||||
'menu_link_content_revision',
|
||||
'menu_link_content_field_revision',
|
||||
'sessions',
|
||||
'path_alias',
|
||||
'path_alias_revision',
|
||||
'user__roles',
|
||||
|
|
|
@ -1621,57 +1621,6 @@ function system_schema() {
|
|||
'primary key' => ['value'],
|
||||
];
|
||||
|
||||
$schema['sessions'] = [
|
||||
'description' => "Drupal's session handlers read and write into the sessions table. Each record represents a user session, either anonymous or authenticated.",
|
||||
'fields' => [
|
||||
'uid' => [
|
||||
'description' => 'The {users}.uid corresponding to a session, or 0 for anonymous user.',
|
||||
'type' => 'int',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
],
|
||||
'sid' => [
|
||||
'description' => "A session ID (hashed). The value is generated by Drupal's session handlers.",
|
||||
'type' => 'varchar_ascii',
|
||||
'length' => 128,
|
||||
'not null' => TRUE,
|
||||
],
|
||||
'hostname' => [
|
||||
'description' => 'The IP address that last used this session ID (sid).',
|
||||
'type' => 'varchar_ascii',
|
||||
'length' => 128,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
],
|
||||
'timestamp' => [
|
||||
'description' => 'The Unix timestamp when this session last requested a page. Old records are purged by PHP automatically.',
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'size' => 'big',
|
||||
],
|
||||
'session' => [
|
||||
'description' => 'The serialized contents of the user\'s session, an array of name/value pairs that persists across page requests by this session ID. Drupal loads the user\'s session from here at the start of each request and saves it at the end.',
|
||||
'type' => 'blob',
|
||||
'not null' => FALSE,
|
||||
'size' => 'big',
|
||||
],
|
||||
],
|
||||
'primary key' => [
|
||||
'sid',
|
||||
],
|
||||
'indexes' => [
|
||||
'timestamp' => ['timestamp'],
|
||||
'uid' => ['uid'],
|
||||
],
|
||||
'foreign keys' => [
|
||||
'session_user' => [
|
||||
'table' => 'users',
|
||||
'columns' => ['uid' => 'uid'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ class InstallerRedirectTraitTest extends KernelTestBase {
|
|||
* - Exceptions to be handled by shouldRedirectToInstaller()
|
||||
* - Whether or not there is a database connection.
|
||||
* - Whether or not there is database connection info.
|
||||
* - Whether or not there exists a sessions table in the database.
|
||||
* - Whether or not there exists a sequences table in the database.
|
||||
*/
|
||||
public static function providerShouldRedirectToInstaller() {
|
||||
return [
|
||||
|
@ -67,7 +67,7 @@ class InstallerRedirectTraitTest extends KernelTestBase {
|
|||
* @covers ::shouldRedirectToInstaller
|
||||
* @dataProvider providerShouldRedirectToInstaller
|
||||
*/
|
||||
public function testShouldRedirectToInstaller($expected, $exception, $connection, $connection_info, $session_table_exists = TRUE) {
|
||||
public function testShouldRedirectToInstaller($expected, $exception, $connection, $connection_info, $sequences_table_exists = TRUE) {
|
||||
try {
|
||||
throw new $exception();
|
||||
}
|
||||
|
@ -106,8 +106,8 @@ class InstallerRedirectTraitTest extends KernelTestBase {
|
|||
|
||||
$schema->expects($this->any())
|
||||
->method('tableExists')
|
||||
->with('sessions')
|
||||
->willReturn($session_table_exists);
|
||||
->with('sequences')
|
||||
->willReturn($sequences_table_exists);
|
||||
|
||||
$connection->expects($this->any())
|
||||
->method('schema')
|
||||
|
|
Loading…
Reference in New Issue