diff --git a/automatic_updates.install b/automatic_updates.install index c17fad0c4cbe06923ca18e4fa2f899cc7118458b..02dbe7d52932641f045eeba2ad48cce337e11c52 100644 --- a/automatic_updates.install +++ b/automatic_updates.install @@ -7,6 +7,13 @@ use Drupal\automatic_updates\Validation\ReadinessRequirements; +/** + * Implements hook_uninstall(). + */ +function automatic_updates_uninstall() { + \Drupal::service('automatic_updates.updater')->destroy(TRUE); +} + /** * Implements hook_requirements(). */ diff --git a/package_manager/package_manager.services.yml b/package_manager/package_manager.services.yml index 271b0428c3d6e914a6ee3e7eaabab269a59baa96..75ae35d2ece1b138391ece253096555ec0775c0a 100644 --- a/package_manager/package_manager.services.yml +++ b/package_manager/package_manager.services.yml @@ -137,3 +137,11 @@ services: - '@package_manager.path_locator' tags: - { name: event_subscriber } + package_manager.uninstall_validator: + class: Drupal\package_manager\PackageManagerUninstallValidator + tags: + - { name: module_install.uninstall_validator } + parent: container.trait + calls: + - ['setContainer', ['@service_container']] + lazy: true diff --git a/package_manager/src/PackageManagerUninstallValidator.php b/package_manager/src/PackageManagerUninstallValidator.php new file mode 100644 index 0000000000000000000000000000000000000000..dd9ab876071ed9f8669e22c0daf1a7c3a437fb44 --- /dev/null +++ b/package_manager/src/PackageManagerUninstallValidator.php @@ -0,0 +1,42 @@ +<?php + +namespace Drupal\package_manager; + +use Drupal\Core\Extension\ModuleUninstallValidatorInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Symfony\Component\DependencyInjection\ContainerAwareInterface; +use Symfony\Component\DependencyInjection\ContainerAwareTrait; + +/** + * Prevents any module from being uninstalled if update is in process. + */ +class PackageManagerUninstallValidator implements ModuleUninstallValidatorInterface, ContainerAwareInterface { + + use ContainerAwareTrait; + use StringTranslationTrait; + + /** + * {@inheritdoc} + */ + public function validate($module) { + $stage = new Stage( + $this->container->get('config.factory'), + $this->container->get('package_manager.path_locator'), + $this->container->get('package_manager.beginner'), + $this->container->get('package_manager.stager'), + $this->container->get('package_manager.committer'), + $this->container->get('file_system'), + $this->container->get('event_dispatcher'), + $this->container->get('tempstore.shared'), + $this->container->get('datetime.time') + ); + if ($stage->isAvailable() || !$stage->isApplying()) { + return []; + } + if ($stage->isApplying()) { + $reasons[] = $this->t('Modules cannot be uninstalled while Package Manager is applying staged changes to the active code base.'); + } + return $reasons; + } + +} diff --git a/package_manager/src/ProxyClass/PackageManagerUninstallValidator.php b/package_manager/src/ProxyClass/PackageManagerUninstallValidator.php new file mode 100644 index 0000000000000000000000000000000000000000..275e878aa585bd6ac7f8a52609f7177c113966bb --- /dev/null +++ b/package_manager/src/ProxyClass/PackageManagerUninstallValidator.php @@ -0,0 +1,88 @@ +<?php +// phpcs:ignoreFile + +/** + * This file was generated via php core/scripts/generate-proxy-class.php 'Drupal\package_manager\PackageManagerUninstallValidator' "modules/contrib/automatic_updates/package_manager/src". + */ + +namespace Drupal\package_manager\ProxyClass { + + /** + * Provides a proxy class for \Drupal\package_manager\PackageManagerUninstallValidator. + * + * @see \Drupal\Component\ProxyBuilder + */ + class PackageManagerUninstallValidator implements \Drupal\Core\Extension\ModuleUninstallValidatorInterface + { + + use \Drupal\Core\DependencyInjection\DependencySerializationTrait; + + /** + * The id of the original proxied service. + * + * @var string + */ + protected $drupalProxyOriginalServiceId; + + /** + * The real proxied service, after it was lazy loaded. + * + * @var \Drupal\package_manager\PackageManagerUninstallValidator + */ + protected $service; + + /** + * The service container. + * + * @var \Symfony\Component\DependencyInjection\ContainerInterface + */ + protected $container; + + /** + * Constructs a ProxyClass Drupal proxy object. + * + * @param \Symfony\Component\DependencyInjection\ContainerInterface $container + * The container. + * @param string $drupal_proxy_original_service_id + * The service ID of the original service. + */ + public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $drupal_proxy_original_service_id) + { + $this->container = $container; + $this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id; + } + + /** + * Lazy loads the real service from the container. + * + * @return object + * Returns the constructed real service. + */ + protected function lazyLoadItself() + { + if (!isset($this->service)) { + $this->service = $this->container->get($this->drupalProxyOriginalServiceId); + } + + return $this->service; + } + + /** + * {@inheritdoc} + */ + public function validate($module) + { + return $this->lazyLoadItself()->validate($module); + } + + /** + * {@inheritdoc} + */ + public function setStringTranslation(\Drupal\Core\StringTranslation\TranslationInterface $translation) + { + return $this->lazyLoadItself()->setStringTranslation($translation); + } + + } + +} diff --git a/package_manager/src/Stage.php b/package_manager/src/Stage.php index 71c4ceac261b57f039870f75e7a8e81b4236b41f..810806fe743ab728d415df8b7a9befd36c4b2ce1 100644 --- a/package_manager/src/Stage.php +++ b/package_manager/src/Stage.php @@ -359,12 +359,7 @@ class Stage { if (!$force) { $this->checkOwnership(); } - - // If we started applying staged changes to the active directory less than - // an hour ago, prevent the stage from being destroyed. - // @see :apply() - $apply_time = $this->tempStore->get(self::TEMPSTORE_APPLY_TIME_KEY); - if (isset($apply_time) && $this->time->getRequestTime() - $apply_time < 3600) { + if ($this->isApplying()) { throw new StageException('Cannot destroy the staging area while it is being applied to the active directory.'); } @@ -547,4 +542,21 @@ class Stage { return FileSystem::getOsTemporaryDirectory() . DIRECTORY_SEPARATOR . '.package_manager' . $site_id; } + /** + * Checks if staged changes are being applied to the active directory. + * + * @return bool + * TRUE if the staged changes are being applied to the active directory, and + * it has been less than an hour since that operation began. If more than an + * hour has elapsed since the changes started to be applied, FALSE is + * returned even if the stage internally thinks that changes are still being + * applied. + * + * @see ::apply() + */ + final public function isApplying(): bool { + $apply_time = $this->tempStore->get(self::TEMPSTORE_APPLY_TIME_KEY); + return isset($apply_time) && $this->time->getRequestTime() - $apply_time < 3600; + } + } diff --git a/package_manager/tests/src/Kernel/StageTest.php b/package_manager/tests/src/Kernel/StageTest.php index 90ac26e4d9f26251fbce4b5ae45a4f3642049370..bf68cc8f39bcd6829659fc50d6021418394e3e59 100644 --- a/package_manager/tests/src/Kernel/StageTest.php +++ b/package_manager/tests/src/Kernel/StageTest.php @@ -4,6 +4,7 @@ namespace Drupal\Tests\package_manager\Kernel; use Drupal\Component\Datetime\Time; use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\Extension\ModuleUninstallValidatorException; use Drupal\package_manager\Event\PostApplyEvent; use Drupal\package_manager\Event\PreApplyEvent; use Drupal\package_manager\Event\StageEvent; @@ -12,6 +13,8 @@ use Drupal\package_manager\Exception\StageException; /** * @coversDefaultClass \Drupal\package_manager\Stage * + * @covers \Drupal\package_manager\PackageManagerUninstallValidator + * * @group package_manager */ class StageTest extends PackageManagerKernelTestBase { @@ -179,6 +182,32 @@ class StageTest extends PackageManagerKernelTestBase { $stage->apply(); } + /** + * Test uninstalling any module while the staged changes are being applied. + */ + public function testUninstallModuleDuringApply(): void { + $listener = function (PreApplyEvent $event): void { + $this->assertTrue($event->getStage()->isApplying()); + + // Trying to uninstall any module while the stage is being applied should + // result in a module uninstall validation error. + try { + $this->container->get('module_installer') + ->uninstall(['package_manager_bypass']); + $this->fail('Expected an exception to be thrown while uninstalling a module.'); + } + catch (ModuleUninstallValidatorException $e) { + $this->assertStringContainsString('Modules cannot be uninstalled while Package Manager is applying staged changes to the active code base.', $e->getMessage()); + } + }; + $this->container->get('event_dispatcher') + ->addListener(PreApplyEvent::class, $listener); + + $stage = $this->createStage(); + $stage->create(); + $stage->apply(); + } + } /**