diff options
Diffstat (limited to 'core')
53 files changed, 1761 insertions, 310 deletions
diff --git a/core/lib/Drupal/Core/Database/Transaction.php b/core/lib/Drupal/Core/Database/Transaction.php index dcecc44e17c6..b8693e4bb761 100644 --- a/core/lib/Drupal/Core/Database/Transaction.php +++ b/core/lib/Drupal/Core/Database/Transaction.php @@ -30,12 +30,12 @@ class Transaction { /** * Destructs the object. * - * Depending on the nesting level of the object, this leads to a COMMIT (for - * a root item) or to a RELEASE SAVEPOINT (for a savepoint item) executed on - * the database. + * If the transaction is still active at this stage, and depending on the + * state of the transaction stack, this leads to a COMMIT (for a root item) + * or to a RELEASE SAVEPOINT (for a savepoint item) executed on the database. */ public function __destruct() { - $this->connection->transactionManager()->unpile($this->name, $this->id); + $this->connection->transactionManager()->purge($this->name, $this->id); } /** @@ -46,16 +46,22 @@ class Transaction { } /** - * Rolls back the current transaction. + * Returns the transaction to the parent nesting level. * - * This is just a wrapper method to rollback whatever transaction stack we are - * currently in, which is managed by the TransactionManager. Note that logging - * needs to happen after a transaction has been rolled back or the log - * messages will be rolled back too. + * Depending on the state of the transaction stack, this leads to a COMMIT + * operation (for a root item), or to a RELEASE SAVEPOINT operation (for a + * savepoint item) executed on the database. + */ + public function commitOrRelease(): void { + $this->connection->transactionManager()->unpile($this->name, $this->id); + } + + /** + * Rolls back the transaction. * - * Depending on the nesting level of the object, this leads to a ROLLBACK (for - * a root item) or to a ROLLBACK TO SAVEPOINT (for a savepoint item) executed - * on the database. + * Depending on the state of the transaction stack, this leads to a ROLLBACK + * operation (for a root item), or to a ROLLBACK TO SAVEPOINT + a RELEASE + * SAVEPOINT operations (for a savepoint item) executed on the database. */ public function rollBack() { $this->connection->transactionManager()->rollback($this->name, $this->id); diff --git a/core/lib/Drupal/Core/Database/Transaction/TransactionManagerBase.php b/core/lib/Drupal/Core/Database/Transaction/TransactionManagerBase.php index aa663d942265..fa1a309a7678 100644 --- a/core/lib/Drupal/Core/Database/Transaction/TransactionManagerBase.php +++ b/core/lib/Drupal/Core/Database/Transaction/TransactionManagerBase.php @@ -102,6 +102,16 @@ abstract class TransactionManagerBase implements TransactionManagerInterface { private ClientConnectionTransactionState $connectionTransactionState; /** + * Whether to trigger warnings when unpiling a void transaction. + * + * Normally FALSE, is set to TRUE by specific tests checking the internal + * state of the transaction stack. + * + * @internal + */ + public bool $triggerWarningWhenUnpilingOnVoidTransaction = FALSE; + + /** * Constructor. * * @param \Drupal\Core\Database\Connection $connection @@ -202,7 +212,9 @@ abstract class TransactionManagerBase implements TransactionManagerInterface { protected function voidStackItem(string $id): void { // The item should be removed from $stack and added to $voidedItems for // later processing. - $this->voidedItems[$id] = $this->stack[$id]; + if (isset($this->stack[$id])) { + $this->voidedItems[$id] = $this->stack[$id]; + } $this->removeStackItem($id); } @@ -285,14 +297,29 @@ abstract class TransactionManagerBase implements TransactionManagerInterface { } /** - * {@inheritdoc} + * Purges a Drupal transaction from the manager. + * + * This is only called by a Transaction object's ::__destruct() method and + * should only be called internally by a database driver. + * + * @param string $name + * The name of the transaction. + * @param string $id + * The id of the transaction. + * + * @throws \Drupal\Core\Database\TransactionOutOfOrderException + * If a Drupal Transaction with the specified name does not exist. + * @throws \Drupal\Core\Database\TransactionCommitFailedException + * If the commit of the root transaction failed. + * + * @internal */ - public function unpile(string $name, string $id): void { + public function purge(string $name, string $id): void { // If this is a 'root' transaction, and it is voided (that is, no longer in // the stack), then the transaction on the database is no longer active. An - // action such as a rollback, or a DDL statement, was executed that - // terminated the database transaction. So, we can process the post - // transaction callbacks. + // action such as a commit, a release savepoint, a rollback, or a DDL + // statement, was executed that terminated the database transaction. So, we + // can process the post transaction callbacks. if (!isset($this->stack()[$id]) && isset($this->voidedItems[$id]) && $this->rootId === $id) { $this->processPostTransactionCallbacks(); $this->rootId = NULL; @@ -309,6 +336,62 @@ abstract class TransactionManagerBase implements TransactionManagerInterface { return; } + // When we get here, the transaction (or savepoint) is still active on the + // database. We can unpile it, and if we are left with no more items in the + // stack, we can also process the post transaction callbacks. + $this->commit($name, $id); + $this->removeStackItem($id); + if ($this->rootId === $id) { + $this->processPostTransactionCallbacks(); + $this->rootId = NULL; + } + } + + /** + * {@inheritdoc} + */ + public function unpile(string $name, string $id): void { + // If the transaction was voided, we cannot unpile. Skip but trigger a user + // warning if requested. + if ($this->getConnectionTransactionState() === ClientConnectionTransactionState::Voided) { + if ($this->triggerWarningWhenUnpilingOnVoidTransaction) { + trigger_error('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', E_USER_WARNING); + } + return; + } + + // If there is no $id to commit, or if $id does not correspond to the one + // in the stack for that $name, the commit is out of order. + if (!isset($this->stack()[$id]) || $this->stack()[$id]->name !== $name) { + throw new TransactionOutOfOrderException("Error attempting commit of {$id}\\{$name}. Active stack: " . $this->dumpStackItemsAsString()); + } + + // Commit the transaction. + $this->commit($name, $id); + + // Void the transaction stack item. + $this->voidStackItem($id); + } + + /** + * Commits a Drupal transaction. + * + * @param string $name + * The name of the transaction. + * @param string $id + * The id of the transaction. + * + * @throws \Drupal\Core\Database\TransactionOutOfOrderException + * If a Drupal Transaction with the specified name does not exist. + * @throws \Drupal\Core\Database\TransactionCommitFailedException + * If the commit of the root transaction failed. + */ + protected function commit(string $name, string $id): void { + if ($this->getConnectionTransactionState() !== ClientConnectionTransactionState::Active) { + // The stack got corrupted. + throw new TransactionOutOfOrderException("Transaction {$id}\\{$name} is out of order. Active stack: " . $this->dumpStackItemsAsString()); + } + // If we are not releasing the last savepoint but an earlier one, or // committing a root transaction while savepoints are active, all // subsequent savepoints will be released as well. The stack must be @@ -317,33 +400,20 @@ abstract class TransactionManagerBase implements TransactionManagerInterface { $this->voidStackItem((string) $i); } - if ($this->getConnectionTransactionState() === ClientConnectionTransactionState::Active) { - if ($this->stackDepth() > 1 && $this->stack()[$id]->type === StackItemType::Savepoint) { - // Release the client transaction savepoint in case the Drupal - // transaction is not a root one. - $this->releaseClientSavepoint($name); - } - elseif ($this->stackDepth() === 1 && $this->stack()[$id]->type === StackItemType::Root) { - // If this was the root Drupal transaction, we can commit the client - // transaction. - $this->processRootCommit(); - if ($this->rootId === $id) { - $this->processPostTransactionCallbacks(); - $this->rootId = NULL; - } - } - else { - // The stack got corrupted. - throw new TransactionOutOfOrderException("Transaction {$id}/{$name} is out of order. Active stack: " . $this->dumpStackItemsAsString()); - } - - // Remove the transaction from the stack. - $this->removeStackItem($id); - return; + if ($this->stackDepth() > 1 && $this->stack()[$id]->type === StackItemType::Savepoint) { + // Release the client transaction savepoint in case the Drupal + // transaction is not a root one. + $this->releaseClientSavepoint($name); + } + elseif ($this->stackDepth() === 1 && $this->stack()[$id]->type === StackItemType::Root) { + // If this was the root Drupal transaction, we can commit the client + // transaction. + $this->processRootCommit(); + } + else { + // The stack got corrupted. + throw new TransactionOutOfOrderException("Transaction {$id}/{$name} is out of order. Active stack: " . $this->dumpStackItemsAsString()); } - - // The stack got corrupted. - throw new TransactionOutOfOrderException("Transaction {$id}/{$name} is out of order. Active stack: " . $this->dumpStackItemsAsString()); } /** diff --git a/core/lib/Drupal/Core/Database/Transaction/TransactionManagerInterface.php b/core/lib/Drupal/Core/Database/Transaction/TransactionManagerInterface.php index 11af511f14bc..a9aa2c770525 100644 --- a/core/lib/Drupal/Core/Database/Transaction/TransactionManagerInterface.php +++ b/core/lib/Drupal/Core/Database/Transaction/TransactionManagerInterface.php @@ -53,8 +53,8 @@ interface TransactionManagerInterface { * Removes a Drupal transaction from the stack. * * The unpiled item does not necessarily need to be the last on the stack. - * This method should only be called by a Transaction object going out of - * scope. + * This method should only be called by a Transaction object's + * ::commitOrRelease() method. * * This method should only be called internally by a database driver. * diff --git a/core/lib/Drupal/Core/Hook/Attribute/FormAlter.php b/core/lib/Drupal/Core/Hook/Attribute/FormAlter.php deleted file mode 100644 index 158010463d2a..000000000000 --- a/core/lib/Drupal/Core/Hook/Attribute/FormAlter.php +++ /dev/null @@ -1,54 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Drupal\Core\Hook\Attribute; - -use Drupal\Core\Hook\Order\OrderInterface; - -/** - * Hook attribute for FormAlter. - * - * @see hook_form_alter(). - */ -#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] -class FormAlter extends Hook { - - /** - * {@inheritdoc} - */ - public const string PREFIX = 'form'; - - /** - * {@inheritdoc} - */ - public const string SUFFIX = 'alter'; - - /** - * Constructs a FormAlter attribute object. - * - * @param string $form_id - * (optional) The ID of the form that this implementation alters. - * If this is left blank then `form_alter` is the hook that is registered. - * @param string $method - * (optional) The method name. If this attribute is on a method, this - * parameter is not required. If this attribute is on a class and this - * parameter is omitted, the class must have an __invoke() method, which is - * taken as the hook implementation. - * @param string|null $module - * (optional) The module this implementation is for. This allows one module - * to implement a hook on behalf of another module. Defaults to the module - * the implementation is in. - * @param \Drupal\Core\Hook\Order\OrderInterface|null $order - * (optional) Set the order of the implementation. - */ - public function __construct( - string $form_id = '', - public string $method = '', - public ?string $module = NULL, - public ?OrderInterface $order = NULL, - ) { - parent::__construct($form_id, $method, $module, $order); - } - -} diff --git a/core/lib/Drupal/Core/Hook/Attribute/Hook.php b/core/lib/Drupal/Core/Hook/Attribute/Hook.php index 34dbc8ebf916..0084e651180d 100644 --- a/core/lib/Drupal/Core/Hook/Attribute/Hook.php +++ b/core/lib/Drupal/Core/Hook/Attribute/Hook.php @@ -98,27 +98,10 @@ use Drupal\Core\Hook\Order\OrderInterface; class Hook implements HookAttributeInterface { /** - * The hook prefix such as `form`. - * - * @var string - */ - public const string PREFIX = ''; - - /** - * The hook suffix such as `alter`. - * - * @var string - */ - public const string SUFFIX = ''; - - /** * Constructs a Hook attribute object. * * @param string $hook * The short hook name, without the 'hook_' prefix. - * $hook is only optional when Hook is extended and a PREFIX or SUFFIX is - * defined. When using the [#Hook] attribute directly $hook is required. - * See Drupal\Core\Hook\Attribute\Preprocess. * @param string $method * (optional) The method name. If this attribute is on a method, this * parameter is not required. If this attribute is on a class and this @@ -132,15 +115,10 @@ class Hook implements HookAttributeInterface { * (optional) Set the order of the implementation. */ public function __construct( - public string $hook = '', + public string $hook, public string $method = '', public ?string $module = NULL, public ?OrderInterface $order = NULL, - ) { - $this->hook = implode('_', array_filter([static::PREFIX, $hook, static::SUFFIX])); - if ($this->hook === '') { - throw new \LogicException('The Hook attribute or an attribute extending the Hook attribute must provide the $hook parameter, a PREFIX or a SUFFIX.'); - } - } + ) {} } diff --git a/core/lib/Drupal/Core/Hook/Attribute/Preprocess.php b/core/lib/Drupal/Core/Hook/Attribute/Preprocess.php deleted file mode 100644 index 47642859a20b..000000000000 --- a/core/lib/Drupal/Core/Hook/Attribute/Preprocess.php +++ /dev/null @@ -1,23 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Drupal\Core\Hook\Attribute; - -/** - * Attribute for defining a class method as a preprocess function. - * - * Pass no arguments for hook_preprocess `#[Preprocess]`. - * For `hook_preprocess_HOOK` pass the `HOOK` without the `hook_preprocess` - * portion `#[Preprocess('HOOK')]`. - * - * See \Drupal\Core\Hook\Attribute\Hook for additional information. - */ -#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] -class Preprocess extends Hook { - /** - * {@inheritdoc} - */ - public const string PREFIX = 'preprocess'; - -} diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index d0150fe0127a..fe3f29ea6968 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -250,9 +250,10 @@ class Renderer implements RendererInterface { return $return; } - // Only when we're in a root (non-recursive) Renderer::render() call, - // placeholders must be processed, to prevent breaking the render cache in - // case of nested elements with #cache set. + // Only when rendering the root do placeholders have to be processed. If we + // were to replace them while rendering cacheable nested elements, their + // cacheable metadata would still bubble all the way up the render tree, + // effectively making the use of placeholders pointless. $this->replacePlaceholders($elements); return $elements['#markup']; @@ -304,11 +305,9 @@ class Renderer implements RendererInterface { } $context->push(new BubbleableMetadata()); - // Set the bubbleable rendering metadata that has configurable defaults, if: - // - this is the root call, to ensure that the final render array definitely - // has these configurable defaults, even when no subtree is render cached. - // - this is a render cacheable subtree, to ensure that the cached data has - // the configurable defaults (which may affect the ID and invalidation). + // Set the bubbleable rendering metadata that has configurable defaults if + // this is a render cacheable subtree, to ensure that the cached data has + // the configurable defaults (which may affect the ID and invalidation). if (isset($elements['#cache']['keys'])) { $required_cache_contexts = $this->rendererConfig['required_cache_contexts']; if (isset($elements['#cache']['contexts'])) { diff --git a/core/lib/Drupal/Core/Test/SimpletestTestRunResultsStorage.php b/core/lib/Drupal/Core/Test/SimpletestTestRunResultsStorage.php index cb754e1afaa4..0c000d675c3c 100644 --- a/core/lib/Drupal/Core/Test/SimpletestTestRunResultsStorage.php +++ b/core/lib/Drupal/Core/Test/SimpletestTestRunResultsStorage.php @@ -95,13 +95,14 @@ class SimpletestTestRunResultsStorage implements TestRunResultsStorageInterface * {@inheritdoc} */ public function removeResults(TestRun $test_run): int { - $this->connection->startTransaction('delete_test_run'); + $transaction = $this->connection->startTransaction('delete_test_run'); $this->connection->delete('simpletest') ->condition('test_id', $test_run->id()) ->execute(); $count = $this->connection->delete('simpletest_test_id') ->condition('test_id', $test_run->id()) ->execute(); + $transaction->commitOrRelease(); return $count; } @@ -169,9 +170,10 @@ class SimpletestTestRunResultsStorage implements TestRunResultsStorageInterface */ public function cleanUp(): int { // Clear test results. - $this->connection->startTransaction('delete_simpletest'); + $transaction = $this->connection->startTransaction('delete_simpletest'); $this->connection->delete('simpletest')->execute(); $count = $this->connection->delete('simpletest_test_id')->execute(); + $transaction->commitOrRelease(); return $count; } diff --git a/core/modules/block/migrations/d6_block.yml b/core/modules/block/migrations/d6_block.yml index 74922444e8df..853ce28a47b8 100644 --- a/core/modules/block/migrations/d6_block.yml +++ b/core/modules/block/migrations/d6_block.yml @@ -56,8 +56,6 @@ process: 1: forum_new_block locale: 0: language_block - node: - 0: node_syndicate_block search: 0: search_form_block statistics: diff --git a/core/modules/block/migrations/d7_block.yml b/core/modules/block/migrations/d7_block.yml index 9b031b7daa79..35c6f23d86f4 100644 --- a/core/modules/block/migrations/d7_block.yml +++ b/core/modules/block/migrations/d7_block.yml @@ -59,8 +59,6 @@ process: new: forum_new_block # locale: # 0: language_block - node: - syndicate: node_syndicate_block search: form: search_form_block statistics: diff --git a/core/modules/block/tests/src/Kernel/BlockConfigSchemaTest.php b/core/modules/block/tests/src/Kernel/BlockConfigSchemaTest.php index 8b2ead48edaf..6305ab7f8415 100644 --- a/core/modules/block/tests/src/Kernel/BlockConfigSchemaTest.php +++ b/core/modules/block/tests/src/Kernel/BlockConfigSchemaTest.php @@ -65,6 +65,10 @@ class BlockConfigSchemaTest extends KernelTestBase { */ public function testBlockConfigSchema(): void { foreach ($this->blockManager->getDefinitions() as $block_id => $definition) { + // Skip the syndicate block as it is deprecated. + if ($block_id === 'node_syndicate_block') { + continue; + } $id = $this->randomMachineName(); $block = Block::create([ 'id' => $id, diff --git a/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php b/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php index 3f20b2148b8d..dc96d95e6996 100644 --- a/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php +++ b/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php @@ -100,7 +100,7 @@ class MigrateBlockTest extends MigrateDrupal6TestBase { */ public function testBlockMigration(): void { $blocks = Block::loadMultiple(); - $this->assertCount(25, $blocks); + $this->assertCount(24, $blocks); // Check user blocks. $visibility = [ diff --git a/core/modules/block_content/src/Controller/BlockContentController.php b/core/modules/block_content/src/Controller/BlockContentController.php index b2776f51d7d8..77f8eee7939d 100644 --- a/core/modules/block_content/src/Controller/BlockContentController.php +++ b/core/modules/block_content/src/Controller/BlockContentController.php @@ -2,9 +2,9 @@ namespace Drupal\block_content\Controller; +use Drupal\block_content\BlockContentTypeInterface; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Entity\EntityStorageInterface; -use Drupal\block_content\BlockContentTypeInterface; use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -88,7 +88,8 @@ class BlockContentController extends ControllerBase { uasort($types, [$this->blockContentTypeStorage->getEntityType()->getClass(), 'sort']); if ($types && count($types) == 1) { $type = reset($types); - return $this->addForm($type, $request); + $query = $request->query->all(); + return $this->redirect('block_content.add_form', ['block_content_type' => $type->id()], ['query' => $query]); } if (count($types) === 0) { return [ diff --git a/core/modules/block_content/src/Plugin/Menu/LocalAction/BlockContentAddLocalAction.php b/core/modules/block_content/src/Plugin/Menu/LocalAction/BlockContentAddLocalAction.php index 844e06895cc5..4e6c3b141e70 100644 --- a/core/modules/block_content/src/Plugin/Menu/LocalAction/BlockContentAddLocalAction.php +++ b/core/modules/block_content/src/Plugin/Menu/LocalAction/BlockContentAddLocalAction.php @@ -5,7 +5,6 @@ namespace Drupal\block_content\Plugin\Menu\LocalAction; use Drupal\Core\Menu\LocalActionDefault; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Routing\RouteProviderInterface; -use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RequestStack; @@ -54,11 +53,6 @@ class BlockContentAddLocalAction extends LocalActionDefault { if ($region = $this->requestStack->getCurrentRequest()->query->getString('region')) { $options['query']['region'] = $region; } - - // Adds a destination on content block listing. - if ($route_match->getRouteName() == 'entity.block_content.collection') { - $options['query']['destination'] = Url::fromRoute('<current>')->toString(); - } return $options; } diff --git a/core/modules/block_content/tests/src/Functional/BlockContentCreationTest.php b/core/modules/block_content/tests/src/Functional/BlockContentCreationTest.php index bca42cd3e328..364b5f4524d6 100644 --- a/core/modules/block_content/tests/src/Functional/BlockContentCreationTest.php +++ b/core/modules/block_content/tests/src/Functional/BlockContentCreationTest.php @@ -155,11 +155,7 @@ class BlockContentCreationTest extends BlockContentTestBase { // Create a block and place in block layout. $this->drupalGet('/admin/content/block'); $this->clickLink('Add content block'); - // Verify destination URL, when clicking "Save and configure" this - // destination will be ignored. - $base = base_path(); - $url = 'block/add?destination=' . $base . 'admin/content/block'; - $this->assertSession()->addressEquals($url); + $this->assertSession()->addressEquals('/block/add/basic'); $edit = []; $edit['info[0][value]'] = 'Test Block'; $edit['body[0][value]'] = $this->randomMachineName(16); diff --git a/core/modules/block_content/tests/src/Functional/LocalActionTest.php b/core/modules/block_content/tests/src/Functional/LocalActionTest.php new file mode 100644 index 000000000000..bb1a20df880c --- /dev/null +++ b/core/modules/block_content/tests/src/Functional/LocalActionTest.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\block_content\Functional; + +/** + * Tests block_content local action links. + * + * @group block_content + */ +class LocalActionTest extends BlockContentTestBase { + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $this->drupalLogin($this->adminUser); + } + + /** + * Tests the block_content_add_action link. + */ + public function testAddContentBlockLink(): void { + // Verify that the link takes you straight to the block form if there's only + // one type. + $this->drupalGet('/admin/content/block'); + $this->clickLink('Add content block'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->addressEquals('/block/add/basic'); + + $type = $this->randomMachineName(); + $this->createBlockContentType([ + 'id' => $type, + 'label' => $type, + ]); + + // Verify that the link takes you to the block add page if there's more than + // one type. + $this->drupalGet('/admin/content/block'); + $this->clickLink('Add content block'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->addressEquals('/block/add'); + } + +} diff --git a/core/modules/comment/src/Hook/CommentThemeHooks.php b/core/modules/comment/src/Hook/CommentThemeHooks.php index e789af6dab10..c137d586d416 100644 --- a/core/modules/comment/src/Hook/CommentThemeHooks.php +++ b/core/modules/comment/src/Hook/CommentThemeHooks.php @@ -2,7 +2,7 @@ namespace Drupal\comment\Hook; -use Drupal\Core\Hook\Attribute\Preprocess; +use Drupal\Core\Hook\Attribute\Hook; /** * Hook implementations for comment. @@ -12,7 +12,7 @@ class CommentThemeHooks { /** * Implements hook_preprocess_HOOK() for block templates. */ - #[Preprocess('block')] + #[Hook('preprocess_block')] public function preprocessBlock(&$variables): void { if ($variables['configuration']['provider'] == 'comment') { $variables['attributes']['role'] = 'navigation'; diff --git a/core/modules/comment/tests/modules/comment_empty_title_test/src/Hook/CommentEmptyTitleTestThemeHooks.php b/core/modules/comment/tests/modules/comment_empty_title_test/src/Hook/CommentEmptyTitleTestThemeHooks.php index db1ffae5a6d0..01a40394b409 100644 --- a/core/modules/comment/tests/modules/comment_empty_title_test/src/Hook/CommentEmptyTitleTestThemeHooks.php +++ b/core/modules/comment/tests/modules/comment_empty_title_test/src/Hook/CommentEmptyTitleTestThemeHooks.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Drupal\comment_empty_title_test\Hook; -use Drupal\Core\Hook\Attribute\Preprocess; +use Drupal\Core\Hook\Attribute\Hook; /** * Hook implementations for comment_empty_title_test. @@ -14,7 +14,7 @@ class CommentEmptyTitleTestThemeHooks { /** * Implements hook_preprocess_comment(). */ - #[Preprocess('comment')] + #[Hook('preprocess_comment')] public function preprocessComment(&$variables): void { $variables['title'] = ''; } diff --git a/core/modules/config_translation/migrations/d6_block_translation.yml b/core/modules/config_translation/migrations/d6_block_translation.yml index 6d57fdae1be4..7925c49626f2 100644 --- a/core/modules/config_translation/migrations/d6_block_translation.yml +++ b/core/modules/config_translation/migrations/d6_block_translation.yml @@ -39,8 +39,6 @@ process: 1: forum_new_block locale: 0: language_block - node: - 0: node_syndicate_block search: 0: search_form_block statistics: diff --git a/core/modules/config_translation/migrations/d7_block_translation.yml b/core/modules/config_translation/migrations/d7_block_translation.yml index 9c82ee6b6786..d2530e3b50a8 100644 --- a/core/modules/config_translation/migrations/d7_block_translation.yml +++ b/core/modules/config_translation/migrations/d7_block_translation.yml @@ -44,8 +44,6 @@ process: new: forum_new_block # locale: # 0: language_block - node: - syndicate: node_syndicate_block search: form: search_form_block statistics: diff --git a/core/modules/contact/src/Hook/ContactFormHooks.php b/core/modules/contact/src/Hook/ContactFormHooks.php index ad8223c3ec62..b31b929bddf2 100644 --- a/core/modules/contact/src/Hook/ContactFormHooks.php +++ b/core/modules/contact/src/Hook/ContactFormHooks.php @@ -4,7 +4,7 @@ namespace Drupal\contact\Hook; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Hook\Attribute\FormAlter; +use Drupal\Core\Hook\Attribute\Hook; use Drupal\Core\Session\AccountInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\user\UserDataInterface; @@ -29,7 +29,7 @@ class ContactFormHooks { * * @see \Drupal\user\ProfileForm::form() */ - #[FormAlter('user_form')] + #[Hook('form_user_form_alter')] public function formUserFormAlter(&$form, FormStateInterface $form_state) : void { $form['contact'] = [ '#type' => 'details', @@ -55,7 +55,7 @@ class ContactFormHooks { * * Adds the default personal contact setting on the user settings page. */ - #[FormAlter('user_admin_settings')] + #[Hook('form_user_admin_settings_alter')] public function formUserAdminSettingsAlter(&$form, FormStateInterface $form_state) : void { $form['contact'] = [ '#type' => 'details', diff --git a/core/modules/contextual/src/Hook/ContextualThemeHooks.php b/core/modules/contextual/src/Hook/ContextualThemeHooks.php index 760a42c97854..7d873196b431 100644 --- a/core/modules/contextual/src/Hook/ContextualThemeHooks.php +++ b/core/modules/contextual/src/Hook/ContextualThemeHooks.php @@ -2,7 +2,7 @@ namespace Drupal\contextual\Hook; -use Drupal\Core\Hook\Attribute\Preprocess; +use Drupal\Core\Hook\Attribute\Hook; use Drupal\Core\Session\AccountInterface; /** @@ -21,7 +21,7 @@ class ContextualThemeHooks { * @see contextual_page_attachments() * @see \Drupal\contextual\ContextualController::render() */ - #[Preprocess] + #[Hook('preprocess')] public function preprocess(&$variables, $hook, $info): void { // Determine the primary theme function argument. if (!empty($info['variables'])) { diff --git a/core/modules/locale/src/Hook/LocaleThemeHooks.php b/core/modules/locale/src/Hook/LocaleThemeHooks.php index d1e438f50ace..4ef5ca0b4989 100644 --- a/core/modules/locale/src/Hook/LocaleThemeHooks.php +++ b/core/modules/locale/src/Hook/LocaleThemeHooks.php @@ -2,7 +2,7 @@ namespace Drupal\locale\Hook; -use Drupal\Core\Hook\Attribute\Preprocess; +use Drupal\Core\Hook\Attribute\Hook; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Language\LanguageManagerInterface; @@ -18,7 +18,7 @@ class LocaleThemeHooks { /** * Implements hook_preprocess_HOOK() for node templates. */ - #[Preprocess('node')] + #[Hook('preprocess_node')] public function preprocessNode(&$variables): void { /** @var \Drupal\node\NodeInterface $node */ $node = $variables['node']; diff --git a/core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php b/core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php index 3a1cb8a1b695..77c8b45d00f8 100644 --- a/core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php +++ b/core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php @@ -609,15 +609,15 @@ abstract class SourcePluginBase extends PluginBase implements MigrateSourceInter * {@inheritdoc} */ public function preRollback(MigrateRollbackEvent $event) { - // Nothing to do in this implementation. + // Reset the high-water mark. + $this->saveHighWater(NULL); } /** * {@inheritdoc} */ public function postRollback(MigrateRollbackEvent $event) { - // Reset the high-water mark. - $this->saveHighWater(NULL); + // Nothing to do in this implementation. } /** diff --git a/core/modules/migrate/tests/src/Unit/MigrateSourceTest.php b/core/modules/migrate/tests/src/Unit/MigrateSourceTest.php index 2f0b85ffbc47..e344e3e23e84 100644 --- a/core/modules/migrate/tests/src/Unit/MigrateSourceTest.php +++ b/core/modules/migrate/tests/src/Unit/MigrateSourceTest.php @@ -9,6 +9,7 @@ use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; use Drupal\Core\KeyValueStore\KeyValueStoreInterface; +use Drupal\migrate\Event\MigrateRollbackEvent; use Drupal\migrate\MigrateException; use Drupal\migrate\MigrateExecutable; use Drupal\migrate\MigrateSkipRowException; @@ -448,6 +449,32 @@ class MigrateSourceTest extends MigrateTestCase { return new MigrateExecutable($migration, $message, $event_dispatcher); } + /** + * @covers ::preRollback + */ + public function testPreRollback(): void { + $this->migrationConfiguration['id'] = 'test_migration'; + $plugin_id = 'test_migration'; + $migration = $this->getMigration(); + + // Verify that preRollback() sets the high water mark to NULL. + $key_value = $this->createMock(KeyValueStoreInterface::class); + $key_value->expects($this->once()) + ->method('set') + ->with($plugin_id, NULL); + $key_value_factory = $this->createMock(KeyValueFactoryInterface::class); + $key_value_factory->expects($this->once()) + ->method('get') + ->with('migrate:high_water') + ->willReturn($key_value); + $container = new ContainerBuilder(); + $container->set('keyvalue', $key_value_factory); + \Drupal::setContainer($container); + + $source = new StubSourceGeneratorPlugin([], $plugin_id, [], $migration); + $source->preRollback(new MigrateRollbackEvent($migration)); + } + } /** diff --git a/core/modules/migrate_drupal/tests/src/Kernel/d7/FieldDiscoveryTest.php b/core/modules/migrate_drupal/tests/src/Kernel/d7/FieldDiscoveryTest.php index 1f54f94848ec..efe2b1509282 100644 --- a/core/modules/migrate_drupal/tests/src/Kernel/d7/FieldDiscoveryTest.php +++ b/core/modules/migrate_drupal/tests/src/Kernel/d7/FieldDiscoveryTest.php @@ -18,6 +18,7 @@ use Drupal\field_discovery_test\FieldDiscoveryTestClass; * Test FieldDiscovery Service against Drupal 7. * * @group migrate_drupal + * @group #slow * @coversDefaultClass \Drupal\migrate_drupal\FieldDiscovery */ class FieldDiscoveryTest extends MigrateDrupal7TestBase { diff --git a/core/modules/migrate_drupal/tests/src/Kernel/d7/MigrateDrupal7AuditIdsTest.php b/core/modules/migrate_drupal/tests/src/Kernel/d7/MigrateDrupal7AuditIdsTest.php index ca8a9a0d06b3..27ab60bc0c0c 100644 --- a/core/modules/migrate_drupal/tests/src/Kernel/d7/MigrateDrupal7AuditIdsTest.php +++ b/core/modules/migrate_drupal/tests/src/Kernel/d7/MigrateDrupal7AuditIdsTest.php @@ -16,6 +16,7 @@ use Drupal\Tests\migrate_drupal\Traits\CreateTestContentEntitiesTrait; * Tests the migration auditor for ID conflicts. * * @group migrate_drupal + * @group #slow */ class MigrateDrupal7AuditIdsTest extends MigrateDrupal7TestBase { diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/d6/Upgrade6Test.php b/core/modules/migrate_drupal_ui/tests/src/Functional/d6/Upgrade6Test.php index 64dc7a1ea865..daf06a65468a 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/d6/Upgrade6Test.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/d6/Upgrade6Test.php @@ -73,7 +73,7 @@ class Upgrade6Test extends MigrateUpgradeExecuteTestBase { */ protected function getEntityCounts(): array { return [ - 'block' => 37, + 'block' => 36, 'block_content' => 2, 'block_content_type' => 1, 'comment' => 8, diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/d7/Upgrade7Test.php b/core/modules/migrate_drupal_ui/tests/src/Functional/d7/Upgrade7Test.php index 46b3447e159d..f9b702d22e3a 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/d7/Upgrade7Test.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/d7/Upgrade7Test.php @@ -76,7 +76,7 @@ class Upgrade7Test extends MigrateUpgradeExecuteTestBase { */ protected function getEntityCounts(): array { return [ - 'block' => 27, + 'block' => 26, 'block_content' => 1, 'block_content_type' => 1, 'comment' => 4, diff --git a/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php b/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php index ae9ae566f8dd..5bf9d2477f09 100644 --- a/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php +++ b/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php @@ -73,14 +73,14 @@ class PerformanceTest extends PerformanceTestBase { $expected = [ 'QueryCount' => 4, - 'CacheGetCount' => 48, + 'CacheGetCount' => 47, 'CacheGetCountByBin' => [ 'config' => 11, 'data' => 4, 'discovery' => 10, 'bootstrap' => 6, 'dynamic_page_cache' => 1, - 'render' => 15, + 'render' => 14, 'menu' => 1, ], 'CacheSetCount' => 2, @@ -89,7 +89,7 @@ class PerformanceTest extends PerformanceTestBase { ], 'CacheDeleteCount' => 0, 'CacheTagInvalidationCount' => 0, - 'CacheTagLookupQueryCount' => 14, + 'CacheTagLookupQueryCount' => 13, 'ScriptCount' => 3, 'ScriptBytes' => 167569, 'StylesheetCount' => 2, diff --git a/core/modules/node/src/Hook/NodeHooks.php b/core/modules/node/src/Hook/NodeHooks.php index d5f84e0359ba..8a6b4d887c89 100644 --- a/core/modules/node/src/Hook/NodeHooks.php +++ b/core/modules/node/src/Hook/NodeHooks.php @@ -66,4 +66,13 @@ class NodeHooks { } } + /** + * Implements hook_block_alter(). + */ + #[Hook('block_alter')] + public function blockAlter(&$definitions): void { + // Hide the deprecated Syndicate block from the UI. + $definitions['node_syndicate_block']['_block_ui_hidden'] = TRUE; + } + } diff --git a/core/modules/node/src/Hook/NodeThemeHooks.php b/core/modules/node/src/Hook/NodeThemeHooks.php index 7ee443c458f8..7ed0ef91f5fd 100644 --- a/core/modules/node/src/Hook/NodeThemeHooks.php +++ b/core/modules/node/src/Hook/NodeThemeHooks.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Drupal\node\Hook; -use Drupal\Core\Hook\Attribute\Preprocess; +use Drupal\Core\Hook\Attribute\Hook; /** * Hook implementations for the node module. @@ -14,7 +14,7 @@ class NodeThemeHooks { /** * Implements hook_preprocess_HOOK() for node field templates. */ - #[Preprocess('field__node')] + #[Hook('preprocess_field__node')] public function preprocessFieldNode(&$variables): void { // Set a variable 'is_inline' in cases where inline markup is required, // without any block elements such as <div>. diff --git a/core/modules/node/src/Plugin/Block/SyndicateBlock.php b/core/modules/node/src/Plugin/Block/SyndicateBlock.php index b10c63527e5b..45cfe1eb45c6 100644 --- a/core/modules/node/src/Plugin/Block/SyndicateBlock.php +++ b/core/modules/node/src/Plugin/Block/SyndicateBlock.php @@ -14,6 +14,11 @@ use Drupal\Core\Url; /** * Provides a 'Syndicate' block that links to the site's RSS feed. + * + * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is no + * replacement. + * + * @see https://www.drupal.org/node/3519248 */ #[Block( id: "node_syndicate_block", @@ -43,6 +48,7 @@ class SyndicateBlock extends BlockBase implements ContainerFactoryPluginInterfac * The config factory. */ public function __construct(array $configuration, $plugin_id, $plugin_definition, ConfigFactoryInterface $configFactory) { + @trigger_error('The Syndicate block is deprecated in drupal:11.2.0 and will be removed from drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3519248', E_USER_DEPRECATED); parent::__construct($configuration, $plugin_id, $plugin_definition); $this->configFactory = $configFactory; } diff --git a/core/modules/node/tests/src/Functional/NodeRevisionsAuthorTest.php b/core/modules/node/tests/src/Functional/NodeRevisionsAuthorTest.php new file mode 100644 index 000000000000..5a930df3e2df --- /dev/null +++ b/core/modules/node/tests/src/Functional/NodeRevisionsAuthorTest.php @@ -0,0 +1,102 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\node\Functional; + +use Drupal\Core\Url; + +/** + * Tests reverting node revisions correctly sets authorship information. + * + * @group node + */ +class NodeRevisionsAuthorTest extends NodeTestBase { + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * Tests node authorship is retained after reverting revisions. + */ + public function testNodeRevisionRevertAuthors(): void { + // Create and log in user. + $initialUser = $this->drupalCreateUser([ + 'view page revisions', + 'revert page revisions', + 'edit any page content', + ]); + $initialRevisionUser = $this->drupalCreateUser(); + // Third user is an author only and needs no permissions + $initialRevisionAuthor = $this->drupalCreateUser(); + + // Create initial node (author: $user1). + $this->drupalLogin($initialUser); + $node = $this->drupalCreateNode(); + $originalRevisionId = $node->getRevisionId(); + $originalBody = $node->body->value; + $originalTitle = $node->getTitle(); + + // Create a revision (as $initialUser) showing $initialRevisionAuthor + // as author. + $node->setRevisionLogMessage('Changed author'); + $revisedTitle = $this->randomMachineName(); + $node->setTitle($revisedTitle); + $revisedBody = $this->randomMachineName(32); + $node->set('body', [ + 'value' => $revisedBody, + 'format' => filter_default_format(), + ]); + $node->setOwnerId($initialRevisionAuthor->id()); + $node->setRevisionUserId($initialRevisionUser->id()); + $node->setNewRevision(); + $node->save(); + $revisedRevisionId = $node->getRevisionId(); + + $nodeStorage = \Drupal::entityTypeManager()->getStorage('node'); + + self::assertEquals($node->getOwnerId(), $initialRevisionAuthor->id()); + self::assertEquals($node->getRevisionUserId(), $initialRevisionUser->id()); + + // Revert to the original node revision. + $this->drupalGet(Url::fromRoute('node.revision_revert_confirm', [ + 'node' => $node->id(), + 'node_revision' => $originalRevisionId, + ])); + $this->submitForm([], 'Revert'); + $this->assertSession()->pageTextContains(\sprintf('Basic page %s has been reverted', $originalTitle)); + + // With the revert done, reload the node and verify that the authorship + // fields have reverted correctly. + $nodeStorage->resetCache([$node->id()]); + /** @var \Drupal\node\NodeInterface $revertedNode */ + $revertedNode = $nodeStorage->load($node->id()); + self::assertEquals($originalBody, $revertedNode->body->value); + self::assertEquals($initialUser->id(), $revertedNode->getOwnerId()); + self::assertEquals($initialUser->id(), $revertedNode->getRevisionUserId()); + + // Revert again to the revised version and check that node author and + // revision author fields are correct. + // Revert to the original node. + $this->drupalGet(Url::fromRoute('node.revision_revert_confirm', [ + 'node' => $revertedNode->id(), + 'node_revision' => $revisedRevisionId, + ])); + $this->submitForm([], 'Revert'); + $this->assertSession()->pageTextContains(\sprintf('Basic page %s has been reverted', $revisedTitle)); + + // With the reversion done, reload the node and verify that the + // authorship fields have reverted correctly. + $nodeStorage->resetCache([$revertedNode->id()]); + /** @var \Drupal\node\NodeInterface $re_reverted_node */ + $re_reverted_node = $nodeStorage->load($revertedNode->id()); + self::assertEquals($revisedBody, $re_reverted_node->body->value); + self::assertEquals($initialRevisionAuthor->id(), $re_reverted_node->getOwnerId()); + // The new revision user will be the current logged in user as set in + // NodeRevisionRevertForm. + self::assertEquals($initialUser->id(), $re_reverted_node->getRevisionUserId()); + } + +} diff --git a/core/modules/node/tests/src/Functional/NodeSyndicateBlockTest.php b/core/modules/node/tests/src/Functional/NodeSyndicateBlockTest.php index c3a3d46b4960..f8d52b06ecb3 100644 --- a/core/modules/node/tests/src/Functional/NodeSyndicateBlockTest.php +++ b/core/modules/node/tests/src/Functional/NodeSyndicateBlockTest.php @@ -8,6 +8,7 @@ namespace Drupal\Tests\node\Functional; * Tests if the syndicate block is available. * * @group node + * @group legacy */ class NodeSyndicateBlockTest extends NodeTestBase { @@ -40,6 +41,7 @@ class NodeSyndicateBlockTest extends NodeTestBase { $this->drupalPlaceBlock('node_syndicate_block', ['id' => 'test_syndicate_block', 'label' => 'Subscribe to RSS Feed']); $this->drupalGet(''); $this->assertSession()->elementExists('xpath', '//div[@id="block-test-syndicate-block"]/*'); + $this->expectDeprecation('The Syndicate block is deprecated in drupal:11.2.0 and will be removed from drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3519248'); // Verify syndicate block title. $this->assertSession()->pageTextContains('Subscribe to RSS Feed'); diff --git a/core/modules/node/tests/src/Functional/NodeTranslationUITest.php b/core/modules/node/tests/src/Functional/NodeTranslationUITest.php index 2bb252f7c6e1..ac1e8664badf 100644 --- a/core/modules/node/tests/src/Functional/NodeTranslationUITest.php +++ b/core/modules/node/tests/src/Functional/NodeTranslationUITest.php @@ -242,21 +242,19 @@ class NodeTranslationUITest extends ContentTranslationUITestBase { // Set up the default admin theme and use it for node editing. $this->container->get('theme_installer')->install(['claro']); - $edit = []; - $edit['admin_theme'] = 'claro'; - $edit['use_admin_theme'] = TRUE; - $this->drupalGet('admin/appearance'); - $this->submitForm($edit, 'Save configuration'); - $this->drupalGet('node/' . $article->id() . '/translations'); + $this->config('system.theme')->set('admin', 'claro')->save(); + // Verify that translation uses the admin theme if edit is admin. + $this->drupalGet('node/' . $article->id() . '/translations'); $this->assertSession()->responseContains('core/themes/claro/css/base/elements.css'); // Turn off admin theme for editing, assert inheritance to translations. - $edit['use_admin_theme'] = FALSE; - $this->drupalGet('admin/appearance'); - $this->submitForm($edit, 'Save configuration'); - $this->drupalGet('node/' . $article->id() . '/translations'); + $this->config('node.settings')->set('use_admin_theme', FALSE)->save(); + // Changing node.settings:use_admin_theme requires a route rebuild. + $this->container->get('router.builder')->rebuild(); + // Verify that translation uses the frontend theme if edit is frontend. + $this->drupalGet('node/' . $article->id() . '/translations'); $this->assertSession()->responseNotContains('core/themes/claro/css/base/elements.css'); // Assert presence of translation page itself (vs. DisabledBundle below). @@ -561,12 +559,10 @@ class NodeTranslationUITest extends ContentTranslationUITestBase { 'translatable' => TRUE, ])->save(); - $this->drupalLogin($this->administrator); // Make the image field a multi-value field in order to display a // details form element. - $edit = ['field_storage[subform][cardinality_number]' => 2]; - $this->drupalGet('admin/structure/types/manage/article/fields/node.article.field_image'); - $this->submitForm($edit, 'Save'); + $fieldStorage = FieldStorageConfig::loadByName('node', 'field_image'); + $fieldStorage->setCardinality(2)->save(); // Enable the display of the image field. EntityFormDisplay::load('node.article.default') diff --git a/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeCompleteTest.php b/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeCompleteTest.php index cbe9b346623e..ac47588d5ec4 100644 --- a/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeCompleteTest.php +++ b/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeCompleteTest.php @@ -18,6 +18,7 @@ use Drupal\Tests\migrate_drupal\Traits\NodeMigrateTypeTestTrait; * Test class for a complete node migration for Drupal 7. * * @group migrate_drupal_7 + * @group #slow */ class MigrateNodeCompleteTest extends MigrateDrupal7TestBase { diff --git a/core/modules/page_cache/tests/src/Functional/PageCacheTagsIntegrationTest.php b/core/modules/page_cache/tests/src/Functional/PageCacheTagsIntegrationTest.php index 41f2e8b8e4ff..da6d22bfb055 100644 --- a/core/modules/page_cache/tests/src/Functional/PageCacheTagsIntegrationTest.php +++ b/core/modules/page_cache/tests/src/Functional/PageCacheTagsIntegrationTest.php @@ -155,7 +155,6 @@ class PageCacheTagsIntegrationTest extends BrowserTestBase { 'config:block.block.olivero_messages', 'config:block.block.olivero_primary_local_tasks', 'config:block.block.olivero_secondary_local_tasks', - 'config:block.block.olivero_syndicate', 'config:block.block.olivero_primary_admin_actions', 'config:block.block.olivero_page_title', 'node_view', @@ -195,7 +194,6 @@ class PageCacheTagsIntegrationTest extends BrowserTestBase { 'config:block.block.olivero_messages', 'config:block.block.olivero_primary_local_tasks', 'config:block.block.olivero_secondary_local_tasks', - 'config:block.block.olivero_syndicate', 'config:block.block.olivero_primary_admin_actions', 'config:block.block.olivero_page_title', 'node_view', diff --git a/core/modules/system/tests/fixtures/update/drupal-10.3.0.bare.standard.php.gz b/core/modules/system/tests/fixtures/update/drupal-10.3.0.bare.standard.php.gz Binary files differindex 5d8c99744690..077d0645ddc7 100644 --- a/core/modules/system/tests/fixtures/update/drupal-10.3.0.bare.standard.php.gz +++ b/core/modules/system/tests/fixtures/update/drupal-10.3.0.bare.standard.php.gz diff --git a/core/modules/system/tests/fixtures/update/drupal-10.3.0.filled.standard.php.gz b/core/modules/system/tests/fixtures/update/drupal-10.3.0.filled.standard.php.gz Binary files differindex 423f49a1d409..5db0b3a5aaeb 100644 --- a/core/modules/system/tests/fixtures/update/drupal-10.3.0.filled.standard.php.gz +++ b/core/modules/system/tests/fixtures/update/drupal-10.3.0.filled.standard.php.gz diff --git a/core/modules/system/tests/modules/form_test/src/Form/FormTestClickedButtonForm.php b/core/modules/system/tests/modules/form_test/src/Form/FormTestClickedButtonForm.php index 542c4e162e2d..78328f9f8e4d 100644 --- a/core/modules/system/tests/modules/form_test/src/Form/FormTestClickedButtonForm.php +++ b/core/modules/system/tests/modules/form_test/src/Form/FormTestClickedButtonForm.php @@ -35,32 +35,28 @@ class FormTestClickedButtonForm extends FormBase { '#type' => 'textfield', ]; + // Get button configurations, filter out NULL values. + $args = array_filter([$first, $second, $third]); + + // Define button types for each argument. + $button_types = [ + 's' => 'submit', + 'i' => 'image_button', + 'b' => 'button', + ]; + // Loop through each path argument, adding buttons based on the information // in the argument. For example, if the path is // form-test/clicked-button/s/i/rb, then 3 buttons are added: a 'submit', an // 'image_button', and a 'button' with #access=FALSE. This enables form.test // to test a variety of combinations. - $i = 0; - $args = [$first, $second, $third]; - foreach ($args as $arg) { - $name = 'button' . ++$i; - // 's', 'b', or 'i' in the argument define the button type wanted. - if (!is_string($arg)) { - $type = NULL; - } - elseif (str_contains($arg, 's')) { - $type = 'submit'; - } - elseif (str_contains($arg, 'b')) { - $type = 'button'; - } - elseif (str_contains($arg, 'i')) { - $type = 'image_button'; - } - else { - $type = NULL; - } - if (isset($type)) { + foreach ($args as $index => $arg) { + // Get the button type based on the index of the argument. + $type = $button_types[$arg] ?? NULL; + $name = 'button' . ($index + 1); + + if ($type) { + // Define the button. $form[$name] = [ '#type' => $type, '#name' => $name, diff --git a/core/modules/system/tests/modules/module_test_oop_preprocess/src/Hook/ModuleTestOopPreprocessThemeHooks.php b/core/modules/system/tests/modules/module_test_oop_preprocess/src/Hook/ModuleTestOopPreprocessThemeHooks.php index 1cbb9e6b422b..db923382a21b 100644 --- a/core/modules/system/tests/modules/module_test_oop_preprocess/src/Hook/ModuleTestOopPreprocessThemeHooks.php +++ b/core/modules/system/tests/modules/module_test_oop_preprocess/src/Hook/ModuleTestOopPreprocessThemeHooks.php @@ -4,19 +4,19 @@ declare(strict_types=1); namespace Drupal\module_test_oop_preprocess\Hook; -use Drupal\Core\Hook\Attribute\Preprocess; +use Drupal\Core\Hook\Attribute\Hook; /** * Hook implementations for module_test_oop_preprocess. */ class ModuleTestOopPreprocessThemeHooks { - #[Preprocess] + #[Hook('preprocess')] public function rootPreprocess($arg): mixed { return $arg; } - #[Preprocess('test')] + #[Hook('preprocess_test')] public function preprocessTest($arg): mixed { return $arg; } diff --git a/core/modules/system/tests/modules/theme_test/src/Hook/ThemeTestThemeHooks.php b/core/modules/system/tests/modules/theme_test/src/Hook/ThemeTestThemeHooks.php index fc48756de51a..7bfc10ef0ef9 100644 --- a/core/modules/system/tests/modules/theme_test/src/Hook/ThemeTestThemeHooks.php +++ b/core/modules/system/tests/modules/theme_test/src/Hook/ThemeTestThemeHooks.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Drupal\theme_test\Hook; -use Drupal\Core\Hook\Attribute\Preprocess; +use Drupal\Core\Hook\Attribute\Hook; /** * Hook implementations for theme_test. @@ -14,7 +14,7 @@ class ThemeTestThemeHooks { /** * Implements hook_preprocess_HOOK(). */ - #[Preprocess('theme_test_preprocess_suggestions__monkey')] + #[Hook('preprocess_theme_test_preprocess_suggestions__monkey')] public function preprocessTestSuggestions(&$variables): void { $variables['foo'] = 'Monkey'; } diff --git a/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTranslationTest.php b/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTranslationTest.php index 8d9465f61a3f..7fcb764eac3e 100644 --- a/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTranslationTest.php +++ b/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTranslationTest.php @@ -11,6 +11,7 @@ use Drupal\node\Entity\Node; * Upgrade taxonomy term node associations. * * @group migrate_drupal_6 + * @group #slow */ class MigrateTermNodeTranslationTest extends MigrateDrupal6TestBase { diff --git a/core/modules/views/tests/src/Functional/GlossaryTest.php b/core/modules/views/tests/src/Functional/GlossaryTest.php index 292f91767715..25c08d5f1590 100644 --- a/core/modules/views/tests/src/Functional/GlossaryTest.php +++ b/core/modules/views/tests/src/Functional/GlossaryTest.php @@ -83,7 +83,6 @@ class GlossaryTest extends ViewTestBase { 'url', 'user.node_grants:view', 'user.permissions', - 'route', ], [ 'config:views.view.glossary', diff --git a/core/modules/views/tests/src/Kernel/Handler/ArgumentSummaryTest.php b/core/modules/views/tests/src/Kernel/Handler/ArgumentSummaryTest.php index 03488125064a..e19f1414615f 100644 --- a/core/modules/views/tests/src/Kernel/Handler/ArgumentSummaryTest.php +++ b/core/modules/views/tests/src/Kernel/Handler/ArgumentSummaryTest.php @@ -150,4 +150,38 @@ class ArgumentSummaryTest extends ViewsKernelTestBase { $this->assertStringContainsString($tags[1]->label() . ' (2)', $output); } + /** + * Tests that the active link is set correctly. + */ + public function testActiveLink(): void { + require_once $this->root . '/core/modules/views/views.theme.inc'; + + // We need at least one node. + Node::create([ + 'type' => $this->nodeType->id(), + 'title' => $this->randomMachineName(), + ])->save(); + + $view = Views::getView('test_argument_summary'); + $view->execute(); + $view->build(); + $variables = [ + 'view' => $view, + 'rows' => $view->result, + ]; + + template_preprocess_views_view_summary_unformatted($variables); + $this->assertFalse($variables['rows'][0]->active); + + template_preprocess_views_view_summary($variables); + $this->assertFalse($variables['rows'][0]->active); + + // Checks that the row with the current path is active. + \Drupal::service('path.current')->setPath('/test-argument-summary'); + template_preprocess_views_view_summary_unformatted($variables); + $this->assertTrue($variables['rows'][0]->active); + template_preprocess_views_view_summary($variables); + $this->assertTrue($variables['rows'][0]->active); + } + } diff --git a/core/modules/views/views.theme.inc b/core/modules/views/views.theme.inc index 10c29c5dbf30..04c5de5a535f 100644 --- a/core/modules/views/views.theme.inc +++ b/core/modules/views/views.theme.inc @@ -253,15 +253,12 @@ function template_preprocess_views_view_summary(&$variables): void { $url_options['query'] = $view->exposed_raw_input; } + $currentPath = \Drupal::service('path.current')->getPath(); $active_urls = [ // Force system path. - Url::fromRoute('<current>', [], ['alias' => TRUE])->toString(), - // Force system path. - Url::fromRouteMatch(\Drupal::routeMatch())->setOption('alias', TRUE)->toString(), - // Could be an alias. - Url::fromRoute('<current>')->toString(), + Url::fromUserInput($currentPath, ['alias' => TRUE])->toString(), // Could be an alias. - Url::fromRouteMatch(\Drupal::routeMatch())->toString(), + Url::fromUserInput($currentPath)->toString(), ]; $active_urls = array_combine($active_urls, $active_urls); @@ -342,11 +339,12 @@ function template_preprocess_views_view_summary_unformatted(&$variables): void { } $count = 0; + $currentPath = \Drupal::service('path.current')->getPath(); $active_urls = [ // Force system path. - Url::fromRoute('<current>', [], ['alias' => TRUE])->toString(), + Url::fromUserInput($currentPath, ['alias' => TRUE])->toString(), // Could be an alias. - Url::fromRoute('<current>')->toString(), + Url::fromUserInput($currentPath)->toString(), ]; $active_urls = array_combine($active_urls, $active_urls); diff --git a/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php b/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php index 856c5fb25b20..f3a3196fab6e 100644 --- a/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php +++ b/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php @@ -90,6 +90,7 @@ class StandardPerformanceTest extends PerformanceTestBase { $expected_queries = [ 'SELECT "base_table"."id" AS "id", "base_table"."path" AS "path", "base_table"."alias" AS "alias", "base_table"."langcode" AS "langcode" FROM "path_alias" "base_table" WHERE ("base_table"."status" = 1) AND ("base_table"."alias" LIKE "/node" ESCAPE ' . "'\\\\'" . ') AND ("base_table"."langcode" IN ("en", "und")) ORDER BY "base_table"."langcode" ASC, "base_table"."id" DESC', 'SELECT "name", "route", "fit" FROM "router" WHERE "pattern_outline" IN ( "/node" ) AND "number_parts" >= 1', + 'SELECT 1 AS "expression" FROM "path_alias" "base_table" WHERE ("base_table"."status" = 1) AND ("base_table"."path" LIKE "/rss.xml%" ESCAPE ' . "'\\\\'" . ') LIMIT 1 OFFSET 0', 'SELECT COUNT(*) AS "expression" FROM (SELECT 1 AS "expression" FROM "node_field_data" "node_field_data" WHERE ("node_field_data"."promote" = 1) AND ("node_field_data"."status" = 1)) "subquery"', 'SELECT "node_field_data"."sticky" AS "node_field_data_sticky", "node_field_data"."created" AS "node_field_data_created", "node_field_data"."nid" AS "nid" FROM "node_field_data" "node_field_data" WHERE ("node_field_data"."promote" = 1) AND ("node_field_data"."status" = 1) ORDER BY "node_field_data_sticky" DESC, "node_field_data_created" DESC LIMIT 10 OFFSET 0', 'SELECT "revision"."vid" AS "vid", "revision"."langcode" AS "langcode", "revision"."revision_uid" AS "revision_uid", "revision"."revision_timestamp" AS "revision_timestamp", "revision"."revision_log" AS "revision_log", "revision"."revision_default" AS "revision_default", "base"."nid" AS "nid", "base"."type" AS "type", "base"."uuid" AS "uuid", CASE "base"."vid" WHEN "revision"."vid" THEN 1 ELSE 0 END AS "isDefaultRevision" FROM "node" "base" INNER JOIN "node_revision" "revision" ON "revision"."vid" = "base"."vid" WHERE "base"."nid" IN (1)', @@ -133,8 +134,8 @@ class StandardPerformanceTest extends PerformanceTestBase { $recorded_queries = $performance_data->getQueries(); $this->assertSame($expected_queries, $recorded_queries); $expected = [ - 'QueryCount' => 41, - 'CacheGetCount' => 101, + 'QueryCount' => 42, + 'CacheGetCount' => 100, 'CacheGetCountByBin' => [ 'page' => 1, 'config' => 21, @@ -142,7 +143,7 @@ class StandardPerformanceTest extends PerformanceTestBase { 'discovery' => 38, 'bootstrap' => 8, 'dynamic_page_cache' => 1, - 'render' => 14, + 'render' => 13, 'default' => 5, 'entity' => 2, 'menu' => 3, @@ -150,7 +151,7 @@ class StandardPerformanceTest extends PerformanceTestBase { 'CacheSetCount' => 47, 'CacheDeleteCount' => 0, 'CacheTagInvalidationCount' => 0, - 'CacheTagLookupQueryCount' => 17, + 'CacheTagLookupQueryCount' => 16, 'CacheTagGroupedLookups' => [ [ 'route_match', @@ -182,7 +183,6 @@ class StandardPerformanceTest extends PerformanceTestBase { ['config:block.block.stark_messages'], ['config:block.block.stark_help'], ['config:block.block.stark_powered'], - ['config:block.block.stark_syndicate'], [ 'config:block.block.stark_account_menu', 'config:block.block.stark_breadcrumbs', @@ -234,11 +234,11 @@ class StandardPerformanceTest extends PerformanceTestBase { $this->assertSame($expected_queries, $recorded_queries); $expected = [ 'QueryCount' => 10, - 'CacheGetCount' => 72, + 'CacheGetCount' => 71, 'CacheSetCount' => 16, 'CacheDeleteCount' => 0, 'CacheTagInvalidationCount' => 0, - 'CacheTagLookupQueryCount' => 14, + 'CacheTagLookupQueryCount' => 13, 'CacheTagGroupedLookups' => [ [ 'route_match', @@ -267,7 +267,6 @@ class StandardPerformanceTest extends PerformanceTestBase { ['config:block.block.stark_messages'], ['config:block.block.stark_help'], ['config:block.block.stark_powered'], - ['config:block.block.stark_syndicate'], [ 'config:block.block.stark_account_menu', 'config:block.block.stark_breadcrumbs', @@ -316,11 +315,11 @@ class StandardPerformanceTest extends PerformanceTestBase { $this->assertSame($expected_queries, $recorded_queries); $expected = [ 'QueryCount' => 14, - 'CacheGetCount' => 57, + 'CacheGetCount' => 56, 'CacheSetCount' => 17, 'CacheDeleteCount' => 0, 'CacheTagInvalidationCount' => 0, - 'CacheTagLookupQueryCount' => 13, + 'CacheTagLookupQueryCount' => 12, 'StylesheetCount' => 1, 'StylesheetBytes' => 1800, ]; @@ -375,11 +374,11 @@ class StandardPerformanceTest extends PerformanceTestBase { 'StylesheetBytes' => 1429, 'StylesheetCount' => 1, 'QueryCount' => 17, - 'CacheGetCount' => 69, + 'CacheGetCount' => 68, 'CacheSetCount' => 1, 'CacheDeleteCount' => 1, 'CacheTagInvalidationCount' => 0, - 'CacheTagLookupQueryCount' => 14, + 'CacheTagLookupQueryCount' => 13, 'CacheTagGroupedLookups' => [ // Form submission and login. [ @@ -421,7 +420,6 @@ class StandardPerformanceTest extends PerformanceTestBase { ['config:block.block.stark_messages'], ['config:block.block.stark_help'], ['config:block.block.stark_powered'], - ['config:block.block.stark_syndicate'], ['config:block.block.stark_main_menu'], [ 'config:block.block.stark_account_menu', diff --git a/core/recipes/standard/recipe.yml b/core/recipes/standard/recipe.yml index 4a88f8a9b695..12e764f0ff5d 100644 --- a/core/recipes/standard/recipe.yml +++ b/core/recipes/standard/recipe.yml @@ -59,7 +59,6 @@ config: - block.block.olivero_help - block.block.olivero_search_form_narrow - block.block.olivero_search_form_wide - - block.block.olivero_syndicate user: - core.entity_view_mode.user.compact - search.page.user_search diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php index 3bd0fe48af73..d33d7c4942ab 100644 --- a/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php +++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php @@ -63,25 +63,20 @@ class InstallerNonDefaultDatabaseDriverTest extends InstallerTestBase { // Assert that in the settings.php the database connection array has the // correct values set. - $contents = file_get_contents($this->container->getParameter('app.root') . '/' . $this->siteDirectory . '/settings.php'); - $this->assertStringContainsString("'namespace' => 'Drupal\\\\driver_test\\\\Driver\\\\Database\\\\{$this->testDriverName}',", $contents); - $this->assertStringContainsString("'driver' => '{$this->testDriverName}',", $contents); - $this->assertStringContainsString("'autoload' => 'core/modules/system/tests/modules/driver_test/src/Driver/Database/{$this->testDriverName}/',", $contents); - - $dependencies = "'dependencies' => " . PHP_EOL . - " array (" . PHP_EOL . - " 'mysql' => " . PHP_EOL . - " array (" . PHP_EOL . - " 'namespace' => 'Drupal\\\\mysql'," . PHP_EOL . - " 'autoload' => 'core/modules/mysql/src/'," . PHP_EOL . - " )," . PHP_EOL . - " 'pgsql' => " . PHP_EOL . - " array (" . PHP_EOL . - " 'namespace' => 'Drupal\\\\pgsql'," . PHP_EOL . - " 'autoload' => 'core/modules/pgsql/src/'," . PHP_EOL . - " )," . PHP_EOL . - " )," . PHP_EOL; - $this->assertStringContainsString($dependencies, $contents); + $installedDatabaseSettings = $this->getInstalledDatabaseSettings(); + $this->assertSame("Drupal\\driver_test\\Driver\\Database\\{$this->testDriverName}", $installedDatabaseSettings['default']['default']['namespace']); + $this->assertSame($this->testDriverName, $installedDatabaseSettings['default']['default']['driver']); + $this->assertSame("core/modules/system/tests/modules/driver_test/src/Driver/Database/{$this->testDriverName}/", $installedDatabaseSettings['default']['default']['autoload']); + $this->assertEquals([ + 'mysql' => [ + 'namespace' => 'Drupal\\mysql', + 'autoload' => 'core/modules/mysql/src/', + ], + 'pgsql' => [ + 'namespace' => 'Drupal\\pgsql', + 'autoload' => 'core/modules/pgsql/src/', + ], + ], $installedDatabaseSettings['default']['default']['dependencies']); // Assert that the module "driver_test" and its dependencies have been // installed. @@ -99,4 +94,22 @@ class InstallerNonDefaultDatabaseDriverTest extends InstallerTestBase { $this->assertSession()->elementTextContains('xpath', '//tr[@data-drupal-selector="edit-pgsql"]', "The following reason prevents PostgreSQL from being uninstalled: Required by: driver_test"); } + /** + * Returns the databases setup from the SUT's settings.php. + * + * @return array<string,mixed> + * The value of the $databases variable. + */ + protected function getInstalledDatabaseSettings(): array { + // The $app_root and $site_path variables are required by the settings.php + // file to be parsed correctly. The $databases variable is set in the + // included file, we need to inform PHPStan about that since PHPStan itself + // is unable to determine it. + $app_root = $this->container->getParameter('app.root'); + $site_path = $this->siteDirectory; + include $app_root . '/' . $site_path . '/settings.php'; + assert(isset($databases)); + return $databases; + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php index c361c7af9596..cbda6e3d7f72 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php +++ b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php @@ -432,9 +432,6 @@ class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase { /** * Tests rollback after a DDL statement when no transactional DDL supported. - * - * @todo In drupal:12.0.0, rollBack will throw a - * TransactionOutOfOrderException. Adjust the test accordingly. */ public function testRollbackAfterDdlStatementForNonTransactionalDdlDatabase(): void { if ($this->connection->supportsTransactionalDDL()) { @@ -919,9 +916,6 @@ class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase { * transaction including DDL statements is not possible, since a commit * happened already. We cannot decide what should be the status of the * callback, an exception is thrown. - * - * @todo In drupal:12.0.0, rollBack will throw a - * TransactionOutOfOrderException. Adjust the test accordingly. */ public function testRootTransactionEndCallbackFailureUponDdlAndRollbackForNonTransactionalDdlDatabase(): void { if ($this->connection->supportsTransactionalDDL()) { diff --git a/core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php b/core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php new file mode 100644 index 000000000000..f40a977f6f49 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php @@ -0,0 +1,1278 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\KernelTests\Core\Database; + +use Drupal\Core\Database\Database; +use Drupal\Core\Database\Transaction; +use Drupal\Core\Database\Transaction\ClientConnectionTransactionState; +use Drupal\Core\Database\Transaction\StackItem; +use Drupal\Core\Database\Transaction\StackItemType; +use Drupal\Core\Database\Transaction\TransactionManagerBase; +use Drupal\Core\Database\TransactionNameNonUniqueException; +use Drupal\Core\Database\TransactionOutOfOrderException; + +// cspell:ignore Tinky Winky Dipsy + +/** + * Tests the transactions, using the explicit ::commitOrRelease method. + * + * We test nesting by having two transaction layers, an outer and inner. The + * outer layer encapsulates the inner layer. Our transaction nesting abstraction + * should allow the outer layer function to call any function it wants, + * especially the inner layer that starts its own transaction, and be + * confident that, when the function it calls returns, its own transaction + * is still "alive." + * + * Call structure: + * transactionOuterLayer() + * Start transaction "A" + * transactionInnerLayer() + * Start transaction "B" (does nothing in database) + * [Maybe decide to roll back "B"] + * Do more stuff + * Should still be in transaction "A" + * + * These method can be overridden by non-core database driver if their + * transaction behavior is different from core. For example, both oci8 (Oracle) + * and mysqli (MySql) clients do not have a solution to check if a transaction + * is active, and mysqli does not fail when rolling back and no transaction + * active. + * + * @group Database + */ +class TransactionTest extends DatabaseTestBase { + + /** + * Keeps track of the post-transaction callback action executed. + */ + protected ?string $postTransactionCallbackAction = NULL; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + // Set the transaction manager to trigger warnings when appropriate. + $this->connection->transactionManager()->triggerWarningWhenUnpilingOnVoidTransaction = TRUE; + } + + /** + * Create a root Drupal transaction. + */ + protected function createRootTransaction(string $name = '', bool $insertRow = TRUE): Transaction { + $this->assertFalse($this->connection->inTransaction()); + $this->assertSame(0, $this->connection->transactionManager()->stackDepth()); + + // Start root transaction. Corresponds to 'BEGIN TRANSACTION' on the + // database. + $transaction = $this->connection->startTransaction($name); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + + // Insert a single row into the testing table. + if ($insertRow) { + $this->insertRow('David'); + $this->assertRowPresent('David'); + } + + return $transaction; + } + + /** + * Create a Drupal savepoint transaction after root. + */ + protected function createFirstSavepointTransaction(string $name = '', bool $insertRow = TRUE): Transaction { + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + + // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_1' + // on the database. The name can be changed by the $name argument. + $savepoint = $this->connection->startTransaction($name); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(2, $this->connection->transactionManager()->stackDepth()); + + // Insert a single row into the testing table. + if ($insertRow) { + $this->insertRow('Roger'); + $this->assertRowPresent('Roger'); + } + + return $savepoint; + } + + /** + * Encapsulates a transaction's "inner layer" with an "outer layer". + * + * This "outer layer" transaction starts and then encapsulates the "inner + * layer" transaction. This nesting is used to evaluate whether the database + * transaction API properly supports nesting. By "properly supports," we mean + * the outer transaction continues to exist regardless of what functions are + * called and whether those functions start their own transactions. + * + * In contrast, a typical database would commit the outer transaction, start + * a new transaction for the inner layer, commit the inner layer transaction, + * and then be confused when the outer layer transaction tries to commit its + * transaction (which was already committed when the inner transaction + * started). + * + * @param string $suffix + * Suffix to add to field values to differentiate tests. + */ + protected function transactionOuterLayer(string $suffix): void { + $txn = $this->connection->startTransaction(); + + // Insert a single row into the testing table. + $this->connection->insert('test') + ->fields([ + 'name' => 'David' . $suffix, + 'age' => '24', + ]) + ->execute(); + + $this->assertTrue($this->connection->inTransaction(), 'In transaction before calling nested transaction.'); + + // We're already in a transaction, but we call ->transactionInnerLayer + // to nest another transaction inside the current one. + $this->transactionInnerLayer($suffix); + + $this->assertTrue($this->connection->inTransaction(), 'In transaction after calling nested transaction.'); + + $txn->commitOrRelease(); + } + + /** + * Creates an "inner layer" transaction. + * + * This "inner layer" transaction is either used alone or nested inside of the + * "outer layer" transaction. + * + * @param string $suffix + * Suffix to add to field values to differentiate tests. + */ + protected function transactionInnerLayer(string $suffix): void { + $depth = $this->connection->transactionManager()->stackDepth(); + // Start a transaction. If we're being called from ->transactionOuterLayer, + // then we're already in a transaction. Normally, that would make starting + // a transaction here dangerous, but the database API handles this problem + // for us by tracking the nesting and avoiding the danger. + $txn = $this->connection->startTransaction(); + + $depth2 = $this->connection->transactionManager()->stackDepth(); + $this->assertSame($depth + 1, $depth2, 'Transaction depth has increased with new transaction.'); + + // Insert a single row into the testing table. + $this->connection->insert('test') + ->fields([ + 'name' => 'Daniel' . $suffix, + 'age' => '19', + ]) + ->execute(); + + $this->assertTrue($this->connection->inTransaction(), 'In transaction inside nested transaction.'); + + $txn->commitOrRelease(); + } + + /** + * Tests root transaction rollback. + */ + public function testRollbackRoot(): void { + $transaction = $this->createRootTransaction(); + + // Rollback. Since we are at the root, the transaction is closed. + // Corresponds to 'ROLLBACK' on the database. + $transaction->rollBack(); + $this->assertRowAbsent('David'); + $this->assertFalse($this->connection->inTransaction()); + $this->assertSame(0, $this->connection->transactionManager()->stackDepth()); + } + + /** + * Tests root transaction rollback after savepoint rollback. + */ + public function testRollbackRootAfterSavepointRollback(): void { + $transaction = $this->createRootTransaction(); + $savepoint = $this->createFirstSavepointTransaction(); + + // Rollback savepoint. It should get released too. Corresponds to 'ROLLBACK + // TO savepoint_1' plus 'RELEASE savepoint_1' on the database. + $savepoint->rollBack(); + $this->assertRowPresent('David'); + $this->assertRowAbsent('Roger'); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + + // Try to rollback root. No savepoint is active, this should succeed. + $transaction->rollBack(); + $this->assertRowAbsent('David'); + $this->assertRowAbsent('Roger'); + $this->assertFalse($this->connection->inTransaction()); + $this->assertSame(0, $this->connection->transactionManager()->stackDepth()); + } + + /** + * Tests root transaction rollback failure when savepoint is open. + */ + public function testRollbackRootWithActiveSavepoint(): void { + $transaction = $this->createRootTransaction(); + // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis + $savepoint = $this->createFirstSavepointTransaction(); + + // Try to rollback root. Since a savepoint is active, this should fail. + $this->expectException(TransactionOutOfOrderException::class); + $this->expectExceptionMessageMatches("/^Error attempting rollback of .*\\\\drupal_transaction\\. Active stack: .*\\\\drupal_transaction > .*\\\\savepoint_1/"); + $transaction->rollBack(); + } + + /** + * Tests savepoint transaction rollback. + */ + public function testRollbackSavepoint(): void { + $transaction = $this->createRootTransaction(); + $savepoint = $this->createFirstSavepointTransaction(); + + // Rollback savepoint. It should get released too. Corresponds to 'ROLLBACK + // TO savepoint_1' plus 'RELEASE savepoint_1' on the database. + $savepoint->rollBack(); + $this->assertRowPresent('David'); + $this->assertRowAbsent('Roger'); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + + // Insert a row. + $this->insertRow('Syd'); + + // Commit root. + $transaction->commitOrRelease(); + $this->assertRowPresent('David'); + $this->assertRowAbsent('Roger'); + $this->assertRowPresent('Syd'); + $this->assertFalse($this->connection->inTransaction()); + $this->assertSame(0, $this->connection->transactionManager()->stackDepth()); + } + + /** + * Tests savepoint transaction commit after rollback. + */ + public function testCommitAfterRollbackSameSavepoint(): void { + $transaction = $this->createRootTransaction(); + $savepoint = $this->createFirstSavepointTransaction(); + + // Rollback savepoint. It should get released too. Corresponds to 'ROLLBACK + // TO savepoint_1' plus 'RELEASE savepoint_1' on the database. + $savepoint->rollBack(); + $this->assertRowPresent('David'); + $this->assertRowAbsent('Roger'); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + + // Insert a row. + $this->insertRow('Syd'); + + // Try releasing savepoint. Should fail since it was released already. + try { + $savepoint->commitOrRelease(); + $this->fail('Expected TransactionOutOfOrderException was not thrown'); + } + catch (\Exception $e) { + $this->assertInstanceOf(TransactionOutOfOrderException::class, $e); + $this->assertMatchesRegularExpression("/^Error attempting commit of .*\\\\savepoint_1\\. Active stack: .*\\\\drupal_transaction/", $e->getMessage()); + } + $this->assertRowPresent('David'); + $this->assertRowAbsent('Roger'); + $this->assertRowPresent('Syd'); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + + // Commit root. + $transaction->commitOrRelease(); + $this->assertRowPresent('David'); + $this->assertRowAbsent('Roger'); + $this->assertRowPresent('Syd'); + $this->assertFalse($this->connection->inTransaction()); + $this->assertSame(0, $this->connection->transactionManager()->stackDepth()); + } + + /** + * Tests savepoint transaction rollback after commit. + */ + public function testRollbackAfterCommitSameSavepoint(): void { + $transaction = $this->createRootTransaction(); + $savepoint = $this->createFirstSavepointTransaction(); + + // Release savepoint. Corresponds to 'RELEASE savepoint_1' on the database. + $savepoint->commitOrRelease(); + $this->assertRowPresent('David'); + $this->assertRowPresent('Roger'); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + + // Insert a row. + $this->insertRow('Syd'); + + // Try rolling back savepoint. Should fail since it was released already. + try { + $savepoint->rollback(); + $this->fail('Expected TransactionOutOfOrderException was not thrown'); + } + catch (\Exception $e) { + $this->assertInstanceOf(TransactionOutOfOrderException::class, $e); + $this->assertMatchesRegularExpression("/^Error attempting rollback of .*\\\\savepoint_1\\. Active stack: .*\\\\drupal_transaction/", $e->getMessage()); + } + $this->assertRowPresent('David'); + $this->assertRowPresent('Roger'); + $this->assertRowPresent('Syd'); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + + // Commit root. + $transaction->commitOrRelease(); + $this->assertRowPresent('David'); + $this->assertRowPresent('Roger'); + $this->assertRowPresent('Syd'); + $this->assertFalse($this->connection->inTransaction()); + $this->assertSame(0, $this->connection->transactionManager()->stackDepth()); + } + + /** + * Tests savepoint transaction duplicated rollback. + */ + public function testRollbackTwiceSameSavepoint(): void { + // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis + $transaction = $this->createRootTransaction(); + $savepoint = $this->createFirstSavepointTransaction(); + + // Rollback savepoint. It should get released too. Corresponds to 'ROLLBACK + // TO savepoint_1' plus 'RELEASE savepoint_1' on the database. + $savepoint->rollBack(); + $this->assertRowPresent('David'); + $this->assertRowAbsent('Roger'); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + + // Insert a row. + $this->insertRow('Syd'); + + // Rollback savepoint again. Should fail since it was released already. + try { + $savepoint->rollBack(); + $this->fail('Expected TransactionOutOfOrderException was not thrown'); + } + catch (\Exception $e) { + $this->assertInstanceOf(TransactionOutOfOrderException::class, $e); + $this->assertMatchesRegularExpression("/^Error attempting rollback of .*\\\\savepoint_1\\. Active stack: .*\\\\drupal_transaction/", $e->getMessage()); + } + $this->assertRowPresent('David'); + $this->assertRowAbsent('Roger'); + $this->assertRowPresent('Syd'); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + } + + /** + * Tests savepoint transaction rollback failure when later savepoints exist. + */ + public function testRollbackSavepointWithLaterSavepoint(): void { + // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis + $transaction = $this->createRootTransaction(); + $savepoint1 = $this->createFirstSavepointTransaction(); + + // Starts another savepoint transaction. Corresponds to 'SAVEPOINT + // savepoint_2' on the database. + // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis + $savepoint2 = $this->connection->startTransaction(); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(3, $this->connection->transactionManager()->stackDepth()); + + // Insert a row. + $this->insertRow('Syd'); + $this->assertRowPresent('David'); + $this->assertRowPresent('Roger'); + $this->assertRowPresent('Syd'); + + // Try to rollback to savepoint 1. Out of order. + $this->expectException(TransactionOutOfOrderException::class); + $this->expectExceptionMessageMatches("/^Error attempting rollback of .*\\\\savepoint_1\\. Active stack: .*\\\\drupal_transaction > .*\\\\savepoint_1 > .*\\\\savepoint_2/"); + $savepoint1->rollBack(); + } + + /** + * Tests commit does not fail when committing after DDL. + * + * In core, SQLite and PostgreSql databases support transactional DDL, MySql + * does not. + */ + public function testCommitAfterDdl(): void { + $transaction = $this->createRootTransaction(); + $savepoint = $this->createFirstSavepointTransaction(); + + $this->executeDDLStatement(); + + $this->assertRowPresent('David'); + $this->assertRowPresent('Roger'); + if ($this->connection->supportsTransactionalDDL()) { + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(2, $this->connection->transactionManager()->stackDepth()); + } + else { + $this->assertFalse($this->connection->inTransaction()); + } + + $this->assertRowPresent('David'); + $this->assertRowPresent('Roger'); + if ($this->connection->supportsTransactionalDDL()) { + $savepoint->commitOrRelease(); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + } + else { + set_error_handler(static function (int $errno, string $errstr): bool { + throw new \ErrorException($errstr); + }); + try { + $savepoint->commitOrRelease(); + } + catch (\ErrorException $e) { + $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage()); + } + finally { + restore_error_handler(); + } + $this->assertFalse($this->connection->inTransaction()); + } + + if ($this->connection->supportsTransactionalDDL()) { + $transaction->commitOrRelease(); + } + else { + set_error_handler(static function (int $errno, string $errstr): bool { + throw new \ErrorException($errstr); + }); + try { + $transaction->commitOrRelease(); + } + catch (\ErrorException $e) { + $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage()); + } + finally { + restore_error_handler(); + } + } + $this->assertRowPresent('David'); + $this->assertRowPresent('Roger'); + $this->assertFalse($this->connection->inTransaction()); + } + + /** + * Tests a committed transaction. + * + * The behavior of this test should be identical for connections that support + * transactions and those that do not. + */ + public function testCommittedTransaction(): void { + // Create two nested transactions. The changes should be committed. + $this->transactionOuterLayer('A'); + + // Because we committed, both of the inserted rows should be present. + $saved_age = $this->connection->query('SELECT [age] FROM {test} WHERE [name] = :name', [':name' => 'DavidA'])->fetchField(); + $this->assertSame('24', $saved_age, 'Can retrieve DavidA row after commit.'); + $saved_age = $this->connection->query('SELECT [age] FROM {test} WHERE [name] = :name', [':name' => 'DanielA'])->fetchField(); + $this->assertSame('19', $saved_age, 'Can retrieve DanielA row after commit.'); + } + + /** + * Tests the compatibility of transactions with DDL statements. + */ + public function testTransactionWithDdlStatement(): void { + // First, test that a commit works normally, even with DDL statements. + $transaction = $this->createRootTransaction('', FALSE); + $this->insertRow('row'); + $this->executeDDLStatement(); + if ($this->connection->supportsTransactionalDDL()) { + $transaction->commitOrRelease(); + } + else { + set_error_handler(static function (int $errno, string $errstr): bool { + throw new \ErrorException($errstr); + }); + try { + $transaction->commitOrRelease(); + } + catch (\ErrorException $e) { + $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage()); + } + finally { + restore_error_handler(); + } + } + $this->assertRowPresent('row'); + + // Even in different order. + $this->cleanUp(); + $transaction = $this->createRootTransaction('', FALSE); + $this->executeDDLStatement(); + $this->insertRow('row'); + if ($this->connection->supportsTransactionalDDL()) { + $transaction->commitOrRelease(); + } + else { + set_error_handler(static function (int $errno, string $errstr): bool { + throw new \ErrorException($errstr); + }); + try { + $transaction->commitOrRelease(); + } + catch (\ErrorException $e) { + $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage()); + } + finally { + restore_error_handler(); + } + } + $this->assertRowPresent('row'); + + // Even with stacking. + $this->cleanUp(); + $transaction = $this->createRootTransaction('', FALSE); + $transaction2 = $this->createFirstSavepointTransaction('', FALSE); + $this->executeDDLStatement(); + if ($this->connection->supportsTransactionalDDL()) { + $transaction2->commitOrRelease(); + } + else { + set_error_handler(static function (int $errno, string $errstr): bool { + throw new \ErrorException($errstr); + }); + try { + $transaction2->commitOrRelease(); + } + catch (\ErrorException $e) { + $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage()); + } + finally { + restore_error_handler(); + } + } + $transaction3 = $this->connection->startTransaction(); + $this->insertRow('row'); + $transaction3->commitOrRelease(); + + if ($this->connection->supportsTransactionalDDL()) { + $transaction->commitOrRelease(); + } + else { + try { + $transaction->commitOrRelease(); + $this->fail('TransactionOutOfOrderException was expected, but did not throw.'); + } + catch (TransactionOutOfOrderException) { + // Just continue, this is out or order since $transaction3 started a + // new root. + } + } + $this->assertRowPresent('row'); + + // A transaction after a DDL statement should still work the same. + $this->cleanUp(); + $transaction = $this->createRootTransaction('', FALSE); + $transaction2 = $this->createFirstSavepointTransaction('', FALSE); + $this->executeDDLStatement(); + if ($this->connection->supportsTransactionalDDL()) { + $transaction2->commitOrRelease(); + } + else { + set_error_handler(static function (int $errno, string $errstr): bool { + throw new \ErrorException($errstr); + }); + try { + $transaction2->commitOrRelease(); + } + catch (\ErrorException $e) { + $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage()); + } + finally { + restore_error_handler(); + } + } + $transaction3 = $this->connection->startTransaction(); + $this->insertRow('row'); + $transaction3->rollBack(); + if ($this->connection->supportsTransactionalDDL()) { + $transaction->commitOrRelease(); + } + else { + try { + $transaction->commitOrRelease(); + $this->fail('TransactionOutOfOrderException was expected, but did not throw.'); + } + catch (TransactionOutOfOrderException) { + // Just continue, this is out or order since $transaction3 started a + // new root. + } + } + $this->assertRowAbsent('row'); + + // The behavior of a rollback depends on the type of database server. + if ($this->connection->supportsTransactionalDDL()) { + // For database servers that support transactional DDL, a rollback + // of a transaction including DDL statements should be possible. + $this->cleanUp(); + $transaction = $this->createRootTransaction('', FALSE); + $this->insertRow('row'); + $this->executeDDLStatement(); + $transaction->rollBack(); + $this->assertRowAbsent('row'); + + // Including with stacking. + $this->cleanUp(); + $transaction = $this->createRootTransaction('', FALSE); + $transaction2 = $this->createFirstSavepointTransaction('', FALSE); + $this->executeDDLStatement(); + $transaction2->commitOrRelease(); + $transaction3 = $this->connection->startTransaction(); + $this->insertRow('row'); + $transaction3->commitOrRelease(); + $this->assertRowPresent('row'); + $transaction->rollBack(); + $this->assertRowAbsent('row'); + } + } + + /** + * Tests rollback after a DDL statement when no transactional DDL supported. + */ + public function testRollbackAfterDdlStatementForNonTransactionalDdlDatabase(): void { + if ($this->connection->supportsTransactionalDDL()) { + $this->markTestSkipped('This test only works for database that do not support transactional DDL.'); + } + + // For database servers that do not support transactional DDL, + // the DDL statement should commit the transaction stack. + $this->cleanUp(); + $transaction = $this->createRootTransaction('', FALSE); + $reflectionMethod = new \ReflectionMethod(get_class($this->connection->transactionManager()), 'getConnectionTransactionState'); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + $this->assertEquals(ClientConnectionTransactionState::Active, $reflectionMethod->invoke($this->connection->transactionManager())); + $this->insertRow('row'); + $this->executeDDLStatement(); + $this->assertSame(0, $this->connection->transactionManager()->stackDepth()); + $this->assertEquals(ClientConnectionTransactionState::Voided, $reflectionMethod->invoke($this->connection->transactionManager())); + + // Try to rollback the root transaction. Since the DDL already committed + // it, it should fail. + set_error_handler(static function (int $errno, string $errstr): bool { + throw new \ErrorException($errstr); + }); + try { + $transaction->rollBack(); + } + catch (\ErrorException $e) { + $this->assertSame('Transaction::rollBack() failed because of a prior execution of a DDL statement.', $e->getMessage()); + } + finally { + restore_error_handler(); + } + + try { + $transaction->commitOrRelease(); + $this->fail('TransactionOutOfOrderException was expected, but did not throw.'); + } + catch (TransactionOutOfOrderException) { + // Just continue, the attempted rollback made the overall state to + // ClientConnectionTransactionState::RollbackFailed. + } + + $manager = $this->connection->transactionManager(); + $this->assertSame(0, $manager->stackDepth()); + $reflectedTransactionState = new \ReflectionMethod($manager, 'getConnectionTransactionState'); + $this->assertSame(ClientConnectionTransactionState::RollbackFailed, $reflectedTransactionState->invoke($manager)); + $this->assertRowPresent('row'); + } + + /** + * Inserts a single row into the testing table. + */ + protected function insertRow(string $name): void { + $this->connection->insert('test') + ->fields([ + 'name' => $name, + ]) + ->execute(); + } + + /** + * Executes a DDL statement. + */ + protected function executeDDLStatement(): void { + static $count = 0; + $table = [ + 'fields' => [ + 'id' => [ + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ], + ], + 'primary key' => ['id'], + ]; + $this->connection->schema()->createTable('database_test_' . ++$count, $table); + } + + /** + * Starts over for a new test. + */ + protected function cleanUp(): void { + $this->connection->truncate('test') + ->execute(); + $this->postTransactionCallbackAction = NULL; + $this->assertSame(0, $this->connection->transactionManager()->stackDepth()); + } + + /** + * Asserts that a given row is present in the test table. + * + * @param string $name + * The name of the row. + * @param string $message + * The message to log for the assertion. + * + * @internal + */ + public function assertRowPresent(string $name, ?string $message = NULL): void { + $present = (boolean) $this->connection->query('SELECT 1 FROM {test} WHERE [name] = :name', [':name' => $name])->fetchField(); + $this->assertTrue($present, $message ?? "Row '{$name}' should be present, but it actually does not exist."); + } + + /** + * Asserts that a given row is absent from the test table. + * + * @param string $name + * The name of the row. + * @param string $message + * The message to log for the assertion. + * + * @internal + */ + public function assertRowAbsent(string $name, ?string $message = NULL): void { + $present = (boolean) $this->connection->query('SELECT 1 FROM {test} WHERE [name] = :name', [':name' => $name])->fetchField(); + $this->assertFalse($present, $message ?? "Row '{$name}' should be absent, but it actually exists."); + } + + /** + * Tests transaction stacking, commit, and rollback. + */ + public function testTransactionStacking(): void { + // Standard case: pop the inner transaction before the outer transaction. + $transaction = $this->createRootTransaction('', FALSE); + $this->insertRow('outer'); + $transaction2 = $this->createFirstSavepointTransaction('', FALSE); + $this->insertRow('inner'); + // Pop the inner transaction. + $transaction2->commitOrRelease(); + $this->assertTrue($this->connection->inTransaction(), 'Still in a transaction after popping the inner transaction'); + // Pop the outer transaction. + $transaction->commitOrRelease(); + $this->assertFalse($this->connection->inTransaction(), 'Transaction closed after popping the outer transaction'); + $this->assertRowPresent('outer'); + $this->assertRowPresent('inner'); + + // Rollback the inner transaction. + $this->cleanUp(); + $transaction = $this->createRootTransaction('', FALSE); + $this->insertRow('outer'); + $transaction2 = $this->createFirstSavepointTransaction('', FALSE); + $this->insertRow('inner'); + // Now rollback the inner transaction. + $transaction2->rollBack(); + $this->assertTrue($this->connection->inTransaction(), 'Still in a transaction after popping the outer transaction'); + // Pop the outer transaction, it should commit. + $this->insertRow('outer-after-inner-rollback'); + $transaction->commitOrRelease(); + $this->assertFalse($this->connection->inTransaction(), 'Transaction closed after popping the inner transaction'); + $this->assertRowPresent('outer'); + $this->assertRowAbsent('inner'); + $this->assertRowPresent('outer-after-inner-rollback'); + } + + /** + * Tests that transactions can continue to be used if a query fails. + */ + public function testQueryFailureInTransaction(): void { + $transaction = $this->createRootTransaction('test_transaction', FALSE); + $this->connection->schema()->dropTable('test'); + + // Test a failed query using the query() method. + try { + $this->connection->query('SELECT [age] FROM {test} WHERE [name] = :name', [':name' => 'David'])->fetchField(); + $this->fail('Using the query method should have failed.'); + } + catch (\Exception) { + // Just continue testing. + } + + // Test a failed select query. + try { + $this->connection->select('test') + ->fields('test', ['name']) + ->execute(); + + $this->fail('Select query should have failed.'); + } + catch (\Exception) { + // Just continue testing. + } + + // Test a failed insert query. + try { + $this->connection->insert('test') + ->fields([ + 'name' => 'David', + 'age' => '24', + ]) + ->execute(); + + $this->fail('Insert query should have failed.'); + } + catch (\Exception) { + // Just continue testing. + } + + // Test a failed update query. + try { + $this->connection->update('test') + ->fields(['name' => 'Tiffany']) + ->condition('id', 1) + ->execute(); + + $this->fail('Update query should have failed.'); + } + catch (\Exception) { + // Just continue testing. + } + + // Test a failed delete query. + try { + $this->connection->delete('test') + ->condition('id', 1) + ->execute(); + + $this->fail('Delete query should have failed.'); + } + catch (\Exception) { + // Just continue testing. + } + + // Test a failed merge query. + try { + $this->connection->merge('test') + ->key('job', 'Presenter') + ->fields([ + 'age' => '31', + 'name' => 'Tiffany', + ]) + ->execute(); + + $this->fail('Merge query should have failed.'); + } + catch (\Exception) { + // Just continue testing. + } + + // Test a failed upsert query. + try { + $this->connection->upsert('test') + ->key('job') + ->fields(['job', 'age', 'name']) + ->values([ + 'job' => 'Presenter', + 'age' => 31, + 'name' => 'Tiffany', + ]) + ->execute(); + + $this->fail('Upsert query should have failed.'); + } + catch (\Exception) { + // Just continue testing. + } + + // Create the missing schema and insert a row. + $this->installSchema('database_test', ['test']); + $this->connection->insert('test') + ->fields([ + 'name' => 'David', + 'age' => '24', + ]) + ->execute(); + + // Commit the transaction. + if ($this->connection->supportsTransactionalDDL()) { + $transaction->commitOrRelease(); + } + else { + set_error_handler(static function (int $errno, string $errstr): bool { + throw new \ErrorException($errstr); + }); + try { + $transaction->commitOrRelease(); + } + catch (\ErrorException $e) { + $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage()); + } + finally { + restore_error_handler(); + } + } + + $saved_age = $this->connection->query('SELECT [age] FROM {test} WHERE [name] = :name', [':name' => 'David'])->fetchField(); + $this->assertEquals('24', $saved_age); + } + + /** + * Tests releasing a savepoint before last is safe. + */ + public function testReleaseIntermediateSavepoint(): void { + $transaction = $this->createRootTransaction(); + $savepoint1 = $this->createFirstSavepointTransaction('', FALSE); + + // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_2' + // on the database. + $savepoint2 = $this->connection->startTransaction(); + $this->assertSame(3, $this->connection->transactionManager()->stackDepth()); + // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_3' + // on the database. + // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis + $savepoint3 = $this->connection->startTransaction(); + $this->assertSame(4, $this->connection->transactionManager()->stackDepth()); + // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_4' + // on the database. + // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis + $savepoint4 = $this->connection->startTransaction(); + $this->assertSame(5, $this->connection->transactionManager()->stackDepth()); + + $this->insertRow('row'); + + // Release savepoint transaction. Corresponds to 'RELEASE SAVEPOINT + // savepoint_2' on the database. + $savepoint2->commitOrRelease(); + // Since we have committed an intermediate savepoint Transaction object, + // the savepoints created later have been dropped by the database already. + $this->assertSame(2, $this->connection->transactionManager()->stackDepth()); + $this->assertRowPresent('row'); + + // Commit the remaining Transaction objects. The client transaction is + // eventually committed. + $savepoint1->commitOrRelease(); + $transaction->commitOrRelease(); + $this->assertFalse($this->connection->inTransaction()); + $this->assertRowPresent('row'); + } + + /** + * Tests committing a transaction while savepoints are active. + */ + public function testCommitWithActiveSavepoint(): void { + $transaction = $this->createRootTransaction(); + // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis + $savepoint1 = $this->createFirstSavepointTransaction('', FALSE); + + // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_2' + // on the database. + $savepoint2 = $this->connection->startTransaction(); + $this->assertSame(3, $this->connection->transactionManager()->stackDepth()); + + $this->insertRow('row'); + + // Commit the root transaction. + $transaction->commitOrRelease(); + // Since we have committed the outer (root) Transaction object, the inner + // (savepoint) ones have been dropped by the database already, and we are + // no longer in an active transaction state. + $this->assertSame(0, $this->connection->transactionManager()->stackDepth()); + $this->assertFalse($this->connection->inTransaction()); + $this->assertRowPresent('row'); + // Trying to release the inner (savepoint) Transaction object, throws an + // exception since it was dropped by the database already, and removed from + // our transaction stack. + $this->expectException(TransactionOutOfOrderException::class); + $this->expectExceptionMessageMatches("/^Error attempting commit of .*\\\\savepoint_2\\. Active stack: .* empty/"); + $savepoint2->commitOrRelease(); + } + + /** + * Tests for transaction names. + */ + public function testTransactionName(): void { + $transaction = $this->createRootTransaction('', FALSE); + $this->assertSame('drupal_transaction', $transaction->name()); + + $savepoint1 = $this->createFirstSavepointTransaction('', FALSE); + $this->assertSame('savepoint_1', $savepoint1->name()); + + $this->expectException(TransactionNameNonUniqueException::class); + $this->expectExceptionMessage("savepoint_1 is already in use."); + $this->connection->startTransaction('savepoint_1'); + } + + /** + * Tests for arbitrary transaction names. + */ + public function testArbitraryTransactionNames(): void { + $transaction = $this->createRootTransaction('TinkyWinky', FALSE); + // Despite setting a name, the root transaction is always named + // 'drupal_transaction'. + $this->assertSame('drupal_transaction', $transaction->name()); + + $savepoint1 = $this->createFirstSavepointTransaction('Dipsy', FALSE); + $this->assertSame('Dipsy', $savepoint1->name()); + + $this->expectException(TransactionNameNonUniqueException::class); + $this->expectExceptionMessage("Dipsy is already in use."); + $this->connection->startTransaction('Dipsy'); + } + + /** + * Tests that adding a post-transaction callback fails with no transaction. + */ + public function testRootTransactionEndCallbackAddedWithoutTransaction(): void { + $this->expectException(\LogicException::class); + $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']); + } + + /** + * Tests post-transaction callback executes after transaction commit. + */ + public function testRootTransactionEndCallbackCalledOnCommit(): void { + $transaction = $this->createRootTransaction('', FALSE); + $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']); + $this->insertRow('row'); + $this->assertNull($this->postTransactionCallbackAction); + $this->assertRowAbsent('rtcCommit'); + + // Callbacks are processed only when destructing the transaction. + // Executing a commit is not sufficient by itself. + $transaction->commitOrRelease(); + $this->assertNull($this->postTransactionCallbackAction); + $this->assertRowPresent('row'); + $this->assertRowAbsent('rtcCommit'); + + // Destruct the transaction. + unset($transaction); + + // The post-transaction callback should now have inserted a 'rtcCommit' + // row. + $this->assertSame('rtcCommit', $this->postTransactionCallbackAction); + $this->assertRowPresent('row'); + $this->assertRowPresent('rtcCommit'); + } + + /** + * Tests post-transaction callback executes after transaction rollback. + */ + public function testRootTransactionEndCallbackCalledAfterRollbackAndDestruction(): void { + $transaction = $this->createRootTransaction('', FALSE); + $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']); + $this->insertRow('row'); + $this->assertNull($this->postTransactionCallbackAction); + + // Callbacks are processed only when destructing the transaction. + // Executing a rollback is not sufficient by itself. + $transaction->rollBack(); + $this->assertNull($this->postTransactionCallbackAction); + $this->assertRowAbsent('rtcCommit'); + $this->assertRowAbsent('rtcRollback'); + $this->assertRowAbsent('row'); + + // Destruct the transaction. + unset($transaction); + + // The post-transaction callback should now have inserted a 'rtcRollback' + // row. + $this->assertSame('rtcRollback', $this->postTransactionCallbackAction); + $this->assertRowAbsent('rtcCommit'); + $this->assertRowPresent('rtcRollback'); + $this->assertRowAbsent('row'); + } + + /** + * Tests post-transaction callback executes after a DDL statement. + */ + public function testRootTransactionEndCallbackCalledAfterDdlAndDestruction(): void { + $transaction = $this->createRootTransaction('', FALSE); + $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']); + $this->insertRow('row'); + $this->assertNull($this->postTransactionCallbackAction); + + // Callbacks are processed only when destructing the transaction. + // Executing a DDL statement is not sufficient itself. + // We cannot use truncate here, since it has protective code to fall back + // to a transactional delete when in transaction. We drop an unrelated + // table instead. + $this->connection->schema()->dropTable('test_people'); + $this->assertNull($this->postTransactionCallbackAction); + $this->assertRowAbsent('rtcCommit'); + $this->assertRowAbsent('rtcRollback'); + $this->assertRowPresent('row'); + + // Destruct the transaction. + unset($transaction); + + // The post-transaction callback should now have inserted a 'rtcCommit' + // row. + $this->assertSame('rtcCommit', $this->postTransactionCallbackAction); + $this->assertRowPresent('rtcCommit'); + $this->assertRowAbsent('rtcRollback'); + $this->assertRowPresent('row'); + } + + /** + * Tests post-transaction rollback executes after a DDL statement. + * + * For database servers that support transactional DDL, a rollback of a + * transaction including DDL statements is possible. + */ + public function testRootTransactionEndCallbackCalledAfterDdlAndRollbackForTransactionalDdlDatabase(): void { + if (!$this->connection->supportsTransactionalDDL()) { + $this->markTestSkipped('This test only works for database supporting transactional DDL.'); + } + + $transaction = $this->createRootTransaction('', FALSE); + $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']); + $this->insertRow('row'); + $this->assertNull($this->postTransactionCallbackAction); + + // Callbacks are processed only when destructing the transaction. + // Executing a DDL statement is not sufficient itself. + // We cannot use truncate here, since it has protective code to fall back + // to a transactional delete when in transaction. We drop an unrelated + // table instead. + $this->connection->schema()->dropTable('test_people'); + $this->assertNull($this->postTransactionCallbackAction); + $this->assertRowAbsent('rtcCommit'); + $this->assertRowAbsent('rtcRollback'); + $this->assertRowPresent('row'); + + // Callbacks are processed only when destructing the transaction. + // Executing the rollback is not sufficient by itself. + $transaction->rollBack(); + $this->assertNull($this->postTransactionCallbackAction); + $this->assertRowAbsent('rtcCommit'); + $this->assertRowAbsent('rtcRollback'); + $this->assertRowAbsent('row'); + + // Destruct the transaction. + unset($transaction); + + // The post-transaction callback should now have inserted a 'rtcRollback' + // row. + $this->assertSame('rtcRollback', $this->postTransactionCallbackAction); + $this->assertRowAbsent('rtcCommit'); + $this->assertRowPresent('rtcRollback'); + $this->assertRowAbsent('row'); + } + + /** + * Tests post-transaction rollback failure after a DDL statement. + * + * For database servers that support transactional DDL, a rollback of a + * transaction including DDL statements is not possible, since a commit + * happened already. We cannot decide what should be the status of the + * callback, an exception is thrown. + */ + public function testRootTransactionEndCallbackFailureUponDdlAndRollbackForNonTransactionalDdlDatabase(): void { + if ($this->connection->supportsTransactionalDDL()) { + $this->markTestSkipped('This test only works for database that do not support transactional DDL.'); + } + + $transaction = $this->createRootTransaction('', FALSE); + $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']); + $this->insertRow('row'); + $this->assertNull($this->postTransactionCallbackAction); + + // Callbacks are processed only when destructing the transaction. + // Executing a DDL statement is not sufficient itself. + // We cannot use truncate here, since it has protective code to fall back + // to a transactional delete when in transaction. We drop an unrelated + // table instead. + $this->connection->schema()->dropTable('test_people'); + $this->assertNull($this->postTransactionCallbackAction); + $this->assertRowAbsent('rtcCommit'); + $this->assertRowAbsent('rtcRollback'); + $this->assertRowPresent('row'); + + set_error_handler(static function (int $errno, string $errstr): bool { + throw new \ErrorException($errstr); + }); + try { + $transaction->rollBack(); + } + catch (\ErrorException $e) { + $this->assertSame('Transaction::rollBack() failed because of a prior execution of a DDL statement.', $e->getMessage()); + } + finally { + restore_error_handler(); + } + + unset($transaction); + + // The post-transaction callback should now have inserted a 'rtcRollback' + // row. + $this->assertSame('rtcRollback', $this->postTransactionCallbackAction); + $this->assertRowAbsent('rtcCommit'); + $this->assertRowPresent('rtcRollback'); + $manager = $this->connection->transactionManager(); + $this->assertSame(0, $manager->stackDepth()); + $reflectedTransactionState = new \ReflectionMethod($manager, 'getConnectionTransactionState'); + $this->assertSame(ClientConnectionTransactionState::RollbackFailed, $reflectedTransactionState->invoke($manager)); + $this->assertRowPresent('row'); + } + + /** + * A post-transaction callback for testing purposes. + */ + public function rootTransactionCallback(bool $success): void { + $this->postTransactionCallbackAction = $success ? 'rtcCommit' : 'rtcRollback'; + $this->insertRow($this->postTransactionCallbackAction); + } + + /** + * Tests TransactionManager failure. + */ + public function testTransactionManagerFailureOnPendingStackItems(): void { + $connectionInfo = Database::getConnectionInfo(); + Database::addConnectionInfo('default', 'test_fail', $connectionInfo['default']); + $testConnection = Database::getConnection('test_fail'); + + // Add a fake item to the stack. + $manager = $testConnection->transactionManager(); + $reflectionMethod = new \ReflectionMethod($manager, 'addStackItem'); + $reflectionMethod->invoke($manager, 'bar', new StackItem('qux', StackItemType::Root)); + // Ensure transaction state can be determined during object destruction. + // This is necessary for the test to pass when xdebug.mode has the 'develop' + // option enabled. + $reflectionProperty = new \ReflectionProperty(TransactionManagerBase::class, 'connectionTransactionState'); + $reflectionProperty->setValue($manager, ClientConnectionTransactionState::Active); + + // Ensure that __destruct() results in an assertion error. Note that this + // will normally be called by PHP during the object's destruction but Drupal + // will commit all transactions when a database is closed thereby making + // this impossible to test unless it is called directly. + try { + $manager->__destruct(); + $this->fail("Expected AssertionError error not thrown"); + } + catch (\AssertionError $e) { + $this->assertStringStartsWith('Transaction $stack was not empty. Active stack: bar\\qux', $e->getMessage()); + } + + // Clean up. + $reflectionProperty = new \ReflectionProperty(TransactionManagerBase::class, 'stack'); + $reflectionProperty->setValue($manager, []); + unset($testConnection); + Database::closeConnection('test_fail'); + } + +} diff --git a/core/themes/olivero/config/optional/block.block.olivero_syndicate.yml b/core/themes/olivero/config/optional/block.block.olivero_syndicate.yml deleted file mode 100644 index 343b8d3256a5..000000000000 --- a/core/themes/olivero/config/optional/block.block.olivero_syndicate.yml +++ /dev/null @@ -1,20 +0,0 @@ -langcode: en -status: true -dependencies: - module: - - node - theme: - - olivero -id: olivero_syndicate -theme: olivero -region: social -weight: 0 -provider: null -plugin: node_syndicate_block -settings: - id: node_syndicate_block - label: 'RSS feed' - label_display: '0' - provider: node - block_count: 10 -visibility: { } |