diff options
34 files changed, 726 insertions, 117 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/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/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/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/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/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/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/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/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/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/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/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/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/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.'); - } - -} |