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.
|
// Redirect if the database is empty.
|
||||||
if ($connection) {
|
if ($connection) {
|
||||||
try {
|
try {
|
||||||
return !$connection->schema()->tableExists('sessions');
|
return !$connection->schema()->tableExists('sequences');
|
||||||
}
|
}
|
||||||
catch (\Exception $e) {
|
catch (\Exception $e) {
|
||||||
// If we still have an exception at this point, we need to be careful
|
// 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\Datetime\TimeInterface;
|
||||||
use Drupal\Component\Utility\Crypt;
|
use Drupal\Component\Utility\Crypt;
|
||||||
use Drupal\Core\Database\Connection;
|
use Drupal\Core\Database\Connection;
|
||||||
|
use Drupal\Core\Database\DatabaseException;
|
||||||
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
|
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
|
||||||
use Symfony\Component\HttpFoundation\RequestStack;
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy;
|
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 {
|
public function read(#[\SensitiveParameter] string $sid): string|false {
|
||||||
$data = '';
|
$data = '';
|
||||||
if (!empty($sid)) {
|
if (!empty($sid)) {
|
||||||
|
try {
|
||||||
// Read the session data from the database.
|
// Read the session data from the database.
|
||||||
$query = $this->connection
|
$query = $this->connection
|
||||||
->queryRange('SELECT [session] FROM {sessions} WHERE [sid] = :sid', 0, 1, [':sid' => Crypt::hashBase64($sid)]);
|
->queryRange('SELECT [session] FROM {sessions} WHERE [sid] = :sid', 0, 1, [':sid' => Crypt::hashBase64($sid)]);
|
||||||
$data = (string) $query->fetchField();
|
$data = (string) $query->fetchField();
|
||||||
}
|
}
|
||||||
|
// Swallow the error if the table hasn't been created yet.
|
||||||
|
catch (\Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,6 +80,7 @@ class SessionHandler extends AbstractProxy implements \SessionHandlerInterface {
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public function write(#[\SensitiveParameter] string $sid, string $value): bool {
|
public function write(#[\SensitiveParameter] string $sid, string $value): bool {
|
||||||
|
$try_again = FALSE;
|
||||||
$request = $this->requestStack->getCurrentRequest();
|
$request = $this->requestStack->getCurrentRequest();
|
||||||
$fields = [
|
$fields = [
|
||||||
'uid' => $request->getSession()->get('uid', 0),
|
'uid' => $request->getSession()->get('uid', 0),
|
||||||
|
@ -81,10 +88,27 @@ class SessionHandler extends AbstractProxy implements \SessionHandlerInterface {
|
||||||
'session' => $value,
|
'session' => $value,
|
||||||
'timestamp' => $this->time->getRequestTime(),
|
'timestamp' => $this->time->getRequestTime(),
|
||||||
];
|
];
|
||||||
|
$doWrite = fn() =>
|
||||||
$this->connection->merge('sessions')
|
$this->connection->merge('sessions')
|
||||||
->keys(['sid' => Crypt::hashBase64($sid)])
|
->keys(['sid' => Crypt::hashBase64($sid)])
|
||||||
->fields($fields)
|
->fields($fields)
|
||||||
->execute();
|
->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;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,10 +123,15 @@ class SessionHandler extends AbstractProxy implements \SessionHandlerInterface {
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public function destroy(#[\SensitiveParameter] string $sid): bool {
|
public function destroy(#[\SensitiveParameter] string $sid): bool {
|
||||||
|
try {
|
||||||
// Delete session data.
|
// Delete session data.
|
||||||
$this->connection->delete('sessions')
|
$this->connection->delete('sessions')
|
||||||
->condition('sid', Crypt::hashBase64($sid))
|
->condition('sid', Crypt::hashBase64($sid))
|
||||||
->execute();
|
->execute();
|
||||||
|
}
|
||||||
|
// Swallow the error if the table hasn't been created yet.
|
||||||
|
catch (\Exception) {
|
||||||
|
}
|
||||||
|
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
@ -116,9 +145,98 @@ class SessionHandler extends AbstractProxy implements \SessionHandlerInterface {
|
||||||
// for three weeks before deleting them, you need to set gc_maxlifetime
|
// 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
|
// to '1814400'. At that value, only after a user doesn't log in after
|
||||||
// three weeks (1814400 seconds) will their session be removed.
|
// three weeks (1814400 seconds) will their session be removed.
|
||||||
|
try {
|
||||||
return $this->connection->delete('sessions')
|
return $this->connection->delete('sessions')
|
||||||
->condition('timestamp', $this->time->getRequestTime() - $lifetime, '<')
|
->condition('timestamp', $this->time->getRequestTime() - $lifetime, '<')
|
||||||
->execute();
|
->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()) {
|
if (!$this->writeSafeHandler->isSessionWritable() || $this->isCli()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// The sessions table may not have been created yet.
|
||||||
|
try {
|
||||||
$this->connection->delete('sessions')
|
$this->connection->delete('sessions')
|
||||||
->condition('uid', $uid)
|
->condition('uid', $uid)
|
||||||
->execute();
|
->execute();
|
||||||
}
|
}
|
||||||
|
catch (\Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
|
|
|
@ -26,6 +26,8 @@ class DbDumpTest extends DriverSpecificKernelTestBase {
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
protected static $modules = [
|
protected static $modules = [
|
||||||
|
// @todo system can be removed from this test once
|
||||||
|
// https://www.drupal.org/project/drupal/issues/2851705 is committed.
|
||||||
'system',
|
'system',
|
||||||
'config',
|
'config',
|
||||||
'dblog',
|
'dblog',
|
||||||
|
@ -87,7 +89,6 @@ class DbDumpTest extends DriverSpecificKernelTestBase {
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
// Create some schemas so our export contains tables.
|
// Create some schemas so our export contains tables.
|
||||||
$this->installSchema('system', ['sessions']);
|
|
||||||
$this->installSchema('dblog', ['watchdog']);
|
$this->installSchema('dblog', ['watchdog']);
|
||||||
$this->installEntitySchema('block_content');
|
$this->installEntitySchema('block_content');
|
||||||
$this->installEntitySchema('user');
|
$this->installEntitySchema('user');
|
||||||
|
@ -131,7 +132,6 @@ class DbDumpTest extends DriverSpecificKernelTestBase {
|
||||||
'menu_link_content_data',
|
'menu_link_content_data',
|
||||||
'menu_link_content_revision',
|
'menu_link_content_revision',
|
||||||
'menu_link_content_field_revision',
|
'menu_link_content_field_revision',
|
||||||
'sessions',
|
|
||||||
'path_alias',
|
'path_alias',
|
||||||
'path_alias_revision',
|
'path_alias_revision',
|
||||||
'user__roles',
|
'user__roles',
|
||||||
|
|
|
@ -1621,57 +1621,6 @@ function system_schema() {
|
||||||
'primary key' => ['value'],
|
'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;
|
return $schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ class InstallerRedirectTraitTest extends KernelTestBase {
|
||||||
* - Exceptions to be handled by shouldRedirectToInstaller()
|
* - Exceptions to be handled by shouldRedirectToInstaller()
|
||||||
* - Whether or not there is a database connection.
|
* - Whether or not there is a database connection.
|
||||||
* - Whether or not there is database connection info.
|
* - 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() {
|
public static function providerShouldRedirectToInstaller() {
|
||||||
return [
|
return [
|
||||||
|
@ -67,7 +67,7 @@ class InstallerRedirectTraitTest extends KernelTestBase {
|
||||||
* @covers ::shouldRedirectToInstaller
|
* @covers ::shouldRedirectToInstaller
|
||||||
* @dataProvider providerShouldRedirectToInstaller
|
* @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 {
|
try {
|
||||||
throw new $exception();
|
throw new $exception();
|
||||||
}
|
}
|
||||||
|
@ -106,8 +106,8 @@ class InstallerRedirectTraitTest extends KernelTestBase {
|
||||||
|
|
||||||
$schema->expects($this->any())
|
$schema->expects($this->any())
|
||||||
->method('tableExists')
|
->method('tableExists')
|
||||||
->with('sessions')
|
->with('sequences')
|
||||||
->willReturn($session_table_exists);
|
->willReturn($sequences_table_exists);
|
||||||
|
|
||||||
$connection->expects($this->any())
|
$connection->expects($this->any())
|
||||||
->method('schema')
|
->method('schema')
|
||||||
|
|
Loading…
Reference in New Issue