summaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--core/.deprecation-ignore.txt50
-rw-r--r--core/MAINTAINERS.txt2
-rw-r--r--core/lib/Drupal/Core/Database/Transaction.php30
-rw-r--r--core/lib/Drupal/Core/Database/Transaction/TransactionManagerBase.php134
-rw-r--r--core/lib/Drupal/Core/Database/Transaction/TransactionManagerInterface.php4
-rw-r--r--core/lib/Drupal/Core/Hook/Attribute/FormAlter.php54
-rw-r--r--core/lib/Drupal/Core/Hook/Attribute/Hook.php26
-rw-r--r--core/lib/Drupal/Core/Hook/Attribute/Preprocess.php23
-rw-r--r--core/lib/Drupal/Core/Render/Renderer.php15
-rw-r--r--core/lib/Drupal/Core/Test/SimpletestTestRunResultsStorage.php6
-rw-r--r--core/misc/cspell/dictionary.txt2
-rw-r--r--core/misc/drupal.js5
-rw-r--r--core/modules/block/block.module4
-rw-r--r--core/modules/block/migrations/d6_block.yml2
-rw-r--r--core/modules/block/migrations/d7_block.yml2
-rw-r--r--core/modules/block/src/Hook/BlockHooks.php7
-rw-r--r--core/modules/block/tests/src/Kernel/BlockConfigSchemaTest.php4
-rw-r--r--core/modules/block/tests/src/Kernel/BlockConfigSyncTest.php86
-rw-r--r--core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php2
-rw-r--r--core/modules/block_content/src/Controller/BlockContentController.php5
-rw-r--r--core/modules/block_content/src/Plugin/Menu/LocalAction/BlockContentAddLocalAction.php6
-rw-r--r--core/modules/block_content/tests/src/Functional/BlockContentCreationTest.php6
-rw-r--r--core/modules/block_content/tests/src/Functional/LocalActionTest.php53
-rw-r--r--core/modules/comment/src/Hook/CommentThemeHooks.php4
-rw-r--r--core/modules/comment/tests/modules/comment_empty_title_test/src/Hook/CommentEmptyTitleTestThemeHooks.php4
-rw-r--r--core/modules/config_translation/migrations/d6_block_translation.yml2
-rw-r--r--core/modules/config_translation/migrations/d7_block_translation.yml2
-rw-r--r--core/modules/contact/src/Hook/ContactFormHooks.php6
-rw-r--r--core/modules/contextual/src/Hook/ContextualThemeHooks.php4
-rw-r--r--core/modules/help/src/HelpTopicTwigLoader.php2
-rw-r--r--core/modules/help/src/HelpTwigExtension.php2
-rw-r--r--core/modules/help/tests/modules/help_topics_twig_tester/src/HelpTestTwigExtension.php2
-rw-r--r--core/modules/help/tests/modules/help_topics_twig_tester/src/HelpTestTwigNodeVisitor.php2
-rw-r--r--core/modules/help/tests/src/Unit/HelpTopicTwigTest.php6
-rw-r--r--core/modules/locale/src/Hook/LocaleThemeHooks.php4
-rw-r--r--core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php6
-rw-r--r--core/modules/migrate/tests/src/Unit/MigrateSourceTest.php27
-rw-r--r--core/modules/migrate_drupal/tests/src/Kernel/d7/FieldDiscoveryTest.php1
-rw-r--r--core/modules/migrate_drupal/tests/src/Kernel/d7/MigrateDrupal7AuditIdsTest.php1
-rw-r--r--core/modules/migrate_drupal_ui/tests/src/Functional/d6/Upgrade6Test.php2
-rw-r--r--core/modules/migrate_drupal_ui/tests/src/Functional/d7/Upgrade7Test.php2
-rw-r--r--core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php6
-rw-r--r--core/modules/node/js/node.preview.js7
-rw-r--r--core/modules/node/src/Controller/NodeController.php3
-rw-r--r--core/modules/node/src/Hook/NodeHooks.php9
-rw-r--r--core/modules/node/src/Hook/NodeThemeHooks.php4
-rw-r--r--core/modules/node/src/Plugin/Block/SyndicateBlock.php6
-rw-r--r--core/modules/node/tests/src/Functional/NodeRevisionsAuthorTest.php102
-rw-r--r--core/modules/node/tests/src/Functional/NodeRevisionsUiTest.php16
-rw-r--r--core/modules/node/tests/src/Functional/NodeSyndicateBlockTest.php2
-rw-r--r--core/modules/node/tests/src/Functional/NodeTranslationUITest.php24
-rw-r--r--core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeCompleteTest.php1
-rw-r--r--core/modules/package_manager/package_manager.api.php7
-rw-r--r--core/modules/package_manager/package_manager.services.yml7
-rw-r--r--core/modules/package_manager/src/Attribute/AllowDirectWrite.php21
-rw-r--r--core/modules/package_manager/src/ComposerInspector.php2
-rw-r--r--core/modules/package_manager/src/DirectWritePreconditionBypass.php98
-rw-r--r--core/modules/package_manager/src/EventSubscriber/ChangeLogger.php10
-rw-r--r--core/modules/package_manager/src/EventSubscriber/DirectWriteSubscriber.php92
-rw-r--r--core/modules/package_manager/src/SandboxManagerBase.php80
-rw-r--r--core/modules/package_manager/src/Validator/LockFileValidator.php6
-rw-r--r--core/modules/package_manager/src/Validator/RsyncValidator.php6
-rw-r--r--core/modules/package_manager/tests/modules/package_manager_test_api/src/ApiController.php2
-rw-r--r--core/modules/package_manager/tests/src/Build/PackageInstallTest.php7
-rw-r--r--core/modules/package_manager/tests/src/Build/TemplateProjectTestBase.php2
-rw-r--r--core/modules/package_manager/tests/src/Kernel/ComposerInspectorTest.php2
-rw-r--r--core/modules/package_manager/tests/src/Kernel/DirectWriteTest.php234
-rw-r--r--core/modules/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php22
-rw-r--r--core/modules/package_manager/tests/src/Kernel/RsyncValidatorTest.php9
-rw-r--r--core/modules/page_cache/tests/src/Functional/PageCacheTagsIntegrationTest.php2
-rw-r--r--core/modules/system/tests/fixtures/update/drupal-10.3.0.bare.standard.php.gzbin162581 -> 162168 bytes
-rw-r--r--core/modules/system/tests/fixtures/update/drupal-10.3.0.filled.standard.php.gzbin596855 -> 596593 bytes
-rw-r--r--core/modules/system/tests/modules/form_test/src/Form/FormTestClickedButtonForm.php38
-rw-r--r--core/modules/system/tests/modules/module_test_oop_preprocess/src/Hook/ModuleTestOopPreprocessThemeHooks.php6
-rw-r--r--core/modules/system/tests/modules/theme_test/src/Hook/ThemeTestThemeHooks.php4
-rw-r--r--core/modules/system/tests/modules/twig_loader_test/src/Loader/TestLoader.php2
-rw-r--r--core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTranslationTest.php1
-rw-r--r--core/modules/toolbar/js/escapeAdmin.js2
-rw-r--r--core/modules/toolbar/tests/src/Nightwatch/Tests/toolbarTest.js27
-rw-r--r--core/modules/toolbar/tests/src/Nightwatch/ToolbarTestSetup.php38
-rw-r--r--core/modules/views/js/ajax_view.js2
-rw-r--r--core/modules/views/src/Entity/View.php2
-rw-r--r--core/modules/views/src/Form/ViewsExposedForm.php1
-rw-r--r--core/modules/views/tests/modules/views_test_config/test_views/views.view.test_third_party_uninstall.yml37
-rw-r--r--core/modules/views/tests/modules/views_third_party_settings_test/config/schema/views_third_party_settings_test.schema.yml7
-rw-r--r--core/modules/views/tests/modules/views_third_party_settings_test/views_third_party_settings_test.info.yml8
-rw-r--r--core/modules/views/tests/src/Functional/GlossaryTest.php1
-rw-r--r--core/modules/views/tests/src/Kernel/Handler/ArgumentSummaryTest.php34
-rw-r--r--core/modules/views/tests/src/Kernel/Plugin/ExposedFormRenderTest.php5
-rw-r--r--core/modules/views/tests/src/Kernel/ThirdPartyUninstallTest.php55
-rw-r--r--core/modules/views/views.theme.inc14
-rw-r--r--core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php24
-rw-r--r--core/recipes/standard/recipe.yml1
-rw-r--r--core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php51
-rw-r--r--core/tests/Drupal/KernelTests/Core/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php3
-rw-r--r--core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php6
-rw-r--r--core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php1278
-rw-r--r--core/tests/Drupal/Tests/Core/Template/StubTwigTemplate.php43
-rw-r--r--core/themes/claro/css/components/breadcrumb.pcss.css2
-rw-r--r--core/themes/claro/css/components/card.pcss.css2
-rw-r--r--core/themes/claro/css/components/details.css1
-rw-r--r--core/themes/claro/css/components/details.pcss.css8
-rw-r--r--core/themes/claro/css/components/form--checkbox-radio.pcss.css2
-rw-r--r--core/themes/claro/css/components/form--managed-file.pcss.css10
-rw-r--r--core/themes/claro/css/components/form--password-confirm.pcss.css6
-rw-r--r--core/themes/claro/css/components/form--select.pcss.css7
-rw-r--r--core/themes/claro/css/components/messages.pcss.css4
-rw-r--r--core/themes/claro/css/components/page-title.pcss.css2
-rw-r--r--core/themes/claro/css/components/shortcut.pcss.css10
-rw-r--r--core/themes/claro/css/components/system-admin--admin-list.pcss.css2
-rw-r--r--core/themes/claro/css/components/system-status-counter.css7
-rw-r--r--core/themes/claro/css/components/system-status-counter.pcss.css9
-rw-r--r--core/themes/claro/css/components/tabledrag.pcss.css12
-rw-r--r--core/themes/claro/css/components/vertical-tabs.pcss.css4
-rw-r--r--core/themes/claro/css/components/views_ui.admin.pcss.css2
-rw-r--r--core/themes/olivero/config/optional/block.block.olivero_syndicate.yml20
116 files changed, 2695 insertions, 501 deletions
diff --git a/core/.deprecation-ignore.txt b/core/.deprecation-ignore.txt
index 3eb0b15f07a..12772fbb55b 100644
--- a/core/.deprecation-ignore.txt
+++ b/core/.deprecation-ignore.txt
@@ -2,37 +2,33 @@
# deprecated code.
# See https://www.drupal.org/node/3285162 for more details.
-%The "Symfony\\Component\\Validator\\Context\\ExecutionContextInterface::.*\(\)" method is considered internal Used by the validator engine\. (Should not be called by user\W+code\. )?It may change without further notice\. You should not extend it from "[^"]+"\.%
+# @todo Remove when we no longer support PHPUnit 10.
+%The "PHPUnit\\Framework\\TestCase::__construct\(\)" method is considered internal.* You should not extend it from "Drupal\\[^"]+"%
-# Skip some dependencies' DebugClassLoader forward compatibility warnings.
-%Method "Behat\\[^"]+" might add "[^"]+" as a native return type declaration in the future. Do the same in (child class|implementation) "[^"]+" now to avoid errors or add an explicit @return annotation to suppress this message%
-%Method "Doctrine\\Common\\Annotations\\Reader::[^"]+" might add "[^"]+" as a native return type declaration in the future. Do the same in (child class|implementation) "[^"]+" now to avoid errors or add an explicit @return annotation to suppress this message%
-%Method "Twig\\Extension\\ExtensionInterface::[^"]+" might add "[^"]+" as a native return type declaration in the future. Do the same in (child class|implementation) "[^"]+" now to avoid errors or add an explicit @return annotation to suppress this message%
-%Method "Twig\\Loader\\FilesystemLoader::findTemplate\(\)" might add "[^"]+" as a native return type declaration in the future. Do the same in (child class|implementation) "[^"]+" now to avoid errors or add an explicit @return annotation to suppress this message%
-%Method "Twig\\Loader\\LoaderInterface::exists\(\)" might add "[^"]+" as a native return type declaration in the future. Do the same in (child class|implementation) "[^"]+" now to avoid errors or add an explicit @return annotation to suppress this message%
-%Method "Twig\\Node\\Node::compile\(\)" might add "[^"]+" as a native return type declaration in the future. Do the same in (child class|implementation) "[^"]+" now to avoid errors or add an explicit @return annotation to suppress this message%
-%Method "Twig\\NodeVisitor\\AbstractNodeVisitor::[^"]+" might add "[^"]+" as a native return type declaration in the future. Do the same in (child class|implementation) "[^"]+" now to avoid errors or add an explicit @return annotation to suppress this message%
-%Method "Twig\\NodeVisitor\\NodeVisitorInterface::[^"]+" might add "[^"]+" as a native return type declaration in the future. Do the same in (child class|implementation) "[^"]+" now to avoid errors or add an explicit @return annotation to suppress this message%
-%Method "Twig\\TokenParser\\TokenParserInterface::[^"]+" might add "[^"]+" as a native return type declaration in the future. Do the same in (child class|implementation) "[^"]+" now to avoid errors or add an explicit @return annotation to suppress this message%
-%Method "WebDriver\\Service\\CurlServiceInterface::[^"]+" might add "[^"]+" as a native return type declaration in the future. Do the same in implementation "[^"]+" now to avoid errors or add an explicit @return annotation to suppress this message%
-
-# Indirect deprecations. These are not in Drupal's remit to fix, but it is
-# worth keeping track of dependencies' issues.
-%Method "[^"]+" might add "[^"]+" as a native return type declaration in the future. Do the same in implementation "org\\bovigo\\vfs\\[^"]+" now to avoid errors or add an explicit @return annotation to suppress this message%
-%Method "[^"]+" might add "[^"]+" as a native return type declaration in the future. Do the same in (child class|implementation) "OpenTelemetry\\[^"]+" now to avoid errors or add an explicit @return annotation to suppress this message%
-
-# The following deprecation is listed for Twig 2 compatibility when unit
-# testing using \Symfony\Component\ErrorHandler\DebugClassLoader.
-%The "Twig\\Template" class is considered internal\. It may change without further notice\. You should not use it from "Drupal\\Tests\\Core\\Template\\StubTwigTemplate"\.%
+# Internal code that we cannot avoid extending.
+%The "PHPUnit\\Framework\\TestCase::__construct\(\)" method is considered final.* You should not extend it from "Drupal\\[^"]+"%
%The "Twig\\Environment::getTemplateClass\(\)" method is considered internal\. It may change without further notice\. You should not extend it from "Drupal\\Core\\Template\\TwigEnvironment"\.%
-# PHPUnit 10.
-%The "PHPUnit\\Framework\\TestCase::__construct\(\)" method is considered internal.*You should not extend it from "Drupal\\[^"]+"%
+# Skip some dependencies' DebugClassLoader forward compatibility warnings, in
+# order to let contrib modules make their necessary fixes first.
+%Method "Behat\\Mink\\Driver\\CoreDriver::[^"]+" might add "void" as a native return type declaration in the future. Do the same in child class "Drupal\\FunctionalJavascriptTests\\DrupalSelenium2Driver" now to avoid errors or add an explicit @return annotation to suppress this message%
+%Method "Behat\\Mink\\WebAssert::[^"]+" might add "void" as a native return type declaration in the future. Do the same in child class "Drupal\\FunctionalJavascriptTests\\WebDriverWebAssert" now to avoid errors or add an explicit @return annotation to suppress this message%
+%Method "Behat\\Mink\\WebAssert::[^"]+" might add "void" as a native return type declaration in the future. Do the same in child class "Drupal\\Tests\\WebAssert" now to avoid errors or add an explicit @return annotation to suppress this message%
+%Method "Doctrine\\Common\\Annotations\\Reader::[^"]+" might add "[^"]+" as a native return type declaration in the future. Do the same in implementation "Drupal\\Component\\Annotation\\Doctrine\\SimpleAnnotationReader" now to avoid errors or add an explicit @return annotation to suppress this message%
+%Method "Twig\\Extension\\ExtensionInterface::[^"]+" might add "array" as a native return type declaration in the future. Do the same in implementation "Drupal\\Core\\Template\\TwigExtension" now to avoid errors or add an explicit @return annotation to suppress this message%
+%Method "Twig\\Loader\\FilesystemLoader::findTemplate\(\)" might add "\?string" as a native return type declaration in the future. Do the same in child class "Drupal\\Core\\Template\\Loader\\FilesystemLoader" now to avoid errors or add an explicit @return annotation to suppress this message%
+%Method "Twig\\Loader\\LoaderInterface::exists\(\)" might add "bool" as a native return type declaration in the future. Do the same in implementation "Drupal\\Core\\Template\\Loader\\StringLoader" now to avoid errors or add an explicit @return annotation to suppress this message%
+%Method "Twig\\Node\\Node::compile\(\)" might add "void" as a native return type declaration in the future. Do the same in child class "Drupal\\Core\\Template\\TwigNodeCheckDeprecations" now to avoid errors or add an explicit @return annotation to suppress this message%
+%Method "Twig\\Node\\Node::compile\(\)" might add "void" as a native return type declaration in the future. Do the same in child class "Drupal\\Core\\Template\\TwigNodeTrans" now to avoid errors or add an explicit @return annotation to suppress this message%
+%Method "Twig\\NodeVisitor\\NodeVisitorInterface::[^"]+" might add "[^"]+" as a native return type declaration in the future. Do the same in implementation "Drupal\\Core\\Template\\RemoveCheckToStringNodeVisitor" now to avoid errors or add an explicit @return annotation to suppress this message%
+%Method "Twig\\NodeVisitor\\NodeVisitorInterface::[^"]+" might add "[^"]+" as a native return type declaration in the future. Do the same in implementation "Drupal\\Core\\Template\\TwigNodeVisitor" now to avoid errors or add an explicit @return annotation to suppress this message%
+%Method "Twig\\TokenParser\\TokenParserInterface::[^"]+" might add "[^"]+" as a native return type declaration in the future. Do the same in implementation "Drupal\\Core\\Template\\TwigTransTokenParser" now to avoid errors or add an explicit @return annotation to suppress this message%
-# PHPUnit 11.
-%The "PHPUnit\\Framework\\TestCase::__construct\(\)" method is considered final\. It may change without further notice as of its next major version\. You should not extend it from "Drupal\\[^"]+"%
+# Indirect deprecations. These are not in Drupal's remit to fix, but it is
+# worth keeping track of dependencies' issues.
+%Method "Iterator::[^"]+" might add "void" as a native return type declaration in the future. Do the same in implementation "org\\bovigo\\vfs\\vfsStreamContainerIterator" now to avoid errors or add an explicit @return annotation to suppress this message%
-# Symfony 7.2
+# Symfony 7.2.
%Since symfony/http-foundation 7.2: NativeSessionStorage's "sid_length" option is deprecated and will be ignored in Symfony 8.0.%
%Since symfony/http-foundation 7.2: NativeSessionStorage's "sid_bits_per_character" option is deprecated and will be ignored in Symfony 8.0.%
@@ -44,5 +40,5 @@
%The "Drupal\\Core\\Entity\\Query\\QueryBase::hasAllTags\(\)" method will require a new "string \.\.\. \$tags" argument in the next major version of its interface%
%The "Drupal\\Core\\Entity\\Query\\QueryBase::hasAnyTag\(\)" method will require a new "string \.\.\. \$tags" argument in the next major version of its interface%
-# Symfony 7.3
+# Symfony 7.3.
%Since symfony/validator 7.3: Passing an array of options to configure the "[^"]+" constraint is deprecated, use named arguments instead.%
diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt
index e44df814938..f1856e58306 100644
--- a/core/MAINTAINERS.txt
+++ b/core/MAINTAINERS.txt
@@ -228,6 +228,7 @@ Filter
Form API
- Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia
- Tim Plunkett 'tim.plunkett' https://www.drupal.org/u/tim.plunkett
+- Lee Rowlands 'larowlan' https://www.drupal.org/u/larowlan
History
- Andrey Postnikov 'andypost' https://www.drupal.org/u/andypost
@@ -354,6 +355,7 @@ Recipes
Render API
- Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia
- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman
+- Lee Rowlands 'larowlan' https://www.drupal.org/u/larowlan
Request Processing
- ?
diff --git a/core/lib/Drupal/Core/Database/Transaction.php b/core/lib/Drupal/Core/Database/Transaction.php
index dcecc44e17c..b8693e4bb76 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 aa663d94226..fa1a309a767 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 11af511f14b..a9aa2c77052 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 158010463d2..00000000000
--- 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 34dbc8ebf91..0084e651180 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 47642859a20..00000000000
--- 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 d0150fe0127..fe3f29ea696 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 cb754e1afaa..0c000d675c3 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/misc/cspell/dictionary.txt b/core/misc/cspell/dictionary.txt
index 24999a781ac..69b32dfa718 100644
--- a/core/misc/cspell/dictionary.txt
+++ b/core/misc/cspell/dictionary.txt
@@ -427,6 +427,8 @@ rowspans
rtsp
ruleset
sameorigin
+sandboxed
+sandboxing
savepoints
sayre
schemaapi
diff --git a/core/misc/drupal.js b/core/misc/drupal.js
index 416c4f415a5..641c461a802 100644
--- a/core/misc/drupal.js
+++ b/core/misc/drupal.js
@@ -404,7 +404,6 @@ window.Drupal = { behaviors: {}, locale: {} };
*
* @see https://github.com/angular/angular.js/blob/v1.4.4/src/ng/urlUtils.js
* @see https://grack.com/blog/2009/11/17/absolutizing-url-in-javascript
- * @see https://github.com/jquery/jquery-ui/blob/1.11.4/ui/tabs.js#L53
*/
Drupal.url.toAbsolute = function (url) {
const urlParsingNode = document.createElement('a');
@@ -419,9 +418,7 @@ window.Drupal = { behaviors: {}, locale: {} };
urlParsingNode.setAttribute('href', url);
- // IE <= 7 normalizes the URL when assigned to the anchor node similar to
- // the other browsers.
- return urlParsingNode.cloneNode(false).href;
+ return urlParsingNode.href;
};
/**
diff --git a/core/modules/block/block.module b/core/modules/block/block.module
index 94a2cb9fc7a..24e28589491 100644
--- a/core/modules/block/block.module
+++ b/core/modules/block/block.module
@@ -16,6 +16,10 @@ use Drupal\Core\Installer\InstallerKernel;
* @see block_modules_installed()
*/
function block_themes_installed($theme_list): void {
+ // Do not create blocks during config sync.
+ if (\Drupal::service('config.installer')->isSyncing()) {
+ return;
+ }
// Disable this functionality prior to install profile installation because
// block configuration is often optional or provided by the install profile
// itself. block_theme_initialize() will be called when the install profile is
diff --git a/core/modules/block/migrations/d6_block.yml b/core/modules/block/migrations/d6_block.yml
index 74922444e8d..853ce28a47b 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 9b031b7daa7..35c6f23d86f 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/src/Hook/BlockHooks.php b/core/modules/block/src/Hook/BlockHooks.php
index 657109309a3..802a60bccb1 100644
--- a/core/modules/block/src/Hook/BlockHooks.php
+++ b/core/modules/block/src/Hook/BlockHooks.php
@@ -151,7 +151,12 @@ class BlockHooks {
* @see block_themes_installed()
*/
#[Hook('modules_installed')]
- public function modulesInstalled($modules): void {
+ public function modulesInstalled($modules, bool $is_syncing): void {
+ // Do not create blocks during config sync.
+ if ($is_syncing) {
+ return;
+ }
+
// block_themes_installed() does not call block_theme_initialize() during
// site installation because block configuration can be optional or provided
// by the profile. Now, when the profile is installed, this configuration
diff --git a/core/modules/block/tests/src/Kernel/BlockConfigSchemaTest.php b/core/modules/block/tests/src/Kernel/BlockConfigSchemaTest.php
index 8b2ead48eda..6305ab7f841 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/BlockConfigSyncTest.php b/core/modules/block/tests/src/Kernel/BlockConfigSyncTest.php
new file mode 100644
index 00000000000..80e3f798342
--- /dev/null
+++ b/core/modules/block/tests/src/Kernel/BlockConfigSyncTest.php
@@ -0,0 +1,86 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\block\Kernel;
+
+use Drupal\Core\Config\ConfigInstallerInterface;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Extension\ThemeInstallerInterface;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\block\Entity\Block;
+
+/**
+ * Tests that blocks are not created during config sync.
+ *
+ * @group block
+ */
+class BlockConfigSyncTest extends KernelTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $modules = ['block', 'system'];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+
+ \Drupal::service(ThemeInstallerInterface::class)
+ ->install(['stark', 'claro']);
+
+ // Delete all existing blocks.
+ foreach (Block::loadMultiple() as $block) {
+ $block->delete();
+ }
+
+ // Set the default theme.
+ $this->config('system.theme')
+ ->set('default', 'stark')
+ ->save();
+
+ // Create a block for the default theme to be copied later.
+ Block::create([
+ 'id' => 'test_block',
+ 'plugin' => 'system_powered_by_block',
+ 'region' => 'content',
+ 'theme' => 'stark',
+ ])->save();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function register(ContainerBuilder $container): void {
+ parent::register($container);
+ $container->setParameter('install_profile', 'testing');
+ }
+
+ /**
+ * Tests blocks are not created during config sync.
+ *
+ * @param bool $syncing
+ * Whether or not config is syncing when the hook is invoked.
+ * @param string|null $expected_block_id
+ * The expected ID of the block that should be created, or NULL if no block
+ * should be created.
+ *
+ * @testWith [true, null]
+ * [false, "claro_test_block"]
+ */
+ public function testNoBlocksCreatedDuringConfigSync(bool $syncing, ?string $expected_block_id): void {
+ \Drupal::service(ConfigInstallerInterface::class)
+ ->setSyncing($syncing);
+
+ // Invoke the hook that should skip block creation due to config sync.
+ \Drupal::moduleHandler()->invoke('block', 'themes_installed', [['claro']]);
+ // This should hold true if the "current" install profile triggers an
+ // invocation of hook_modules_installed().
+ \Drupal::moduleHandler()->invoke('block', 'modules_installed', [['testing'], $syncing]);
+
+ $this->assertSame($expected_block_id, Block::load('claro_test_block')?->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 3f20b2148b8..dc96d95e699 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 b2776f51d7d..77f8eee7939 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 844e06895cc..4e6c3b141e7 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 bca42cd3e32..364b5f4524d 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 00000000000..bb1a20df880
--- /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 e789af6dab1..c137d586d41 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 db1ffae5a6d..01a40394b40 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 6d57fdae1be..7925c49626f 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 9c82ee6b678..d2530e3b50a 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 ad8223c3ec6..b31b929bddf 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 760a42c9785..7d873196b43 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/help/src/HelpTopicTwigLoader.php b/core/modules/help/src/HelpTopicTwigLoader.php
index fc2e61bbaaf..9178166597c 100644
--- a/core/modules/help/src/HelpTopicTwigLoader.php
+++ b/core/modules/help/src/HelpTopicTwigLoader.php
@@ -96,7 +96,7 @@ class HelpTopicTwigLoader extends FilesystemLoader {
/**
* {@inheritdoc}
*/
- protected function findTemplate($name, $throw = TRUE) {
+ protected function findTemplate($name, $throw = TRUE): ?string {
if (!str_ends_with($name, '.html.twig')) {
if (!$throw) {
return NULL;
diff --git a/core/modules/help/src/HelpTwigExtension.php b/core/modules/help/src/HelpTwigExtension.php
index e41ad66503d..b8a77a914f6 100644
--- a/core/modules/help/src/HelpTwigExtension.php
+++ b/core/modules/help/src/HelpTwigExtension.php
@@ -41,7 +41,7 @@ class HelpTwigExtension extends AbstractExtension {
/**
* {@inheritdoc}
*/
- public function getFunctions() {
+ public function getFunctions(): array {
return [
new TwigFunction('help_route_link', [$this, 'getRouteLink']),
new TwigFunction('help_topic_link', [$this, 'getTopicLink']),
diff --git a/core/modules/help/tests/modules/help_topics_twig_tester/src/HelpTestTwigExtension.php b/core/modules/help/tests/modules/help_topics_twig_tester/src/HelpTestTwigExtension.php
index f54e15e882a..abe16ebdb48 100644
--- a/core/modules/help/tests/modules/help_topics_twig_tester/src/HelpTestTwigExtension.php
+++ b/core/modules/help/tests/modules/help_topics_twig_tester/src/HelpTestTwigExtension.php
@@ -14,7 +14,7 @@ class HelpTestTwigExtension extends AbstractExtension {
/**
* {@inheritdoc}
*/
- public function getNodeVisitors() {
+ public function getNodeVisitors(): array {
return [
new HelpTestTwigNodeVisitor(),
];
diff --git a/core/modules/help/tests/modules/help_topics_twig_tester/src/HelpTestTwigNodeVisitor.php b/core/modules/help/tests/modules/help_topics_twig_tester/src/HelpTestTwigNodeVisitor.php
index 953f2aa2ce4..9c53a2e0cf3 100644
--- a/core/modules/help/tests/modules/help_topics_twig_tester/src/HelpTestTwigNodeVisitor.php
+++ b/core/modules/help/tests/modules/help_topics_twig_tester/src/HelpTestTwigNodeVisitor.php
@@ -97,7 +97,7 @@ class HelpTestTwigNodeVisitor implements NodeVisitorInterface {
/**
* {@inheritdoc}
*/
- public function getPriority() {
+ public function getPriority(): int {
return -100;
}
diff --git a/core/modules/help/tests/src/Unit/HelpTopicTwigTest.php b/core/modules/help/tests/src/Unit/HelpTopicTwigTest.php
index 1e182076608..13e6bdffda1 100644
--- a/core/modules/help/tests/src/Unit/HelpTopicTwigTest.php
+++ b/core/modules/help/tests/src/Unit/HelpTopicTwigTest.php
@@ -6,8 +6,8 @@ namespace Drupal\Tests\help\Unit;
use Drupal\Core\Cache\Cache;
use Drupal\help\HelpTopicTwig;
-use Drupal\Tests\Core\Template\StubTwigTemplate;
use Drupal\Tests\UnitTestCase;
+use Twig\Template;
use Twig\TemplateWrapper;
/**
@@ -101,8 +101,8 @@ class HelpTopicTwigTest extends UnitTestCase {
->getMock();
$template = $this
- ->getMockBuilder(StubTwigTemplate::class)
- ->onlyMethods(['render'])
+ ->getMockBuilder(Template::class)
+ ->onlyMethods(['render', 'getTemplateName', 'getDebugInfo', 'getSourceContext', 'doDisplay'])
->setConstructorArgs([$twig])
->getMock();
diff --git a/core/modules/locale/src/Hook/LocaleThemeHooks.php b/core/modules/locale/src/Hook/LocaleThemeHooks.php
index d1e438f50ac..4ef5ca0b498 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 3a1cb8a1b69..77c8b45d00f 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 2f0b85ffbc4..e344e3e23e8 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 1f54f94848e..efe2b150928 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 ca8a9a0d06b..27ab60bc0c0 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 64dc7a1ea86..daf06a65468 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 46b3447e159..f9b702d22e3 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 ae9ae566f8d..5bf9d2477f0 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/js/node.preview.js b/core/modules/node/js/node.preview.js
index 50bc58ade77..e23be0b71e2 100644
--- a/core/modules/node/js/node.preview.js
+++ b/core/modules/node/js/node.preview.js
@@ -34,13 +34,13 @@
const $previewDialog = $(
`<div>${Drupal.theme('nodePreviewModal')}</div>`,
).appendTo('body');
- Drupal.dialog($previewDialog, {
+ const confirmationDialog = Drupal.dialog($previewDialog, {
title: Drupal.t('Leave preview?'),
buttons: [
{
text: Drupal.t('Cancel'),
click() {
- $(this).dialog('close');
+ confirmationDialog.close();
},
},
{
@@ -50,7 +50,8 @@
},
},
],
- }).showModal();
+ });
+ confirmationDialog.showModal();
}
}
diff --git a/core/modules/node/src/Controller/NodeController.php b/core/modules/node/src/Controller/NodeController.php
index e860d0c1d2a..87c9586daee 100644
--- a/core/modules/node/src/Controller/NodeController.php
+++ b/core/modules/node/src/Controller/NodeController.php
@@ -3,7 +3,6 @@
namespace Drupal\node\Controller;
use Drupal\Component\Utility\Xss;
-use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
@@ -200,7 +199,7 @@ class NodeController extends ControllerBase implements ContainerInjectionInterfa
],
];
// @todo Simplify once https://www.drupal.org/node/2334319 lands.
- $this->renderer->addCacheableDependency($column['data'], CacheableMetadata::createFromRenderArray($username));
+ $this->renderer->addCacheableDependency($column['data'], $username);
$row[] = $column;
if ($is_current_revision) {
diff --git a/core/modules/node/src/Hook/NodeHooks.php b/core/modules/node/src/Hook/NodeHooks.php
index d5f84e0359b..8a6b4d887c8 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 7ee443c458f..7ed0ef91f5f 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 b10c63527e5..45cfe1eb45c 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 00000000000..5a930df3e2d
--- /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/NodeRevisionsUiTest.php b/core/modules/node/tests/src/Functional/NodeRevisionsUiTest.php
index 201d4b6c7d2..88fe3e34e3e 100644
--- a/core/modules/node/tests/src/Functional/NodeRevisionsUiTest.php
+++ b/core/modules/node/tests/src/Functional/NodeRevisionsUiTest.php
@@ -215,20 +215,4 @@ class NodeRevisionsUiTest extends NodeTestBase {
$this->assertSession()->elementsCount('xpath', $xpath, 1);
}
- /**
- * Tests the node revisions page is cacheable by dynamic page cache.
- */
- public function testNodeRevisionsCacheability(): void {
- $this->drupalLogin($this->editor);
- $node = $this->drupalCreateNode();
- // Admin paths are always uncacheable by dynamic page cache, swap node
- // to non admin theme to test cacheability.
- $this->config('node.settings')->set('use_admin_theme', FALSE)->save();
- \Drupal::service('router.builder')->rebuild();
- $this->drupalGet($node->toUrl('version-history'));
- $this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'MISS');
- $this->drupalGet($node->toUrl('version-history'));
- $this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'HIT');
- }
-
}
diff --git a/core/modules/node/tests/src/Functional/NodeSyndicateBlockTest.php b/core/modules/node/tests/src/Functional/NodeSyndicateBlockTest.php
index c3a3d46b496..f8d52b06ecb 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 2bb252f7c6e..ac1e8664bad 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 cbe9b346623..ac47588d5ec 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/package_manager/package_manager.api.php b/core/modules/package_manager/package_manager.api.php
index 216737e1573..9fa34742ef9 100644
--- a/core/modules/package_manager/package_manager.api.php
+++ b/core/modules/package_manager/package_manager.api.php
@@ -95,6 +95,8 @@
* for event subscribers to flag errors before the active directory is
* modified, because once that has happened, the changes cannot be undone.
* This event may be dispatched multiple times during the stage life cycle.
+ * Note that this event is NOT dispatched when the sandbox manager is
+ * operating in direct-write mode.
*
* - \Drupal\package_manager\Event\PostApplyEvent
* Dispatched after changes in the stage directory have been copied to the
@@ -109,6 +111,11 @@
* life cycle, and should *never* be used for schema changes (i.e., operations
* that should happen in `hook_update_N()` or a post-update function).
*
+ * Since the apply events are not dispatched in direct-write mode, event
+ * subscribers that want to prevent a sandbox from moving through its life cycle
+ * in direct-write mode should do it by subscribing to PreCreateEvent or
+ * StatusCheckEvent.
+ *
* @section sec_stage_api Stage API: Public methods
* The public API of any stage consists of the following methods:
*
diff --git a/core/modules/package_manager/package_manager.services.yml b/core/modules/package_manager/package_manager.services.yml
index 54c8fb846e0..d7bbaf94820 100644
--- a/core/modules/package_manager/package_manager.services.yml
+++ b/core/modules/package_manager/package_manager.services.yml
@@ -47,6 +47,7 @@ services:
Drupal\package_manager\EventSubscriber\ChangeLogger:
calls:
- [setLogger, ['@logger.channel.package_manager_change_log']]
+ Drupal\package_manager\EventSubscriber\DirectWriteSubscriber: {}
Drupal\package_manager\ComposerInspector: {}
# Validators.
@@ -201,3 +202,9 @@ services:
PhpTuf\ComposerStager\Internal\Translation\Service\SymfonyTranslatorProxyInterface:
class: PhpTuf\ComposerStager\Internal\Translation\Service\SymfonyTranslatorProxy
public: false
+
+ Drupal\package_manager\DirectWritePreconditionBypass:
+ decorates: 'PhpTuf\ComposerStager\API\Precondition\Service\ActiveAndStagingDirsAreDifferentInterface'
+ arguments:
+ - '@.inner'
+ public: false
diff --git a/core/modules/package_manager/src/Attribute/AllowDirectWrite.php b/core/modules/package_manager/src/Attribute/AllowDirectWrite.php
new file mode 100644
index 00000000000..d41de1a87e4
--- /dev/null
+++ b/core/modules/package_manager/src/Attribute/AllowDirectWrite.php
@@ -0,0 +1,21 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\package_manager\Attribute;
+
+/**
+ * Identifies sandbox managers which can operate on the running code base.
+ *
+ * Package Manager normally creates and operates on a fully separate, sandboxed
+ * copy of the site. This is pretty safe, but not always necessary for certain
+ * kinds of operations (e.g., adding a new module to the site).
+ * SandboxManagerBase subclasses with this attribute are allowed to skip the
+ * sandboxing and operate directly on the live site, but ONLY if the
+ * `package_manager_allow_direct_write` setting is set to TRUE.
+ *
+ * @see \Drupal\package_manager\SandboxManagerBase::isDirectWrite()
+ */
+#[\Attribute(\Attribute::TARGET_CLASS)]
+final class AllowDirectWrite {
+}
diff --git a/core/modules/package_manager/src/ComposerInspector.php b/core/modules/package_manager/src/ComposerInspector.php
index 69d30738850..32bde1002ea 100644
--- a/core/modules/package_manager/src/ComposerInspector.php
+++ b/core/modules/package_manager/src/ComposerInspector.php
@@ -54,7 +54,7 @@ class ComposerInspector implements LoggerAwareInterface {
*
* @var string
*/
- final public const SUPPORTED_VERSION = '^2.6';
+ final public const SUPPORTED_VERSION = '^2.7';
public function __construct(
private readonly ComposerProcessRunnerInterface $runner,
diff --git a/core/modules/package_manager/src/DirectWritePreconditionBypass.php b/core/modules/package_manager/src/DirectWritePreconditionBypass.php
new file mode 100644
index 00000000000..ba456d270d7
--- /dev/null
+++ b/core/modules/package_manager/src/DirectWritePreconditionBypass.php
@@ -0,0 +1,98 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\package_manager;
+
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use PhpTuf\ComposerStager\API\Path\Value\PathInterface;
+use PhpTuf\ComposerStager\API\Path\Value\PathListInterface;
+use PhpTuf\ComposerStager\API\Precondition\Service\ActiveAndStagingDirsAreDifferentInterface;
+use PhpTuf\ComposerStager\API\Process\Service\ProcessInterface;
+use PhpTuf\ComposerStager\API\Translation\Value\TranslatableInterface;
+
+/**
+ * Allows certain Composer Stager preconditions to be bypassed.
+ *
+ * Only certain preconditions can be bypassed; this class implements all of
+ * those interfaces, and only accepts them in its constructor.
+ *
+ * @internal
+ * This is an internal part of Package Manager and may be changed or removed
+ * at any time without warning. External code should not interact with this
+ * class.
+ */
+final class DirectWritePreconditionBypass implements ActiveAndStagingDirsAreDifferentInterface {
+
+ use StringTranslationTrait;
+
+ /**
+ * Whether or not the decorated precondition is being bypassed.
+ *
+ * @var bool
+ */
+ private static bool $isBypassed = FALSE;
+
+ public function __construct(
+ private readonly ActiveAndStagingDirsAreDifferentInterface $decorated,
+ ) {}
+
+ /**
+ * Bypasses the decorated precondition.
+ */
+ public static function activate(): void {
+ static::$isBypassed = TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName(): TranslatableInterface {
+ return $this->decorated->getName();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription(): TranslatableInterface {
+ return $this->decorated->getDescription();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStatusMessage(PathInterface $activeDir, PathInterface $stagingDir, ?PathListInterface $exclusions = NULL, int $timeout = ProcessInterface::DEFAULT_TIMEOUT): TranslatableInterface {
+ if (static::$isBypassed) {
+ return new TranslatableStringAdapter('This precondition has been skipped because it is not needed in direct-write mode.');
+ }
+ return $this->decorated->getStatusMessage($activeDir, $stagingDir, $exclusions, $timeout);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isFulfilled(PathInterface $activeDir, PathInterface $stagingDir, ?PathListInterface $exclusions = NULL, int $timeout = ProcessInterface::DEFAULT_TIMEOUT): bool {
+ if (static::$isBypassed) {
+ return TRUE;
+ }
+ return $this->decorated->isFulfilled($activeDir, $stagingDir, $exclusions, $timeout);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function assertIsFulfilled(PathInterface $activeDir, PathInterface $stagingDir, ?PathListInterface $exclusions = NULL, int $timeout = ProcessInterface::DEFAULT_TIMEOUT): void {
+ if (static::$isBypassed) {
+ return;
+ }
+ $this->decorated->assertIsFulfilled($activeDir, $stagingDir, $exclusions, $timeout);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLeaves(): array {
+ return [$this];
+ }
+
+}
diff --git a/core/modules/package_manager/src/EventSubscriber/ChangeLogger.php b/core/modules/package_manager/src/EventSubscriber/ChangeLogger.php
index 703dbf4603b..c8c19324c87 100644
--- a/core/modules/package_manager/src/EventSubscriber/ChangeLogger.php
+++ b/core/modules/package_manager/src/EventSubscriber/ChangeLogger.php
@@ -85,15 +85,21 @@ final class ChangeLogger implements EventSubscriberInterface, LoggerAwareInterfa
$event->getDevPackages(),
);
$event->sandboxManager->setMetadata(static::REQUESTED_PACKAGES_KEY, $requested_packages);
+
+ // If we're in direct-write mode, the changes have already been made, so
+ // we should log them right away.
+ if ($event->sandboxManager->isDirectWrite()) {
+ $this->logChanges($event);
+ }
}
/**
* Logs changes made by Package Manager.
*
- * @param \Drupal\package_manager\Event\PostApplyEvent $event
+ * @param \Drupal\package_manager\Event\PostApplyEvent|\Drupal\package_manager\Event\PostRequireEvent $event
* The event being handled.
*/
- public function logChanges(PostApplyEvent $event): void {
+ public function logChanges(PostApplyEvent|PostRequireEvent $event): void {
$installed_at_start = $event->sandboxManager->getMetadata(static::INSTALLED_PACKAGES_KEY);
$installed_post_apply = $this->composerInspector->getInstalledPackagesList($this->pathLocator->getProjectRoot());
diff --git a/core/modules/package_manager/src/EventSubscriber/DirectWriteSubscriber.php b/core/modules/package_manager/src/EventSubscriber/DirectWriteSubscriber.php
new file mode 100644
index 00000000000..7785a9168a3
--- /dev/null
+++ b/core/modules/package_manager/src/EventSubscriber/DirectWriteSubscriber.php
@@ -0,0 +1,92 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\package_manager\EventSubscriber;
+
+use Drupal\Core\State\StateInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\package_manager\Event\PostRequireEvent;
+use Drupal\package_manager\Event\PreRequireEvent;
+use Drupal\package_manager\Event\StatusCheckEvent;
+use Drupal\system\SystemManager;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Handles sandbox events when direct-write is enabled.
+ *
+ * @internal
+ * This is an internal part of Package Manager and may be changed or removed
+ * at any time without warning. External code should not interact with this
+ * class.
+ */
+final class DirectWriteSubscriber implements EventSubscriberInterface {
+
+ use StringTranslationTrait;
+
+ /**
+ * The state key which holds the original status of maintenance mode.
+ *
+ * @var string
+ */
+ private const STATE_KEY = 'package_manager.maintenance_mode';
+
+ public function __construct(private readonly StateInterface $state) {}
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents(): array {
+ return [
+ StatusCheckEvent::class => 'warnAboutDirectWrite',
+ // We want to go into maintenance mode after other subscribers, to give
+ // them a chance to flag errors.
+ PreRequireEvent::class => ['enterMaintenanceMode', -10000],
+ // We want to exit maintenance mode as early as possible.
+ PostRequireEvent::class => ['exitMaintenanceMode', 10000],
+ ];
+ }
+
+ /**
+ * Logs a warning about direct-write mode, if it is in use.
+ *
+ * @param \Drupal\package_manager\Event\StatusCheckEvent $event
+ * The event being handled.
+ */
+ public function warnAboutDirectWrite(StatusCheckEvent $event): void {
+ if ($event->sandboxManager->isDirectWrite()) {
+ $event->addWarning([
+ $this->t('Direct-write mode is enabled, which means that changes will be made without sandboxing them first. This can be risky and is not recommended for production environments. For safety, your site will be put into maintenance mode while dependencies are updated.'),
+ ]);
+ }
+ }
+
+ /**
+ * Enters maintenance mode before a direct-mode require operation.
+ *
+ * @param \Drupal\package_manager\Event\PreRequireEvent $event
+ * The event being handled.
+ */
+ public function enterMaintenanceMode(PreRequireEvent $event): void {
+ $errors = $event->getResults(SystemManager::REQUIREMENT_ERROR);
+
+ if (empty($errors) && $event->sandboxManager->isDirectWrite()) {
+ $this->state->set(static::STATE_KEY, (bool) $this->state->get('system.maintenance_mode'));
+ $this->state->set('system.maintenance_mode', TRUE);
+ }
+ }
+
+ /**
+ * Leaves maintenance mode after a direct-mode require operation.
+ *
+ * @param \Drupal\package_manager\Event\PreRequireEvent $event
+ * The event being handled.
+ */
+ public function exitMaintenanceMode(PostRequireEvent $event): void {
+ if ($event->sandboxManager->isDirectWrite()) {
+ $this->state->set('system.maintenance_mode', $this->state->get(static::STATE_KEY));
+ $this->state->delete(static::STATE_KEY);
+ }
+ }
+
+}
diff --git a/core/modules/package_manager/src/SandboxManagerBase.php b/core/modules/package_manager/src/SandboxManagerBase.php
index 4b3c6065432..15836def8f8 100644
--- a/core/modules/package_manager/src/SandboxManagerBase.php
+++ b/core/modules/package_manager/src/SandboxManagerBase.php
@@ -8,11 +8,13 @@ use Composer\Semver\VersionParser;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Utility\Random;
use Drupal\Core\Queue\QueueFactory;
+use Drupal\Core\Site\Settings;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TempStore\SharedTempStore;
use Drupal\Core\TempStore\SharedTempStoreFactory;
use Drupal\Core\Utility\Error;
+use Drupal\package_manager\Attribute\AllowDirectWrite;
use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
use Drupal\package_manager\Event\PostApplyEvent;
use Drupal\package_manager\Event\PostCreateEvent;
@@ -147,9 +149,9 @@ abstract class SandboxManagerBase implements LoggerAwareInterface {
*
* Consists of a unique random string and the current class name.
*
- * @var string[]
+ * @var string[]|null
*/
- private $lock;
+ private ?array $lock = NULL;
/**
* The shared temp store.
@@ -338,6 +340,7 @@ abstract class SandboxManagerBase implements LoggerAwareInterface {
$id,
static::class,
$this->getType(),
+ $this->isDirectWrite(),
]);
$this->claim($id);
@@ -351,7 +354,12 @@ abstract class SandboxManagerBase implements LoggerAwareInterface {
$this->dispatch($event, [$this, 'markAsAvailable']);
try {
- $this->beginner->begin($active_dir, $stage_dir, $excluded_paths, NULL, $timeout);
+ if ($this->isDirectWrite()) {
+ $this->logger?->info($this->t('Direct-write is enabled. Skipping sandboxing.'));
+ }
+ else {
+ $this->beginner->begin($active_dir, $stage_dir, $excluded_paths, NULL, $timeout);
+ }
}
catch (\Throwable $error) {
$this->destroy();
@@ -372,7 +380,10 @@ abstract class SandboxManagerBase implements LoggerAwareInterface {
}
/**
- * Adds or updates packages in the stage directory.
+ * Adds or updates packages in the sandbox directory.
+ *
+ * If this sandbox manager is running in direct-write mode, the changes will
+ * be made in the active directory.
*
* @param string[] $runtime
* The packages to add as regular top-level dependencies, in the form
@@ -430,8 +441,18 @@ abstract class SandboxManagerBase implements LoggerAwareInterface {
// If constraints were changed, update those packages.
if ($runtime || $dev) {
- $command = array_merge(['update', '--with-all-dependencies', '--optimize-autoloader'], $runtime, $dev);
- $do_stage($command);
+ $do_stage([
+ 'update',
+ // Allow updating top-level dependencies.
+ '--with-all-dependencies',
+ // Always optimize the autoloader for better site performance.
+ '--optimize-autoloader',
+ // For extra safety and speed, make Composer do only the necessary
+ // changes to transitive (indirect) dependencies.
+ '--minimal-changes',
+ ...$runtime,
+ ...$dev,
+ ]);
}
$this->dispatch(new PostRequireEvent($this, $runtime, $dev));
}
@@ -458,6 +479,13 @@ abstract class SandboxManagerBase implements LoggerAwareInterface {
* a failed commit operation.
*/
public function apply(?int $timeout = 600): void {
+ // In direct-write mode, changes are made directly to the running code base,
+ // so there is nothing to do.
+ if ($this->isDirectWrite()) {
+ $this->logger?->info($this->t('Direct-write is enabled. Changes have been made to the running code base.'));
+ return;
+ }
+
$this->checkOwnership();
$active_dir = $this->pathFactory->create($this->pathLocator->getProjectRoot());
@@ -556,7 +584,7 @@ abstract class SandboxManagerBase implements LoggerAwareInterface {
// If the stage directory exists, queue it to be automatically cleaned up
// later by a queue (which may or may not happen during cron).
// @see \Drupal\package_manager\Plugin\QueueWorker\Cleaner
- if ($this->sandboxDirectoryExists()) {
+ if ($this->sandboxDirectoryExists() && !$this->isDirectWrite()) {
$this->queueFactory->get('package_manager_cleanup')
->createItem($this->getSandboxDirectory());
}
@@ -659,8 +687,14 @@ abstract class SandboxManagerBase implements LoggerAwareInterface {
)->render());
}
- if ($stored_lock === [$unique_id, static::class, $this->getType()]) {
+ if (array_slice($stored_lock, 0, 3) === [$unique_id, static::class, $this->getType()]) {
$this->lock = $stored_lock;
+
+ if ($this->isDirectWrite()) {
+ // Bypass a hard-coded set of Composer Stager preconditions that prevent
+ // the active directory from being modified directly.
+ DirectWritePreconditionBypass::activate();
+ }
return $this;
}
@@ -717,7 +751,9 @@ abstract class SandboxManagerBase implements LoggerAwareInterface {
* Returns the path of the directory where changes should be staged.
*
* @return string
- * The absolute path of the directory where changes should be staged.
+ * The absolute path of the directory where changes should be staged. If
+ * this sandbox manager is operating in direct-write mode, this will be
+ * path of the active directory.
*
* @throws \LogicException
* If this method is called before the stage has been created or claimed.
@@ -726,6 +762,10 @@ abstract class SandboxManagerBase implements LoggerAwareInterface {
if (!$this->lock) {
throw new \LogicException(__METHOD__ . '() cannot be called because the stage has not been created or claimed.');
}
+
+ if ($this->isDirectWrite()) {
+ return $this->pathLocator->getProjectRoot();
+ }
return $this->getStagingRoot() . DIRECTORY_SEPARATOR . $this->lock[0];
}
@@ -848,4 +888,26 @@ abstract class SandboxManagerBase implements LoggerAwareInterface {
$this->tempStore->set(self::TEMPSTORE_DESTROYED_STAGES_INFO_PREFIX . $id, $message);
}
+ /**
+ * Indicates whether the active directory will be changed directly.
+ *
+ * This can only happen if direct-write is globally enabled by the
+ * `package_manager_allow_direct_write` setting, AND this class explicitly
+ * allows it (by adding the AllowDirectWrite attribute).
+ *
+ * @return bool
+ * TRUE if the sandbox manager is operating in direct-write mode, otherwise
+ * FALSE.
+ */
+ final public function isDirectWrite(): bool {
+ // The use of direct-write is stored as part of the lock so that it will
+ // remain consistent during the sandbox's entire life cycle, even if the
+ // underlying global settings are changed.
+ if ($this->lock) {
+ return $this->lock[3];
+ }
+ $reflector = new \ReflectionClass($this);
+ return Settings::get('package_manager_allow_direct_write', FALSE) && $reflector->getAttributes(AllowDirectWrite::class);
+ }
+
}
diff --git a/core/modules/package_manager/src/Validator/LockFileValidator.php b/core/modules/package_manager/src/Validator/LockFileValidator.php
index ead8740ba84..c63b283b238 100644
--- a/core/modules/package_manager/src/Validator/LockFileValidator.php
+++ b/core/modules/package_manager/src/Validator/LockFileValidator.php
@@ -111,6 +111,12 @@ final class LockFileValidator implements EventSubscriberInterface {
public function validate(SandboxValidationEvent $event): void {
$sandbox_manager = $event->sandboxManager;
+ // If we're going to change the active directory directly, we don't need to
+ // validate the lock file's consistency, since there is no separate
+ // sandbox directory to compare against.
+ if ($sandbox_manager->isDirectWrite()) {
+ return;
+ }
// Early return if the stage is not already created.
if ($event instanceof StatusCheckEvent && $sandbox_manager->isAvailable()) {
return;
diff --git a/core/modules/package_manager/src/Validator/RsyncValidator.php b/core/modules/package_manager/src/Validator/RsyncValidator.php
index 37fe6eb76a5..eeb3f3a8b56 100644
--- a/core/modules/package_manager/src/Validator/RsyncValidator.php
+++ b/core/modules/package_manager/src/Validator/RsyncValidator.php
@@ -38,6 +38,12 @@ final class RsyncValidator implements EventSubscriberInterface {
* The event being handled.
*/
public function validate(SandboxValidationEvent $event): void {
+ // If the we are going to change the active directory directly, we don't
+ // need rsync.
+ if ($event->sandboxManager->isDirectWrite()) {
+ return;
+ }
+
try {
$this->executableFinder->find('rsync');
$rsync_found = TRUE;
diff --git a/core/modules/package_manager/tests/modules/package_manager_test_api/src/ApiController.php b/core/modules/package_manager/tests/modules/package_manager_test_api/src/ApiController.php
index be088454061..b7920aba169 100644
--- a/core/modules/package_manager/tests/modules/package_manager_test_api/src/ApiController.php
+++ b/core/modules/package_manager/tests/modules/package_manager_test_api/src/ApiController.php
@@ -7,6 +7,7 @@ namespace Drupal\package_manager_test_api;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Queue\QueueFactory;
use Drupal\Core\Url;
+use Drupal\package_manager\Attribute\AllowDirectWrite;
use Drupal\package_manager\FailureMarker;
use Drupal\package_manager\PathLocator;
use Drupal\package_manager\SandboxManagerBase;
@@ -142,6 +143,7 @@ class ApiController extends ControllerBase {
*
* @see \Drupal\package_manager\SandboxManagerBase::claim()
*/
+#[AllowDirectWrite]
final class ControllerSandboxManager extends SandboxManagerBase {
/**
diff --git a/core/modules/package_manager/tests/src/Build/PackageInstallTest.php b/core/modules/package_manager/tests/src/Build/PackageInstallTest.php
index ec53f485dfb..bea2c0d4024 100644
--- a/core/modules/package_manager/tests/src/Build/PackageInstallTest.php
+++ b/core/modules/package_manager/tests/src/Build/PackageInstallTest.php
@@ -15,9 +15,14 @@ class PackageInstallTest extends TemplateProjectTestBase {
/**
* Tests installing packages in a stage directory.
+ *
+ * @testWith [true]
+ * [false]
*/
- public function testPackageInstall(): void {
+ public function testPackageInstall(bool $allow_direct_write): void {
$this->createTestProject('RecommendedProject');
+ $allow_direct_write = var_export($allow_direct_write, TRUE);
+ $this->writeSettings("\n\$settings['package_manager_allow_direct_write'] = $allow_direct_write;");
$this->setReleaseMetadata([
'alpha' => __DIR__ . '/../../fixtures/release-history/alpha.1.1.0.xml',
diff --git a/core/modules/package_manager/tests/src/Build/TemplateProjectTestBase.php b/core/modules/package_manager/tests/src/Build/TemplateProjectTestBase.php
index 18b63b8376b..7e0cdb46e4a 100644
--- a/core/modules/package_manager/tests/src/Build/TemplateProjectTestBase.php
+++ b/core/modules/package_manager/tests/src/Build/TemplateProjectTestBase.php
@@ -347,7 +347,7 @@ END;
$this->assertDirectoryIsWritable($log);
$log .= '/' . str_replace('\\', '_', static::class) . '-' . $this->name();
if ($this->usesDataProvider()) {
- $log .= '-' . preg_replace('/[^a-z0-9]+/i', '_', $this->dataName());
+ $log .= '-' . preg_replace('/[^a-z0-9]+/i', '_', (string) $this->dataName());
}
$code .= <<<END
\$config['package_manager.settings']['log'] = '$log-package_manager.log';
diff --git a/core/modules/package_manager/tests/src/Kernel/ComposerInspectorTest.php b/core/modules/package_manager/tests/src/Kernel/ComposerInspectorTest.php
index 0411978a175..61f922824bd 100644
--- a/core/modules/package_manager/tests/src/Kernel/ComposerInspectorTest.php
+++ b/core/modules/package_manager/tests/src/Kernel/ComposerInspectorTest.php
@@ -230,7 +230,7 @@ class ComposerInspectorTest extends PackageManagerKernelTestBase {
* ["2.5.0", "<default>"]
* ["2.5.5", "<default>"]
* ["2.5.11", "<default>"]
- * ["2.6.0", null]
+ * ["2.7.0", null]
* ["2.2.11", "<default>"]
* ["2.2.0-dev", "<default>"]
* ["2.3.6", "<default>"]
diff --git a/core/modules/package_manager/tests/src/Kernel/DirectWriteTest.php b/core/modules/package_manager/tests/src/Kernel/DirectWriteTest.php
new file mode 100644
index 00000000000..3208fddbbf4
--- /dev/null
+++ b/core/modules/package_manager/tests/src/Kernel/DirectWriteTest.php
@@ -0,0 +1,234 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\package_manager\Kernel;
+
+use ColinODell\PsrTestLogger\TestLogger;
+use Drupal\Core\Queue\QueueFactory;
+use Drupal\Core\State\StateInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\package_manager\Event\PostRequireEvent;
+use Drupal\package_manager\Event\PreApplyEvent;
+use Drupal\package_manager\Event\PreRequireEvent;
+use Drupal\package_manager\Event\SandboxEvent;
+use Drupal\package_manager\Exception\SandboxEventException;
+use Drupal\package_manager\PathLocator;
+use Drupal\package_manager\StatusCheckTrait;
+use Drupal\package_manager\ValidationResult;
+use PhpTuf\ComposerStager\API\Core\BeginnerInterface;
+use PhpTuf\ComposerStager\API\Core\CommitterInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
+
+/**
+ * @covers \Drupal\package_manager\EventSubscriber\DirectWriteSubscriber
+ * @covers \Drupal\package_manager\SandboxManagerBase::isDirectWrite
+ *
+ * @group package_manager
+ */
+class DirectWriteTest extends PackageManagerKernelTestBase implements EventSubscriberInterface {
+
+ use StatusCheckTrait;
+ use StringTranslationTrait;
+
+ /**
+ * Whether we are in maintenance mode before a require operation.
+ *
+ * @var bool|null
+ *
+ * @see ::onPreRequire()
+ */
+ private ?bool $preRequireMaintenanceMode = NULL;
+
+ /**
+ * Whether we are in maintenance mode after a require operation.
+ *
+ * @var bool|null
+ *
+ * @see ::onPostRequire()
+ */
+ private ?bool $postRequireMaintenanceMode = NULL;
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents(): array {
+ return [
+ // The pre-require and post-require listeners need to run after
+ // \Drupal\package_manager\EventSubscriber\DirectWriteSubscriber.
+ PreRequireEvent::class => ['onPreRequire', -10001],
+ PostRequireEvent::class => ['onPostRequire', 9999],
+ PreApplyEvent::class => 'assertNotDirectWrite',
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->container->get(EventDispatcherInterface::class)
+ ->addSubscriber($this);
+ }
+
+ /**
+ * Event listener that asserts the sandbox manager isn't in direct-write mode.
+ *
+ * @param \Drupal\package_manager\Event\SandboxEvent $event
+ * The event being handled.
+ */
+ public function assertNotDirectWrite(SandboxEvent $event): void {
+ $this->assertFalse($event->sandboxManager->isDirectWrite());
+ }
+
+ /**
+ * Event listener that records the maintenance mode flag on pre-require.
+ */
+ public function onPreRequire(): void {
+ $this->preRequireMaintenanceMode = (bool) $this->container->get(StateInterface::class)
+ ->get('system.maintenance_mode');
+ }
+
+ /**
+ * Event listener that records the maintenance mode flag on post-require.
+ */
+ public function onPostRequire(): void {
+ $this->postRequireMaintenanceMode = (bool) $this->container->get(StateInterface::class)
+ ->get('system.maintenance_mode');
+ }
+
+ /**
+ * Tests that direct-write does not work if it is globally disabled.
+ */
+ public function testSiteSandboxedIfDirectWriteGloballyDisabled(): void {
+ // Even if we use a sandbox manager that supports direct write, it should
+ // not be enabled.
+ $sandbox_manager = $this->createStage(TestDirectWriteSandboxManager::class);
+ $logger = new TestLogger();
+ $sandbox_manager->setLogger($logger);
+ $this->assertFalse($sandbox_manager->isDirectWrite());
+ $sandbox_manager->create();
+ $this->assertTrue($sandbox_manager->sandboxDirectoryExists());
+ $this->assertNotSame(
+ $this->container->get(PathLocator::class)->getProjectRoot(),
+ $sandbox_manager->getSandboxDirectory(),
+ );
+ $this->assertFalse($logger->hasRecords('info'));
+ }
+
+ /**
+ * Tests direct-write mode when globally enabled.
+ */
+ public function testSiteNotSandboxedIfDirectWriteGloballyEnabled(): void {
+ $mock_beginner = $this->createMock(BeginnerInterface::class);
+ $mock_beginner->expects($this->never())
+ ->method('begin')
+ ->withAnyParameters();
+ $this->container->set(BeginnerInterface::class, $mock_beginner);
+
+ $mock_committer = $this->createMock(CommitterInterface::class);
+ $mock_committer->expects($this->never())
+ ->method('commit')
+ ->withAnyParameters();
+ $this->container->set(CommitterInterface::class, $mock_committer);
+
+ $this->setSetting('package_manager_allow_direct_write', TRUE);
+
+ $sandbox_manager = $this->createStage(TestDirectWriteSandboxManager::class);
+ $logger = new TestLogger();
+ $sandbox_manager->setLogger($logger);
+ $this->assertTrue($sandbox_manager->isDirectWrite());
+
+ // A status check should flag a warning about running in direct-write mode.
+ $expected_results = [
+ ValidationResult::createWarning([
+ $this->t('Direct-write mode is enabled, which means that changes will be made without sandboxing them first. This can be risky and is not recommended for production environments. For safety, your site will be put into maintenance mode while dependencies are updated.'),
+ ]),
+ ];
+ $actual_results = $this->runStatusCheck($sandbox_manager);
+ $this->assertValidationResultsEqual($expected_results, $actual_results);
+
+ $sandbox_manager->create();
+ // In direct-write mode, the active and sandbox directories are the same.
+ $this->assertTrue($sandbox_manager->sandboxDirectoryExists());
+ $this->assertSame(
+ $this->container->get(PathLocator::class)->getProjectRoot(),
+ $sandbox_manager->getSandboxDirectory(),
+ );
+
+ // Do a require operation so we can assert that we are kicked into, and out
+ // of, maintenance mode.
+ $sandbox_manager->require(['ext-json:*']);
+ $this->assertTrue($this->preRequireMaintenanceMode);
+ $this->assertFalse($this->postRequireMaintenanceMode);
+
+ $sandbox_manager->apply();
+ $sandbox_manager->postApply();
+ // Destroying the sandbox should not populate the clean-up queue.
+ $sandbox_manager->destroy();
+ /** @var \Drupal\Core\Queue\QueueInterface $queue */
+ $queue = $this->container->get(QueueFactory::class)
+ ->get('package_manager_cleanup');
+ $this->assertSame(0, $queue->numberOfItems());
+
+ $records = $logger->recordsByLevel['info'];
+ $this->assertCount(2, $records);
+ $this->assertSame('Direct-write is enabled. Skipping sandboxing.', (string) $records[0]['message']);
+ $this->assertSame('Direct-write is enabled. Changes have been made to the running code base.', (string) $records[1]['message']);
+
+ // A sandbox manager that doesn't support direct-write should not be
+ // influenced by the setting.
+ $this->assertFalse($this->createStage()->isDirectWrite());
+ }
+
+ /**
+ * Tests that pre-require errors prevent maintenance mode during direct-write.
+ */
+ public function testMaintenanceModeNotEnteredIfErrorOnPreRequire(): void {
+ $this->setSetting('package_manager_allow_direct_write', TRUE);
+ // Sanity check: we shouldn't be in maintenance mode to begin with.
+ $state = $this->container->get(StateInterface::class);
+ $this->assertEmpty($state->get('system.maintenance_mode'));
+
+ // Set up an event subscriber which will flag an error.
+ $this->container->get(EventDispatcherInterface::class)
+ ->addListener(PreRequireEvent::class, function (PreRequireEvent $event): void {
+ $event->addError([
+ $this->t('Maintenance mode should not happen.'),
+ ]);
+ });
+
+ $sandbox_manager = $this->createStage(TestDirectWriteSandboxManager::class);
+ $sandbox_manager->create();
+ try {
+ $sandbox_manager->require(['ext-json:*']);
+ $this->fail('Expected an exception to be thrown on pre-require.');
+ }
+ catch (SandboxEventException $e) {
+ $this->assertSame("Maintenance mode should not happen.\n", $e->getMessage());
+ // We should never have entered maintenance mode.
+ $this->assertFalse($this->preRequireMaintenanceMode);
+ // Sanity check: the post-require event should never have been dispatched.
+ $this->assertNull($this->postRequireMaintenanceMode);
+ }
+ }
+
+ /**
+ * Tests that the sandbox's direct-write status is part of its locking info.
+ */
+ public function testDirectWriteFlagIsLocked(): void {
+ $this->setSetting('package_manager_allow_direct_write', TRUE);
+ $sandbox_manager = $this->createStage(TestDirectWriteSandboxManager::class);
+ $this->assertTrue($sandbox_manager->isDirectWrite());
+ $sandbox_manager->create();
+ $this->setSetting('package_manager_allow_direct_write', FALSE);
+ $this->assertTrue($sandbox_manager->isDirectWrite());
+ // Only once the sandbox is destroyed should the sandbox manager reflect the
+ // changed setting.
+ $sandbox_manager->destroy();
+ $this->assertFalse($sandbox_manager->isDirectWrite());
+ }
+
+}
diff --git a/core/modules/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php b/core/modules/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
index 3c2e32b1e7c..5bcc43a8138 100644
--- a/core/modules/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
+++ b/core/modules/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
@@ -12,6 +12,7 @@ use Drupal\Core\Queue\QueueFactory;
use Drupal\Core\Site\Settings;
use Drupal\fixture_manipulator\StageFixtureManipulator;
use Drupal\KernelTests\KernelTestBase;
+use Drupal\package_manager\Attribute\AllowDirectWrite;
use Drupal\package_manager\Event\PreApplyEvent;
use Drupal\package_manager\Event\SandboxValidationEvent;
use Drupal\package_manager\Exception\SandboxEventException;
@@ -173,11 +174,15 @@ abstract class PackageManagerKernelTestBase extends KernelTestBase {
/**
* Creates a stage object for testing purposes.
*
+ * @param class-string $class
+ * (optional) The class of the sandbox manager to create. Defaults to
+ * \Drupal\Tests\package_manager\Kernel\TestSandboxManager.
+ *
* @return \Drupal\Tests\package_manager\Kernel\TestSandboxManager
* A stage object, with test-only modifications.
*/
- protected function createStage(): TestSandboxManager {
- return new TestSandboxManager(
+ protected function createStage(?string $class = TestSandboxManager::class): TestSandboxManager {
+ return new $class(
$this->container->get(PathLocator::class),
$this->container->get(BeginnerInterface::class),
$this->container->get(StagerInterface::class),
@@ -476,6 +481,19 @@ class TestSandboxManager extends SandboxManagerBase {
}
/**
+ * Defines a test-only sandbox manager that allows direct-write.
+ */
+#[AllowDirectWrite]
+class TestDirectWriteSandboxManager extends TestSandboxManager {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected string $type = 'package_manager:test_direct_write';
+
+}
+
+/**
* A test version of the disk space validator to bypass system-level functions.
*/
class TestDiskSpaceValidator extends DiskSpaceValidator {
diff --git a/core/modules/package_manager/tests/src/Kernel/RsyncValidatorTest.php b/core/modules/package_manager/tests/src/Kernel/RsyncValidatorTest.php
index 188c654929d..02be8f298aa 100644
--- a/core/modules/package_manager/tests/src/Kernel/RsyncValidatorTest.php
+++ b/core/modules/package_manager/tests/src/Kernel/RsyncValidatorTest.php
@@ -76,4 +76,13 @@ class RsyncValidatorTest extends PackageManagerKernelTestBase {
$this->assertResults([$result], PreCreateEvent::class);
}
+ /**
+ * Tests that the presence of rsync is not checked in direct-write mode.
+ */
+ public function testRsyncNotNeededForDirectWrite(): void {
+ $this->executableFinder->find('rsync')->shouldNotBeCalled();
+ $this->setSetting('package_manager_allow_direct_write', TRUE);
+ $this->createStage(TestDirectWriteSandboxManager::class)->create();
+ }
+
}
diff --git a/core/modules/page_cache/tests/src/Functional/PageCacheTagsIntegrationTest.php b/core/modules/page_cache/tests/src/Functional/PageCacheTagsIntegrationTest.php
index 41f2e8b8e4f..da6d22bfb05 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
index 5d8c9974469..077d0645ddc 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
Binary files differ
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
index 423f49a1d40..5db0b3a5aae 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
Binary files differ
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 542c4e162e2..78328f9f8e4 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 1cbb9e6b422..db923382a21 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 fc48756de51..7bfc10ef0ef 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/system/tests/modules/twig_loader_test/src/Loader/TestLoader.php b/core/modules/system/tests/modules/twig_loader_test/src/Loader/TestLoader.php
index 272ad65eff3..f5d0c150118 100644
--- a/core/modules/system/tests/modules/twig_loader_test/src/Loader/TestLoader.php
+++ b/core/modules/system/tests/modules/twig_loader_test/src/Loader/TestLoader.php
@@ -24,7 +24,7 @@ class TestLoader implements LoaderInterface {
/**
* {@inheritdoc}
*/
- public function exists(string $name) {
+ public function exists(string $name): bool {
return TRUE;
}
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 8d9465f61a3..7fcb764eac3 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/toolbar/js/escapeAdmin.js b/core/modules/toolbar/js/escapeAdmin.js
index 2d76991e9dc..f7956befe23 100644
--- a/core/modules/toolbar/js/escapeAdmin.js
+++ b/core/modules/toolbar/js/escapeAdmin.js
@@ -14,7 +14,7 @@
// loaded within an existing "workflow".
if (
!pathInfo.currentPathIsAdmin &&
- !/destination=/.test(windowLocation.search)
+ !windowLocation.search.includes('destination=')
) {
sessionStorage.setItem('escapeAdminPath', windowLocation);
}
diff --git a/core/modules/toolbar/tests/src/Nightwatch/Tests/toolbarTest.js b/core/modules/toolbar/tests/src/Nightwatch/Tests/toolbarTest.js
index cbba417abe3..0bed815f330 100644
--- a/core/modules/toolbar/tests/src/Nightwatch/Tests/toolbarTest.js
+++ b/core/modules/toolbar/tests/src/Nightwatch/Tests/toolbarTest.js
@@ -13,27 +13,10 @@ const userOrientationBtn = `${itemUserTray} .toolbar-toggle-orientation button`;
module.exports = {
'@tags': ['core'],
before(browser) {
- browser
- .drupalInstall()
- .drupalInstallModule('toolbar', true)
- .drupalCreateUser({
- name: 'user',
- password: '123',
- permissions: [
- 'access site reports',
- 'access toolbar',
- 'access administration pages',
- 'administer menu',
- 'administer modules',
- 'administer site configuration',
- 'administer account settings',
- 'administer software updates',
- 'access content',
- 'administer permissions',
- 'administer users',
- ],
- })
- .drupalLogin({ name: 'user', password: '123' });
+ browser.drupalInstall({
+ setupFile:
+ 'core/modules/toolbar/tests/src/Nightwatch/ToolbarTestSetup.php',
+ });
},
beforeEach(browser) {
// Set the resolution to the default desktop resolution. Ensure the default
@@ -189,7 +172,7 @@ module.exports = {
browser.drupalRelativeURL('/admin');
// Don't check the visibility as stark doesn't add the .path-admin class
// to the <body> required to display the button.
- browser.assert.attributeContains(escapeSelector, 'href', '/user/2');
+ browser.assert.attributeContains(escapeSelector, 'href', '/user/login');
},
'Aural view test: tray orientation': (browser) => {
browser.waitForElementPresent(
diff --git a/core/modules/toolbar/tests/src/Nightwatch/ToolbarTestSetup.php b/core/modules/toolbar/tests/src/Nightwatch/ToolbarTestSetup.php
new file mode 100644
index 00000000000..47dd0e6e50a
--- /dev/null
+++ b/core/modules/toolbar/tests/src/Nightwatch/ToolbarTestSetup.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\toolbar\Nightwatch;
+
+use Drupal\Core\Extension\ModuleInstallerInterface;
+use Drupal\TestSite\TestSetupInterface;
+use Drupal\user\Entity\Role;
+use Drupal\user\RoleInterface;
+
+/**
+ * Sets up the site for testing the toolbar module.
+ */
+class ToolbarTestSetup implements TestSetupInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setup(): void {
+ $module_installer = \Drupal::service('module_installer');
+ assert($module_installer instanceof ModuleInstallerInterface);
+ $module_installer->install(['toolbar']);
+
+ $role = Role::load(RoleInterface::ANONYMOUS_ID);
+ foreach ([
+ 'access toolbar',
+ 'access administration pages',
+ 'administer modules',
+ 'administer site configuration',
+ 'administer account settings',
+ ] as $permission) {
+ $role->grantPermission($permission);
+ }
+ $role->save();
+ }
+
+}
diff --git a/core/modules/views/js/ajax_view.js b/core/modules/views/js/ajax_view.js
index dd3da9b8350..8e646697d83 100644
--- a/core/modules/views/js/ajax_view.js
+++ b/core/modules/views/js/ajax_view.js
@@ -83,7 +83,7 @@
if (queryString !== '') {
// If there is a '?' in ajaxPath, clean URL are on and & should be
// used to add parameters.
- queryString = (/\?/.test(ajaxPath) ? '&' : '?') + queryString;
+ queryString = (ajaxPath.includes('?') ? '&' : '?') + queryString;
}
}
diff --git a/core/modules/views/src/Entity/View.php b/core/modules/views/src/Entity/View.php
index cd1b2a0a42e..f6bb32cec87 100644
--- a/core/modules/views/src/Entity/View.php
+++ b/core/modules/views/src/Entity/View.php
@@ -481,7 +481,7 @@ class View extends ConfigEntityBase implements ViewEntityInterface {
* {@inheritdoc}
*/
public function onDependencyRemoval(array $dependencies) {
- $changed = FALSE;
+ $changed = parent::onDependencyRemoval($dependencies);
// Don't intervene if the views module is removed.
if (isset($dependencies['module']) && in_array('views', $dependencies['module'])) {
diff --git a/core/modules/views/src/Form/ViewsExposedForm.php b/core/modules/views/src/Form/ViewsExposedForm.php
index d68b1dd5363..9f90160ff55 100644
--- a/core/modules/views/src/Form/ViewsExposedForm.php
+++ b/core/modules/views/src/Form/ViewsExposedForm.php
@@ -196,7 +196,6 @@ class ViewsExposedForm extends FormBase implements WorkspaceSafeFormInterface {
$view->exposed_data = $values;
$view->exposed_raw_input = [];
- $exclude = ['submit', 'form_build_id', 'form_id', 'form_token', 'exposed_form_plugin', 'reset'];
/** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginBase $exposed_form_plugin */
$exposed_form_plugin = $view->display_handler->getPlugin('exposed_form');
$exposed_form_plugin->exposedFormSubmit($form, $form_state, $exclude);
diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_third_party_uninstall.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_third_party_uninstall.yml
new file mode 100644
index 00000000000..eb59548f17f
--- /dev/null
+++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_third_party_uninstall.yml
@@ -0,0 +1,37 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - node
+ - views_third_party_settings_test
+third_party_settings:
+ views_third_party_settings_test:
+ example_setting: true
+id: test_third_party_uninstall
+label: test_third_party_uninstall
+module: views
+description: ''
+tag: ''
+base_table: node_field_data
+base_field: nid
+display:
+ default:
+ display_options:
+ access:
+ type: none
+ cache:
+ type: tag
+ exposed_form:
+ type: basic
+ pager:
+ type: full
+ query:
+ type: views_query
+ style:
+ type: default
+ row:
+ type: fields
+ display_plugin: default
+ display_title: Defaults
+ id: default
+ position: 0
diff --git a/core/modules/views/tests/modules/views_third_party_settings_test/config/schema/views_third_party_settings_test.schema.yml b/core/modules/views/tests/modules/views_third_party_settings_test/config/schema/views_third_party_settings_test.schema.yml
new file mode 100644
index 00000000000..0bdeeed705a
--- /dev/null
+++ b/core/modules/views/tests/modules/views_third_party_settings_test/config/schema/views_third_party_settings_test.schema.yml
@@ -0,0 +1,7 @@
+views.view.*.third_party.views_third_party_settings_test:
+ type: config_entity
+ label: "Example settings"
+ mapping:
+ example_setting:
+ type: boolean
+ label: "Example setting"
diff --git a/core/modules/views/tests/modules/views_third_party_settings_test/views_third_party_settings_test.info.yml b/core/modules/views/tests/modules/views_third_party_settings_test/views_third_party_settings_test.info.yml
new file mode 100644
index 00000000000..be975279565
--- /dev/null
+++ b/core/modules/views/tests/modules/views_third_party_settings_test/views_third_party_settings_test.info.yml
@@ -0,0 +1,8 @@
+name: 'Third Party Settings Test'
+type: module
+description: 'A dummy module that third party settings tests can depend on'
+package: Testing
+version: VERSION
+dependencies:
+ - drupal:node
+ - drupal:views
diff --git a/core/modules/views/tests/src/Functional/GlossaryTest.php b/core/modules/views/tests/src/Functional/GlossaryTest.php
index 292f9176771..25c08d5f159 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 03488125064..e19f1414615 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/tests/src/Kernel/Plugin/ExposedFormRenderTest.php b/core/modules/views/tests/src/Kernel/Plugin/ExposedFormRenderTest.php
index 97d670634b3..14f90fd0c33 100644
--- a/core/modules/views/tests/src/Kernel/Plugin/ExposedFormRenderTest.php
+++ b/core/modules/views/tests/src/Kernel/Plugin/ExposedFormRenderTest.php
@@ -137,12 +137,13 @@ class ExposedFormRenderTest extends ViewsKernelTestBase {
$view->save();
$this->executeView($view);
+ // The "type" filter should be excluded from the raw input because its
+ // value is "All".
$expected = [
- 'type' => 'All',
'type_with_default_value' => 'article',
'multiple_types_with_default_value' => ['article' => 'article'],
];
- $this->assertSame($view->exposed_raw_input, $expected);
+ $this->assertSame($expected, $view->exposed_raw_input);
}
}
diff --git a/core/modules/views/tests/src/Kernel/ThirdPartyUninstallTest.php b/core/modules/views/tests/src/Kernel/ThirdPartyUninstallTest.php
new file mode 100644
index 00000000000..0f3d3eb5291
--- /dev/null
+++ b/core/modules/views/tests/src/Kernel/ThirdPartyUninstallTest.php
@@ -0,0 +1,55 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\views\Kernel;
+
+use Drupal\views\Entity\View;
+
+/**
+ * Tests proper removal of third-party settings from views.
+ *
+ * @group views
+ */
+class ThirdPartyUninstallTest extends ViewsKernelTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $modules = ['node', 'views_third_party_settings_test'];
+
+ /**
+ * Views used by this test.
+ *
+ * @var array
+ */
+ public static $testViews = ['test_third_party_uninstall'];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp($import_test_views = TRUE): void {
+ parent::setUp($import_test_views);
+
+ $this->installEntitySchema('user');
+ $this->installSchema('user', ['users_data']);
+ }
+
+ /**
+ * Tests removing third-party settings when a provider module is uninstalled.
+ */
+ public function testThirdPartyUninstall(): void {
+ $view = View::load('test_third_party_uninstall');
+ $this->assertNotEmpty($view);
+ $this->assertContains('views_third_party_settings_test', $view->getDependencies()['module']);
+ $this->assertTrue($view->getThirdPartySetting('views_third_party_settings_test', 'example_setting'));
+
+ \Drupal::service('module_installer')->uninstall(['views_third_party_settings_test']);
+
+ $view = View::load('test_third_party_uninstall');
+ $this->assertNotEmpty($view);
+ $this->assertNotContains('views_third_party_settings_test', $view->getDependencies()['module']);
+ $this->assertNull($view->getThirdPartySetting('views_third_party_settings_test', 'example_setting'));
+ }
+
+}
diff --git a/core/modules/views/views.theme.inc b/core/modules/views/views.theme.inc
index 10c29c5dbf3..04c5de5a535 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 856c5fb25b2..f3a3196fab6 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 4a88f8a9b69..12e764f0ff5 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 3bd0fe48af7..d33d7c4942a 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/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php b/core/tests/Drupal/KernelTests/Core/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php
index 43ae494680d..15c97bea71f 100644
--- a/core/tests/Drupal/KernelTests/Core/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php
@@ -155,6 +155,8 @@ class ResolvedLibraryDefinitionsFilesMatchTest extends KernelTestBase {
}
sort($this->allModules);
$this->container->get('module_installer')->install($this->allModules);
+ // Get a library discovery from the new container.
+ $this->libraryDiscovery = $this->container->get('library.discovery');
$this->assertLibraries();
}
@@ -174,6 +176,7 @@ class ResolvedLibraryDefinitionsFilesMatchTest extends KernelTestBase {
}
});
$this->container->get('module_installer')->install(array_keys($deprecated_modules_to_test));
+ $this->libraryDiscovery = $this->container->get('library.discovery');
$this->allModules = array_keys(\Drupal::moduleHandler()->getModuleList());
$this->assertLibraries();
diff --git a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php
index c361c7af959..cbda6e3d7f7 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 00000000000..f40a977f6f4
--- /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/tests/Drupal/Tests/Core/Template/StubTwigTemplate.php b/core/tests/Drupal/Tests/Core/Template/StubTwigTemplate.php
deleted file mode 100644
index 6ab42a6c41a..00000000000
--- a/core/tests/Drupal/Tests/Core/Template/StubTwigTemplate.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Drupal\Tests\Core\Template;
-
-use Twig\Source;
-use Twig\Template;
-
-/**
- * A stub of the Twig Template class for testing.
- */
-class StubTwigTemplate extends Template {
-
- /**
- * {@inheritdoc}
- */
- public function getTemplateName(): string {
- return '';
- }
-
- /**
- * {@inheritdoc}
- */
- public function getDebugInfo(): array {
- return [];
- }
-
- /**
- * {@inheritdoc}
- */
- public function getSourceContext(): Source {
- throw new \LogicException(__METHOD__ . '() not implemented.');
- }
-
- /**
- * {@inheritdoc}
- */
- protected function doDisplay(array $context, array $blocks = []): iterable {
- throw new \LogicException(__METHOD__ . '() not implemented.');
- }
-
-}
diff --git a/core/themes/claro/css/components/breadcrumb.pcss.css b/core/themes/claro/css/components/breadcrumb.pcss.css
index 2f9790a5a14..320fc2c67d1 100644
--- a/core/themes/claro/css/components/breadcrumb.pcss.css
+++ b/core/themes/claro/css/components/breadcrumb.pcss.css
@@ -29,7 +29,7 @@
padding: 0 0.75rem;
content: url(../../images/icons/currentColor/arrow-breadcrumb.svg);
- @nest [dir="rtl"] & {
+ [dir="rtl"] & {
transform: scaleX(-1);
}
diff --git a/core/themes/claro/css/components/card.pcss.css b/core/themes/claro/css/components/card.pcss.css
index b7de858f6c7..2f3db1770f9 100644
--- a/core/themes/claro/css/components/card.pcss.css
+++ b/core/themes/claro/css/components/card.pcss.css
@@ -143,7 +143,7 @@
margin-block: 0;
text-align: right; /* LTR */
- @nest [dir="rtl"] & {
+ [dir="rtl"] & {
text-align: left;
}
}
diff --git a/core/themes/claro/css/components/details.css b/core/themes/claro/css/components/details.css
index d5a34caee31..9f7f852c19e 100644
--- a/core/themes/claro/css/components/details.css
+++ b/core/themes/claro/css/components/details.css
@@ -176,6 +176,7 @@ td .claro-details {
border-inline-end: 0.125rem solid;
background: none;
}
+
[dir="rtl"] .claro-details__summary::before {
transform: rotate(-225deg);
}
diff --git a/core/themes/claro/css/components/details.pcss.css b/core/themes/claro/css/components/details.pcss.css
index a83aa729425..274386bded0 100644
--- a/core/themes/claro/css/components/details.pcss.css
+++ b/core/themes/claro/css/components/details.pcss.css
@@ -49,7 +49,7 @@
* element constrains the width. This can happen when toggling the
* "lazy-load" option within an image field.
*/
- @nest td & {
+ td & {
width: min-content;
min-width: 100%;
}
@@ -142,7 +142,7 @@
background-image: url(../../images/icons/545560/chevron-right.svg);
background-size: contain;
- @nest [dir="rtl"] & {
+ [dir="rtl"] & {
transform: rotate(-270deg);
}
}
@@ -165,7 +165,7 @@
border-inline-end: 0.125rem solid;
background: none;
- @nest [dir="rtl"] & {
+ [dir="rtl"] & {
transform: rotate(-225deg);
}
}
@@ -310,7 +310,7 @@
border-width: 0 0 0 var(--details-summary-focus-border-size); /* LTR */
box-shadow: none;
- @nest [dir="rtl"] & {
+ [dir="rtl"] & {
border-width: 0 var(--details-summary-focus-border-size) 0 0;
}
}
diff --git a/core/themes/claro/css/components/form--checkbox-radio.pcss.css b/core/themes/claro/css/components/form--checkbox-radio.pcss.css
index 94bcd446294..27144dbea20 100644
--- a/core/themes/claro/css/components/form--checkbox-radio.pcss.css
+++ b/core/themes/claro/css/components/form--checkbox-radio.pcss.css
@@ -21,7 +21,7 @@
margin-inline-start: calc(var(--input--label-spacing) * -1);
transform: translateY(-50%);
- @nest [dir="rtl"] & {
+ [dir="rtl"] & {
float: right;
}
}
diff --git a/core/themes/claro/css/components/form--managed-file.pcss.css b/core/themes/claro/css/components/form--managed-file.pcss.css
index ecfcf4e9c6c..da1e2a27df9 100644
--- a/core/themes/claro/css/components/form--managed-file.pcss.css
+++ b/core/themes/claro/css/components/form--managed-file.pcss.css
@@ -79,7 +79,7 @@
}
}
- @nest .draggable .form-managed-file.has-value & {
+ .draggable .form-managed-file.has-value & {
/**
* In tables, this should be inline-flex. This is needed to make this element be
* pushed to a new line, to the bottom of the drag handle.
@@ -98,7 +98,7 @@
max-width: 100%;
margin-block-end: var(--space-m);
- @nest .form-managed-file.has-meta & {
+ .form-managed-file.has-meta & {
/* Add some 'end' margin if there are other meta inputs. */
margin-inline-end: var(--space-m);
}
@@ -107,7 +107,7 @@
* If this is rendered inside a file multiple table and there are no alt or
* title, we have to reduce the amount of the bottom margin.
*/
- @nest td .form-managed-file.no-meta & {
+ td .form-managed-file.no-meta & {
margin-block-end: var(--space-xs);
}
}
@@ -164,11 +164,11 @@
}
/* Add some bottom margin for single widgets if no meta is present. */
- @nest .form-managed-file.is-single.has-value &:last-child {
+ .form-managed-file.is-single.has-value &:last-child {
margin-block-end: var(--space-m);
}
- @nest .draggable .form-managed-file.has-value & {
+ .draggable .form-managed-file.has-value & {
/**
* Inside (draggable) tables, this should be flex-displayed. This keeps even
* long file names in the same visual line where the drag handle is.
diff --git a/core/themes/claro/css/components/form--password-confirm.pcss.css b/core/themes/claro/css/components/form--password-confirm.pcss.css
index a013469cbc7..1d7d856e907 100644
--- a/core/themes/claro/css/components/form--password-confirm.pcss.css
+++ b/core/themes/claro/css/components/form--password-confirm.pcss.css
@@ -21,7 +21,7 @@
.password-confirm__confirm {
margin-block-end: 0;
- @nest .js & {
+ .js & {
max-height: 10rem;
transition:
max-height var(--speed-transition) ease-in-out,
@@ -125,7 +125,7 @@
font-size: var(--progress-bar-description-font-size);
line-height: var(--space-m);
- @nest .is-initial.is-password-empty & {
+ .is-initial.is-password-empty & {
margin: 0;
line-height: 0;
}
@@ -163,7 +163,7 @@
color: var(--progress-bar-description-color);
font-size: var(--progress-bar-description-font-size);
- @nest .is-confirm-empty & {
+ .is-confirm-empty & {
visibility: hidden;
}
}
diff --git a/core/themes/claro/css/components/form--select.pcss.css b/core/themes/claro/css/components/form--select.pcss.css
index 6b4c992955c..ce3fd572a13 100644
--- a/core/themes/claro/css/components/form--select.pcss.css
+++ b/core/themes/claro/css/components/form--select.pcss.css
@@ -20,12 +20,13 @@
background-image: url(../../images/icons/8e929c/chevron-down.svg);
}
- @nest [dir="rtl"] & {
+ [dir="rtl"] & {
background-position: 0 50%;
}
- @nest .no-touchevents & {
- &.form-element--extrasmall, &[name$="][_weight]"] {
+ .no-touchevents & {
+ &.form-element--extrasmall,
+ &[name$="][_weight]"] {
padding-inline-end: calc(1.5rem - var(--input-border-size));
background-size: 1.75rem 0.4375rem; /* w: 14px + (2 * 7px), h: 7px */
}
diff --git a/core/themes/claro/css/components/messages.pcss.css b/core/themes/claro/css/components/messages.pcss.css
index 69d75c63528..1bff37f060b 100644
--- a/core/themes/claro/css/components/messages.pcss.css
+++ b/core/themes/claro/css/components/messages.pcss.css
@@ -56,7 +56,7 @@
margin: 0;
}
- @nest [dir="rtl"] & {
+ [dir="rtl"] & {
border-right-width: var(--messages-border-width);
border-left-width: 0;
}
@@ -97,7 +97,7 @@
align-items: center;
margin-block-end: var(--space-m);
- @nest [dir="rtl"] & {
+ [dir="rtl"] & {
background-position: center right;
}
}
diff --git a/core/themes/claro/css/components/page-title.pcss.css b/core/themes/claro/css/components/page-title.pcss.css
index 0de14eb2276..11364373446 100644
--- a/core/themes/claro/css/components/page-title.pcss.css
+++ b/core/themes/claro/css/components/page-title.pcss.css
@@ -17,7 +17,7 @@
font-size: var(--font-size-h1);
-webkit-font-smoothing: antialiased;
- @nest .region-header > & {
+ .region-header > & {
/**
* In this case page title is not rendered as a block ¯\_(ツ)_/¯.
*
diff --git a/core/themes/claro/css/components/shortcut.pcss.css b/core/themes/claro/css/components/shortcut.pcss.css
index 72e157db6fc..87644ecea2f 100644
--- a/core/themes/claro/css/components/shortcut.pcss.css
+++ b/core/themes/claro/css/components/shortcut.pcss.css
@@ -48,19 +48,21 @@
vertical-align: -0.0625rem;
background: transparent url(../../images/shortcut/favstar.svg) left top / calc(var(--shortcut-icon-size) * 4) var(--shortcut-icon-size) no-repeat;
- @nest .shortcut-action--add:hover &, .shortcut-action--add:focus & {
+ .shortcut-action--add:hover &,
+ .shortcut-action--add:focus & {
background-position: calc(-1 * var(--shortcut-icon-size)) top;
}
- @nest .shortcut-action--remove & {
+ .shortcut-action--remove & {
background-position: calc(-2 * var(--shortcut-icon-size)) top;
}
- @nest .shortcut-action--remove:focus &, .shortcut-action--remove:hover & {
+ .shortcut-action--remove:focus &,
+ .shortcut-action--remove:hover & {
background-position: calc(-3 * var(--shortcut-icon-size)) top;
}
- @nest [dir="rtl"] & {
+ [dir="rtl"] & {
background-image: url(../../images/shortcut/favstar-rtl.svg);
}
}
diff --git a/core/themes/claro/css/components/system-admin--admin-list.pcss.css b/core/themes/claro/css/components/system-admin--admin-list.pcss.css
index 92f32d49b94..2db336bfc36 100644
--- a/core/themes/claro/css/components/system-admin--admin-list.pcss.css
+++ b/core/themes/claro/css/components/system-admin--admin-list.pcss.css
@@ -51,7 +51,7 @@
background: transparent no-repeat 50% 50%;
background-image: url(../../images/icons/003ecc/arrow-right.svg);
- @nest [dir="rtl"] & {
+ [dir="rtl"] & {
transform: scaleX(-1);
}
diff --git a/core/themes/claro/css/components/system-status-counter.css b/core/themes/claro/css/components/system-status-counter.css
index a8d21be0cf0..764a9176a6f 100644
--- a/core/themes/claro/css/components/system-status-counter.css
+++ b/core/themes/claro/css/components/system-status-counter.css
@@ -35,6 +35,10 @@
background-position: right center;
background-size: 2.5rem;
}
+[dir="rtl"] .system-status-counter__status-icon::before {
+ border-inline-end: 1px solid #e6e4df;
+ border-inline-start: 0;
+}
@media (forced-colors: active) {
.system-status-counter__status-icon::before {
background-color: canvastext;
@@ -44,9 +48,6 @@
mask-size: 2.5rem;
}
}
-[dir="rtl"] .system-status-counter__status-icon::before {
- background-position: left center;
-}
.system-status-counter__status-icon--error::before {
background-image: var(--system-status-counter-status-icon-error);
}
diff --git a/core/themes/claro/css/components/system-status-counter.pcss.css b/core/themes/claro/css/components/system-status-counter.pcss.css
index a5b139eabe4..f61ca3a374a 100644
--- a/core/themes/claro/css/components/system-status-counter.pcss.css
+++ b/core/themes/claro/css/components/system-status-counter.pcss.css
@@ -30,6 +30,11 @@
background-position: right center;
background-size: 40px;
+ [dir="rtl"] & {
+ border-inline-end: 1px solid #e6e4df;
+ border-inline-start: 0;
+ }
+
@media (forced-colors: active) {
background-color: canvastext;
background-image: none;
@@ -37,10 +42,6 @@
mask-position: right center;
mask-size: 40px;
}
-
- @nest [dir="rtl"] & {
- background-position: left center;
- }
}
}
diff --git a/core/themes/claro/css/components/tabledrag.pcss.css b/core/themes/claro/css/components/tabledrag.pcss.css
index 2484ef2a4c3..4a90d40a10b 100644
--- a/core/themes/claro/css/components/tabledrag.pcss.css
+++ b/core/themes/claro/css/components/tabledrag.pcss.css
@@ -53,16 +53,16 @@ body.drag-y {
.tabledrag-changed {
/* Don't display the abbreviation of 'add-new' table rows. */
- @nest .add-new & {
+ .add-new & {
display: none;
}
- @nest .draggable & {
+ .draggable & {
position: relative;
inset-inline-start: calc(var(--space-xs) * -1);
}
- @nest .tabledrag-cell--only-drag & {
+ .tabledrag-cell--only-drag & {
width: var(--space-l);
min-width: var(--space-l);
}
@@ -189,7 +189,7 @@ body.drag-y {
text-align: end;
/* Hide nested weight toggles as they are redundant. */
- @nest .draggable-table & {
+ .draggable-table & {
display: none;
}
}
@@ -251,7 +251,7 @@ body.drag-y {
background: none !important;
line-height: 0;
- @nest .tabledrag-cell-content & {
+ .tabledrag-cell-content & {
/* Fixes Safari bug (16.1 at least) where table rows are overly large when
using indentation (e.g. re-ordering menu items. */
display: inline-flex;
@@ -261,7 +261,7 @@ body.drag-y {
height: 100%;
}
- @nest [dir="rtl"] & {
+ [dir="rtl"] & {
float: right;
}
}
diff --git a/core/themes/claro/css/components/vertical-tabs.pcss.css b/core/themes/claro/css/components/vertical-tabs.pcss.css
index ef09e440db8..bdac67e2369 100644
--- a/core/themes/claro/css/components/vertical-tabs.pcss.css
+++ b/core/themes/claro/css/components/vertical-tabs.pcss.css
@@ -27,7 +27,7 @@
list-style: none;
color: var(--color-text);
- @nest [dir="rtl"] & {
+ [dir="rtl"] & {
float: right;
}
}
@@ -224,7 +224,7 @@
margin-inline-start: var(--vertical-tabs-menu-width);
border-top-left-radius: 0;
- @nest [dir="rtl"] & {
+ [dir="rtl"] & {
border-top-left-radius: var(--vertical-tabs-border-radius);
}
}
diff --git a/core/themes/claro/css/components/views_ui.admin.pcss.css b/core/themes/claro/css/components/views_ui.admin.pcss.css
index b4f1dd72e0b..38ead2a8e6f 100644
--- a/core/themes/claro/css/components/views_ui.admin.pcss.css
+++ b/core/themes/claro/css/components/views_ui.admin.pcss.css
@@ -50,7 +50,7 @@
margin-inline-start: 0;
}
- @nest [dir="rtl"] & {
+ [dir="rtl"] & {
& > * {
float: right;
}
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 343b8d3256a..00000000000
--- 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: { }