From 32f733eb2715b809adbf7baed4c682df8e9f398b Mon Sep 17 00:00:00 2001 From: Yash Rode <57207-yash.rode@users.noreply.drupalcode.org> Date: Fri, 21 Apr 2023 11:25:40 +0000 Subject: [PATCH] Issue #3353219 by yash.rode, phenaproxima, Wim Leers, omkar.podey: Create a PreApply validator that prevents Drupal projects from being removed if they are enabled --- package_manager/package_manager.services.yml | 4 + .../src/PackageManagerServiceProvider.php | 1 + .../Validator/EnabledExtensionsValidator.php | 88 ++++++++++ .../Kernel/EnabledExtensionsValidatorTest.php | 165 ++++++++++++++++++ 4 files changed, 258 insertions(+) create mode 100644 package_manager/src/Validator/EnabledExtensionsValidator.php create mode 100644 package_manager/tests/src/Kernel/EnabledExtensionsValidatorTest.php diff --git a/package_manager/package_manager.services.yml b/package_manager/package_manager.services.yml index 791172e209..28b08b69ba 100644 --- a/package_manager/package_manager.services.yml +++ b/package_manager/package_manager.services.yml @@ -112,6 +112,10 @@ services: class: Drupal\package_manager\Validator\DuplicateInfoFileValidator tags: - { name: event_subscriber } + package_manager.validator.enabled_extensions: + class: Drupal\package_manager\Validator\EnabledExtensionsValidator + tags: + - { name: event_subscriber } package_manager.validator.overwrite_existing_packages: class: Drupal\package_manager\Validator\OverwriteExistingPackagesValidator tags: diff --git a/package_manager/src/PackageManagerServiceProvider.php b/package_manager/src/PackageManagerServiceProvider.php index d6b5b8f055..45c0783bb5 100644 --- a/package_manager/src/PackageManagerServiceProvider.php +++ b/package_manager/src/PackageManagerServiceProvider.php @@ -107,6 +107,7 @@ final class PackageManagerServiceProvider extends ServiceProviderBase { 'tempstore.shared' => 'Drupal\Core\TempStore\SharedTempStoreFactory', 'class_resolver' => 'Drupal\Core\DependencyInjection\ClassResolverInterface', 'request_stack' => 'Symfony\Component\HttpFoundation\RequestStack', + 'theme_handler' => 'Drupal\Core\Extension\ThemeHandlerInterface', ]; foreach ($aliases as $service_id => $alias) { if (!$container->hasAlias($alias)) { diff --git a/package_manager/src/Validator/EnabledExtensionsValidator.php b/package_manager/src/Validator/EnabledExtensionsValidator.php new file mode 100644 index 0000000000..daeca84b1a --- /dev/null +++ b/package_manager/src/Validator/EnabledExtensionsValidator.php @@ -0,0 +1,88 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\package_manager\Validator; + +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Extension\ThemeHandlerInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\package_manager\ComposerInspector; +use Drupal\package_manager\Event\PreApplyEvent; +use Drupal\package_manager\PathLocator; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Validates no enabled Drupal extensions are removed from the stage directory. + * + * @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 EnabledExtensionsValidator implements EventSubscriberInterface { + + use StringTranslationTrait; + + /** + * Constructs an EnabledExtensionsValidator object. + * + * @param \Drupal\package_manager\PathLocator $pathLocator + * The path locator service. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler + * The module handler service. + * @param \Drupal\package_manager\ComposerInspector $composerInspector + * The Composer inspector service. + * @param \Drupal\Core\Extension\ThemeHandlerInterface $themeHandler + * The theme handler service. + */ + public function __construct( + private PathLocator $pathLocator, + private ModuleHandlerInterface $moduleHandler, + private ComposerInspector $composerInspector, + private ThemeHandlerInterface $themeHandler + ) {} + + /** + * Validates that no enabled Drupal extensions have been removed. + * + * @param \Drupal\package_manager\Event\PreApplyEvent $event + * The event object. + */ + public function validate(PreApplyEvent $event): void { + $active_packages_list = $this->composerInspector->getInstalledPackagesList($this->pathLocator->getProjectRoot()); + $stage_packages_list = $this->composerInspector->getInstalledPackagesList($event->stage->getStageDirectory()); + + $extensions_list = $this->moduleHandler->getModuleList() + $this->themeHandler->listInfo(); + foreach ($extensions_list as $extension) { + $extension_name = $extension->getName(); + $package = $active_packages_list->getPackageByDrupalProjectName($extension_name); + if ($package && $stage_packages_list->getPackageByDrupalProjectName($extension_name) === NULL) { + $removed_project_messages[] = t("'@name' @type (provided by <code>@package</code>)", [ + '@name' => $extension_name, + '@type' => $extension->getType(), + '@package' => $package->name, + ]); + } + } + + if (!empty($removed_project_messages)) { + $removed_packages_summary = $this->formatPlural( + count($removed_project_messages), + 'The update cannot proceed because the following enabled Drupal extension was removed during the update.', + 'The update cannot proceed because the following enabled Drupal extensions were removed during the update.' + ); + $event->addError($removed_project_messages, $removed_packages_summary); + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents(): array { + return [ + PreApplyEvent::class => 'validate', + ]; + } + +} diff --git a/package_manager/tests/src/Kernel/EnabledExtensionsValidatorTest.php b/package_manager/tests/src/Kernel/EnabledExtensionsValidatorTest.php new file mode 100644 index 0000000000..617ee41191 --- /dev/null +++ b/package_manager/tests/src/Kernel/EnabledExtensionsValidatorTest.php @@ -0,0 +1,165 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\Tests\package_manager\Kernel; + +use Drupal\Core\Extension\Extension; +use Drupal\fixture_manipulator\ActiveFixtureManipulator; +use Drupal\package_manager\Event\PreApplyEvent; +use Drupal\package_manager\PathLocator; +use Drupal\package_manager\ValidationResult; +use Drupal\Tests\package_manager\Traits\ComposerInstallersTrait; + +/** + * @covers \Drupal\package_manager\Validator\EnabledExtensionsValidator + * @group package_manager + * @internal + */ +class EnabledExtensionsValidatorTest extends PackageManagerKernelTestBase { + + use ComposerInstallersTrait; + + /** + * Data provider for testExtensionRemoved(). + * + * @return mixed[][] + * The test cases. + */ + public function providerExtensionRemoved(): array { + $summary = t('The update cannot proceed because the following enabled Drupal extension was removed during the update.'); + return [ + 'module' => [ + [ + [ + 'name' => 'drupal/test_module2', + 'version' => '1.3.1', + 'type' => 'drupal-module', + ], + ], + [ + ValidationResult::createError([t("'test_module2' module (provided by <code>drupal/test_module2</code>)")], $summary), + ], + ], + 'module and theme' => [ + [ + [ + 'name' => 'drupal/test_module1', + 'version' => '1.3.1', + 'type' => 'drupal-module', + ], + [ + 'name' => 'drupal/test_theme', + 'version' => '1.3.1', + 'type' => 'drupal-theme', + ], + ], + [ + ValidationResult::createError([ + t("'test_module1' module (provided by <code>drupal/test_module1</code>)"), + t("'test_theme' theme (provided by <code>drupal/test_theme</code>)"), + ], t('The update cannot proceed because the following enabled Drupal extensions were removed during the update.')), + ], + ], + 'profile' => [ + [ + [ + 'name' => 'drupal/test_profile', + 'version' => '1.3.1', + 'type' => 'drupal-profile', + ], + ], + [ + ValidationResult::createError([t("'test_profile' profile (provided by <code>drupal/test_profile</code>)")], $summary), + ], + ], + 'theme' => [ + [ + [ + 'name' => 'drupal/test_theme', + 'version' => '1.3.1', + 'type' => 'drupal-theme', + ], + ], + [ + ValidationResult::createError([t("'test_theme' theme (provided by <code>drupal/test_theme</code>)")], $summary), + ], + ], + ]; + } + + /** + * Tests that error is raised if Drupal modules, profiles or themes are removed. + * + * @param array $packages + * Packages that will be added to the active directory, and removed from the + * stage directory. + * @param \Drupal\package_manager\ValidationResult[] $expected_results + * The expected validation results. + * + * @dataProvider providerExtensionRemoved + */ + public function testExtensionRemoved(array $packages, array $expected_results): void { + $project_root = $this->container->get(PathLocator::class)->getProjectRoot(); + $this->installComposerInstallers($project_root); + // @todo Remove this in https://www.drupal.org/project/automatic_updates/issues/3355553. + $this->setInstallerPaths([], $project_root); + + $active_manipulator = new ActiveFixtureManipulator(); + $stage_manipulator = $this->getStageFixtureManipulator(); + foreach ($packages as $package) { + $active_manipulator->addPackage($package, FALSE, TRUE); + $stage_manipulator->removePackage($package['name']); + } + $active_manipulator->commitChanges(); + + foreach ($packages as $package) { + $extension_name = str_replace('drupal/', '', $package['name']); + $extension = self::createExtension($project_root, $package['type'], $extension_name); + + if ($extension->getType() === 'theme') { + /** @var \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler */ + $theme_handler = $this->container->get('theme_handler'); + $theme_handler->addTheme($extension); + $this->assertArrayHasKey($extension_name, $theme_handler->listInfo()); + } + else { + /** @var \Drupal\Core\Extension\ModuleHandlerInterface $module_handler */ + $module_handler = $this->container->get('module_handler'); + $module_list = $module_handler->getModuleList(); + $module_list[$extension_name] = $extension; + $module_handler->setModuleList($module_list); + $this->assertArrayHasKey($extension_name, $module_handler->getModuleList()); + } + } + $this->assertResults($expected_results, PreApplyEvent::class); + } + + /** + * Returns a mocked extension object for a package. + * + * @param string $project_root + * The project root directory. + * @param string $package_type + * The package type (e.g., `drupal-module` or `drupal-theme`). + * @param string $extension_name + * The name of the extension. + * + * @return \Drupal\Core\Extension\Extension + * An extension object. + */ + private static function createExtension(string $project_root, string $package_type, string $extension_name): Extension { + $type = match ($package_type) { + 'drupal-theme' => 'theme', + 'drupal-profile' => 'profile', + default => 'module', + }; + $subdirectory = match ($type) { + 'theme' => 'themes', + 'profile' => 'profiles', + 'module' => 'modules', + }; + return new Extension($project_root, $type, "$subdirectory/contrib/$extension_name/$extension_name.info.yml"); + } + +} -- GitLab