Skip to content
Snippets Groups Projects
Commit 32f733eb authored by Yash Rode's avatar Yash Rode Committed by Adam G-H
Browse files

Issue #3353219 by yash.rode, phenaproxima, Wim Leers, omkar.podey: Create a...

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
parent fe8b4ac6
No related branches found
No related tags found
No related merge requests found
......@@ -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:
......
......@@ -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)) {
......
<?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',
];
}
}
<?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");
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment