diff --git a/core/core.services.yml b/core/core.services.yml index 193ffbb7dacd4b7749586672d0cb20293cabd0b9..aa3961dff4496f987f64d03c80d3cd42fc84ed84 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1548,7 +1548,7 @@ services: Drupal\Core\Utility\Token: '@token' batch.storage: class: Drupal\Core\Batch\BatchStorage - arguments: ['@database', '@session', '@csrf_token'] + arguments: ['@database', '@session', '@csrf_token', '@datetime.time'] tags: - { name: backend_overridable } lazy: true diff --git a/core/includes/form.inc b/core/includes/form.inc index 4f353d008826baa87ed1c37bf7b5c16a0d5e8376..d6db3ddeec91fc11e75e7f32b6e1756487f41c44 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -6,6 +6,8 @@ */ use Drupal\Component\Utility\UrlHelper; +use Drupal\Core\Batch\BatchStorageInterface; +use Drupal\Core\Database\IntegrityConstraintViolationException; use Drupal\Core\Render\Element; use Drupal\Core\Render\Element\RenderElement; use Drupal\Core\Template\Attribute; @@ -897,9 +899,26 @@ function batch_process($redirect = NULL, Url $url = NULL, $redirect_callback = N // environments. \Drupal::moduleHandler()->alter('batch', $batch); - // Assign an arbitrary id: don't rely on a serial column in the 'batch' - // table, since non-progressive batches skip database storage completely. - $batch['id'] = \Drupal::database()->nextId(); + // Assign an id to progressive batches. Non-progressive batches skip + // database storage completely. + try { + $batch['id'] = $batch['progressive'] ? \Drupal::service(BatchStorageInterface::class)->getId() : 'non-progressive'; + } + catch (IntegrityConstraintViolationException $e) { + // @todo this is here to support the update path to deprecate + // Connection::nextId(). Remove in Drupal 11. + $connection = \Drupal::database(); + $max_bid = (int) $connection->query('SELECT MAX([bid]) FROM {batch}')->fetchField(); + $batch['id'] = $max_bid + 1; + $connection->insert('batch') + ->fields([ + 'bid' => $batch['id'], + 'timestamp' => \Drupal::time()->getRequestTime(), + 'token' => '', + 'batch' => NULL, + ]) + ->execute(); + } // Move operations to a job queue. Non-progressive batches will use a // memory-based queue. @@ -931,7 +950,7 @@ function batch_process($redirect = NULL, Url $url = NULL, $redirect_callback = N } // Store the batch. - \Drupal::service('batch.storage')->create($batch); + \Drupal::service(BatchStorageInterface::class)->create($batch); // Set the batch number in the session to guarantee that it will stay alive. $_SESSION['batches'][$batch['id']] = TRUE; diff --git a/core/lib/Drupal/Core/Batch/BatchStorage.php b/core/lib/Drupal/Core/Batch/BatchStorage.php index 3c44628ef93d9b75af2523a80b13df038052080e..51ad49ef84e1c132b87088e73397b2dc8cb13dca 100644 --- a/core/lib/Drupal/Core/Batch/BatchStorage.php +++ b/core/lib/Drupal/Core/Batch/BatchStorage.php @@ -2,10 +2,11 @@ namespace Drupal\Core\Batch; +use Drupal\Component\Datetime\TimeInterface; +use Drupal\Core\Access\CsrfTokenGenerator; use Drupal\Core\Database\Connection; use Drupal\Core\Database\DatabaseException; use Symfony\Component\HttpFoundation\Session\SessionInterface; -use Drupal\Core\Access\CsrfTokenGenerator; class BatchStorage implements BatchStorageInterface { @@ -35,6 +36,11 @@ class BatchStorage implements BatchStorageInterface { */ protected $csrfToken; + /** + * The time service. + */ + protected readonly TimeInterface $time; + /** * Constructs the database batch storage service. * @@ -44,11 +50,18 @@ class BatchStorage implements BatchStorageInterface { * The session. * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token * The CSRF token generator. + * @param \Drupal\Component\Datetime\TimeInterface $time + * The time service. */ - public function __construct(Connection $connection, SessionInterface $session, CsrfTokenGenerator $csrf_token) { + public function __construct(Connection $connection, SessionInterface $session, CsrfTokenGenerator $csrf_token, TimeInterface $time = NULL) { $this->connection = $connection; $this->session = $session; $this->csrfToken = $csrf_token; + if (!$time) { + @trigger_error('Calling ' . __METHOD__ . '() without the $time argument is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. See https://www.drupal.org/node/3220378', E_USER_DEPRECATED); + $time = \Drupal::service('datetime.time'); + } + $this->time = $time; } /** @@ -109,7 +122,7 @@ public function cleanup() { try { // Cleanup the batch table and the queue for failed batches. $this->connection->delete('batch') - ->condition('timestamp', REQUEST_TIME - 864000, '<') + ->condition('timestamp', $this->time->getRequestTime() - 864000, '<') ->execute(); } catch (\Exception $e) { @@ -121,12 +134,29 @@ public function cleanup() { * {@inheritdoc} */ public function create(array $batch) { - // Ensure that a session is started before using the CSRF token generator. + // Ensure that a session is started before using the CSRF token generator, + // and update the database record. $this->session->start(); + $this->connection->update('batch') + ->fields([ + 'token' => $this->csrfToken->get($batch['id']), + 'batch' => serialize($batch), + ]) + ->condition('bid', $batch['id']) + ->execute(); + } + + /** + * Returns a new batch id. + * + * @return int + * A batch id. + */ + public function getId(): int { $try_again = FALSE; try { // The batch table might not yet exist. - $this->doCreate($batch); + return $this->doInsertBatchRecord(); } catch (\Exception $e) { // If there was an exception, try to create the table. @@ -138,23 +168,22 @@ public function create(array $batch) { } // Now that the table has been created, try again if necessary. if ($try_again) { - $this->doCreate($batch); + return $this->doInsertBatchRecord(); } } /** - * Saves a batch. + * Inserts a record in the table and returns the batch id. * - * @param array $batch - * The array representing the batch to create. + * @return int + * A batch id. */ - protected function doCreate(array $batch) { - $this->connection->insert('batch') + protected function doInsertBatchRecord(): int { + return $this->connection->insert('batch') ->fields([ - 'bid' => $batch['id'], - 'timestamp' => REQUEST_TIME, - 'token' => $this->csrfToken->get($batch['id']), - 'batch' => serialize($batch), + 'timestamp' => $this->time->getRequestTime(), + 'token' => '', + 'batch' => NULL, ]) ->execute(); } @@ -208,9 +237,7 @@ public function schemaDefinition() { 'fields' => [ 'bid' => [ 'description' => 'Primary Key: Unique batch ID.', - // This is not a serial column, to allow both progressive and - // non-progressive batches. See batch_process(). - 'type' => 'int', + 'type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE, ], diff --git a/core/lib/Drupal/Core/ProxyClass/Batch/BatchStorage.php b/core/lib/Drupal/Core/ProxyClass/Batch/BatchStorage.php index 5e6fc6df6d216cb7c7e031e2663f85fa2c91de7d..4b54090cabc0add17eeb14f4dda2a0089314adad 100644 --- a/core/lib/Drupal/Core/ProxyClass/Batch/BatchStorage.php +++ b/core/lib/Drupal/Core/ProxyClass/Batch/BatchStorage.php @@ -107,6 +107,14 @@ public function create(array $batch) return $this->lazyLoadItself()->create($batch); } + /** + * {@inheritdoc} + */ + public function getId(): int + { + return $this->lazyLoadItself()->getId(); + } + /** * {@inheritdoc} */ diff --git a/core/modules/system/system.install b/core/modules/system/system.install index f7be7128f64d9b344585f8b94d272a1ac224b2f2..30aa5b5870fdcc8671f208106959cc57ee71267d 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1780,3 +1780,22 @@ function system_update_10100(&$sandbox = NULL) { } } + +/** + * Change the {batch} table [bid] field to serial. + */ +function system_update_10101(&$sandbox = NULL) { + $connection = \Drupal::database(); + $schema = $connection->schema(); + + // Update batch table. + if ($schema->tableExists('batch')) { + $schema->changeField('batch', 'bid', 'bid', [ + 'description' => 'Primary Key: Unique batch ID.', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ]); + } + +} diff --git a/core/modules/system/tests/src/Functional/Update/BatchBidSerialUpdateTest.php b/core/modules/system/tests/src/Functional/Update/BatchBidSerialUpdateTest.php new file mode 100644 index 0000000000000000000000000000000000000000..0e5f4d3c2283f4b7332f5a946156ff6efcc442f8 --- /dev/null +++ b/core/modules/system/tests/src/Functional/Update/BatchBidSerialUpdateTest.php @@ -0,0 +1,81 @@ +<?php + +namespace Drupal\Tests\system\Functional\Update; + +use Drupal\Core\Database\IntegrityConstraintViolationException; +use Drupal\FunctionalTests\Update\UpdatePathTestBase; + +/** + * Tests system_update_10101() upgrade path. + * + * @group system + * @group legacy + */ +class BatchBidSerialUpdateTest extends UpdatePathTestBase { + + /** + * {@inheritdoc} + */ + protected function setDatabaseDumpFiles() { + $this->databaseDumpFiles = [ + __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-9.4.0.bare.standard.php.gz', + ]; + } + + /** + * Tests the change of the {batch} table [bid] field to serial. + */ + public function testUpdate(): void { + /** @var \Drupal\Core\Database\Connection $connection */ + $connection = \Drupal::service('database'); + + // Before the update, inserting a record in the {batch} table without + // passing a value for [bid] should fail, with the exception of the SQLite + // database where a NOT NULL integer field that is the primary key is set + // to automatic increment anyway. + // + // @see https://www.drupal.org/project/drupal/issues/2665216#comment-14885361 + try { + $connection->insert('batch') + ->fields([ + 'timestamp' => \Drupal::time()->getRequestTime(), + 'token' => '', + 'batch' => NULL, + ]) + ->execute(); + if ($connection->databaseType() !== 'sqlite') { + $this->fail('Insert to {batch} without bid should have failed, but it did not'); + } + } + catch (\Exception $e) { + $this->assertInstanceOf(IntegrityConstraintViolationException::class, $e); + } + + $this->runUpdates(); + + // $bid should be higher than one, since the update process would have + // executed a batch already. We look at the records inserted to determine + // the value of $bid, instead of relying on the value returned by the + // INSERT, because in PostgreSql the test connection gets confused by the + // ::changeField() executed in the SUT and keeps returning 0 instead of + // lastId as result of the insert. + $connection->insert('batch') + ->fields([ + 'timestamp' => \Drupal::time()->getRequestTime(), + 'token' => '', + 'batch' => NULL, + ]) + ->execute(); + $bid = (int) $connection->query('SELECT MAX([bid]) FROM {batch}')->fetchField(); + $this->assertGreaterThan(1, $bid); + $connection->insert('batch') + ->fields([ + 'timestamp' => \Drupal::time()->getRequestTime(), + 'token' => '', + 'batch' => NULL, + ]) + ->execute(); + $this->assertEquals($bid + 1, (int) $connection->query('SELECT MAX([bid]) FROM {batch}')->fetchField()); + } + +} diff --git a/core/phpstan-baseline.neon b/core/phpstan-baseline.neon index 4b0b775af4c08d0a6ee61b157bce3ec1ae87edf4..e74767498466018598a6325e31a652d3655d5ff2 100644 --- a/core/phpstan-baseline.neon +++ b/core/phpstan-baseline.neon @@ -230,11 +230,6 @@ parameters: count: 1 path: lib/Drupal/Core/Asset/JsCollectionRenderer.php - - - message: "#^Call to deprecated constant REQUEST_TIME\\: Deprecated in drupal\\:8\\.3\\.0 and is removed from drupal\\:11\\.0\\.0\\. Use \\\\Drupal\\:\\:time\\(\\)\\-\\>getRequestTime\\(\\); $#" - count: 2 - path: lib/Drupal/Core/Batch/BatchStorage.php - - message: "#^Call to method getDefinitions\\(\\) on an unknown class Drupal\\\\Core\\\\Plugin\\\\CategorizingPluginManagerTrait\\.$#" count: 1