Skip to content
Snippets Groups Projects
Commit 602bc418 authored by Theresa Grannum's avatar Theresa Grannum Committed by Ted Bowman
Browse files

Issue #3275369 by Theresa.Grannum, tedbow, kunal.sachdev, phenaproxima: AU...

Issue #3275369 by Theresa.Grannum, tedbow, kunal.sachdev, phenaproxima: AU Extensions: Validate updated dependencies  are secure and supported
parent b4cc1d3e
No related branches found
No related tags found
No related merge requests found
Showing
with 326 additions and 38 deletions
...@@ -6,6 +6,7 @@ use Drupal\automatic_updates\ProjectInfo; ...@@ -6,6 +6,7 @@ use Drupal\automatic_updates\ProjectInfo;
use Drupal\automatic_updates_extensions\ExtensionUpdater; use Drupal\automatic_updates_extensions\ExtensionUpdater;
use Drupal\automatic_updates\LegacyVersionUtility; use Drupal\automatic_updates\LegacyVersionUtility;
use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\package_manager\Event\PreApplyEvent;
use Drupal\package_manager\Event\PreCreateEvent; use Drupal\package_manager\Event\PreCreateEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
...@@ -20,6 +21,83 @@ final class UpdateReleaseValidator implements EventSubscriberInterface { ...@@ -20,6 +21,83 @@ final class UpdateReleaseValidator implements EventSubscriberInterface {
use StringTranslationTrait; use StringTranslationTrait;
/**
* Checks if the given version of a project is supported.
*
* Checks if the given version of the given project is in the core update
* system's list of known, secure, installable releases of that project.
* considered a supported release by verifying if the project is found in the
* core update system's list of known, secure, and installable releases.
*
* @param string $name
* The name of the project.
* @param string $semantic_version
* A semantic version number for the project.
*
* @return bool
* TRUE if the given version of the project is supported, otherwise FALSE.
* given version is not supported will return FALSE.
*/
protected function isSupportedRelease(string $name, string $semantic_version): bool {
$supported_releases = (new ProjectInfo($name))->getInstallableReleases();
if (!$supported_releases) {
return FALSE;
}
// If this version is found in the list of installable releases, it is
// secured and supported.
if (array_key_exists($semantic_version, $supported_releases)) {
return TRUE;
}
// If the semantic version number wasn't in the list of
// installable releases, convert it to a legacy version number and see
// if the version number is in the list.
$legacy_version = LegacyVersionUtility::convertToLegacyVersion($semantic_version);
if ($legacy_version && array_key_exists($legacy_version, $supported_releases)) {
return TRUE;
}
// Neither the semantic version nor the legacy version are in the list
// of installable releases, so the release isn't supported.
return FALSE;
}
/**
* Checks that the packages are secure and supported.
*
* @param \Drupal\package_manager\Event\PreApplyEvent $event
* The event object.
*/
public function checkStagedReleases(PreApplyEvent $event): void {
$messages = [];
// Get packages that were installed and also updated in the staging area.
$active = $event->getStage()->getActiveComposer();
$staged = $event->getStage()->getStageComposer();
$updated_packages = $staged->getPackagesWithDifferentVersionsIn($active);
foreach ($updated_packages as $staged_package) {
if (!in_array($staged_package->getType(),
['drupal-module', 'drupal-theme'], TRUE)) {
continue;
}
[, $project_name] = explode('/', $staged_package->getName());
$semantic_version = $staged_package->getPrettyVersion();
if (!$this->isSupportedRelease($project_name, $semantic_version)) {
$messages[] = $this->t('Project @project_name to version @version', [
'@project_name' => $project_name,
'@version' => $semantic_version,
]);
}
}
if ($messages) {
$summary = $this->formatPlural(
count($messages),
'Cannot update because the following project version is not in the list of installable releases.',
'Cannot update because the following project versions are not in the list of installable releases.'
);
$event->addError($messages, $summary);
}
}
/** /**
* Checks that the update projects are secure and supported. * Checks that the update projects are secure and supported.
* *
...@@ -41,33 +119,12 @@ final class UpdateReleaseValidator implements EventSubscriberInterface { ...@@ -41,33 +119,12 @@ final class UpdateReleaseValidator implements EventSubscriberInterface {
$project_name = $package_parts[1]; $project_name = $package_parts[1];
// If the version isn't in the list of installable releases, then it // If the version isn't in the list of installable releases, then it
// isn't secure and supported and the user should receive an error. // isn't secure and supported and the user should receive an error.
$releases = (new ProjectInfo($project_name))->getInstallableReleases(); if (!$this->isSupportedRelease($project_name, $sematic_version)) {
$is_missing_version = FALSE;
if (empty($releases)) {
$is_missing_version = TRUE;
}
elseif (!array_key_exists($sematic_version, $releases)) {
$legacy_version = LegacyVersionUtility::convertToLegacyVersion($sematic_version);
if ($legacy_version) {
if (!array_key_exists($legacy_version, $releases)) {
// If we cannot find the version using semantic or legacy then the
// version is missing.
$is_missing_version = TRUE;
}
}
else {
// If we cannot convert the semantic version into a legacy version
// then the version is missing.
$is_missing_version = TRUE;
}
}
if ($is_missing_version) {
$messages[] = $this->t('Project @project_name to version @version', [ $messages[] = $this->t('Project @project_name to version @version', [
'@project_name' => $project_name, '@project_name' => $project_name,
'@version' => $sematic_version, '@version' => $sematic_version,
]); ]);
} }
} }
} }
if ($messages) { if ($messages) {
...@@ -86,6 +143,7 @@ final class UpdateReleaseValidator implements EventSubscriberInterface { ...@@ -86,6 +143,7 @@ final class UpdateReleaseValidator implements EventSubscriberInterface {
public static function getSubscribedEvents() { public static function getSubscribedEvents() {
return [ return [
PreCreateEvent::class => 'checkRelease', PreCreateEvent::class => 'checkRelease',
PreApplyEvent::class => 'checkStagedReleases',
]; ];
} }
......
{
"packages": [
{
"name": "drupal/core-recommended",
"version": "9.8.0",
"require": {
"drupal/core": "9.8.0"
}
},
{
"name": "drupal/core",
"version": "9.8.0"
},
{
"name": "drupal/dependency",
"version": "9.8.0",
"type": "drupal-library"
},
{
"name": "drupal/aaa_automatic_updates_test",
"version": "7.0.0",
"type": "drupal-module"
},
{
"name": "drupal/aaa_update_test",
"version": "2.0.0",
"type": "drupal-module"
},
{
"name": "drupal/semver_test",
"version": "8.1.0",
"type": "drupal-module"
}
]
}
{
"packages": [
{
"name": "drupal/core-recommended",
"version": "9.8.0",
"require": {
"drupal/core": "9.8.0"
}
},
{
"name": "drupal/core",
"version": "9.8.0"
},
{
"name": "drupal/dependency",
"version": "9.8.1",
"type": "drupal-library"
},
{
"name": "drupal/aaa_update_test",
"version": "2.1.0",
"type": "drupal-module"
}
]
}
{
"packages": [
{
"name": "drupal/core-recommended",
"version": "9.8.0",
"require": {
"drupal/core": "9.8.0"
}
},
{
"name": "drupal/core",
"version": "9.8.0"
},
{
"name": "drupal/dependency",
"version": "9.8.1",
"type": "drupal-library"
},
{
"name": "drupal/aaa_update_test",
"version": "3.0.0",
"type": "drupal-module"
}
]
}
{
"packages": [
{
"name": "drupal/core-recommended",
"version": "9.8.0",
"require": {
"drupal/core": "9.8.0"
}
},
{
"name": "drupal/core",
"version": "9.8.0"
},
{
"name": "drupal/dependency",
"version": "9.8.1",
"type": "drupal-library"
},
{
"name": "drupal/semver_test",
"version": "8.1.1",
"type": "drupal-module"
}
]
}
{
"packages": [
{
"name": "drupal/core-recommended",
"version": "9.8.0",
"require": {
"drupal/core": "9.8.0"
}
},
{
"name": "drupal/core",
"version": "9.8.0"
},
{
"name": "drupal/dependency",
"version": "9.8.1",
"type": "drupal-library"
},
{
"name": "drupal/semver_test",
"version": "8.2.0",
"type": "drupal-module"
}
]
}
...@@ -47,8 +47,8 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase { ...@@ -47,8 +47,8 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
*/ */
public function providerSuccessfulUpdate() { public function providerSuccessfulUpdate() {
return [ return [
'maintiance_mode_on, semver' => [TRUE, 'semver_test', '8.1.0', '8.1.1'], 'maintenance mode on, semver' => [TRUE, 'semver_test', '8.1.0', '8.1.1'],
'maintiance_mode_off, legacy' => [FALSE, 'aaa_update_test', '8.x-2.0', '8.x-2.1'], 'maintenance mode off, legacy' => [FALSE, 'aaa_update_test', '8.x-2.0', '8.x-2.1'],
]; ];
} }
...@@ -140,10 +140,13 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase { ...@@ -140,10 +140,13 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
* @dataProvider providerSuccessfulUpdate * @dataProvider providerSuccessfulUpdate
*/ */
public function testSuccessfulUpdate(bool $maintenance_mode_on, string $project_name, string $installed_version, string $target_version): void { public function testSuccessfulUpdate(bool $maintenance_mode_on, string $project_name, string $installed_version, string $target_version): void {
// Disable the scaffold file permissions validator because it will try to // Disable the scaffold file permissions and target release validators
// read composer.json from the staging area, which won't exist because // because they will try to read composer.json from the staging area,
// Package Manager is bypassed. // which won't exist because Package Manager is bypassed.
$this->disableValidators(['automatic_updates.validator.scaffold_file_permissions']); $this->disableValidators([
'automatic_updates.validator.scaffold_file_permissions',
'automatic_updates_extensions.validator.target_release',
]);
$this->container->get('theme_installer')->install(['automatic_updates_theme_with_updates']); $this->container->get('theme_installer')->install(['automatic_updates_theme_with_updates']);
$this->updateProject = $project_name; $this->updateProject = $project_name;
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
namespace Drupal\Tests\automatic_updates_extensions\Kernel\Validator; namespace Drupal\Tests\automatic_updates_extensions\Kernel\Validator;
use Drupal\automatic_updates\LegacyVersionUtility; use Drupal\automatic_updates\LegacyVersionUtility;
use Drupal\package_manager\Event\PreApplyEvent;
use Drupal\package_manager\Event\PreCreateEvent; use Drupal\package_manager\Event\PreCreateEvent;
use Drupal\package_manager\ValidationResult; use Drupal\package_manager\ValidationResult;
use Drupal\Tests\automatic_updates_extensions\Kernel\AutomaticUpdatesExtensionsKernelTestBase; use Drupal\Tests\automatic_updates_extensions\Kernel\AutomaticUpdatesExtensionsKernelTestBase;
...@@ -23,7 +24,22 @@ class UpdateReleaseValidatorTest extends AutomaticUpdatesExtensionsKernelTestBas ...@@ -23,7 +24,22 @@ class UpdateReleaseValidatorTest extends AutomaticUpdatesExtensionsKernelTestBas
} }
/** /**
* Tests updating to a release. * Data provider for testPreCreateException().
*
* @return array[]
* The test cases.
*/
public function providerTestPreCreateException(): array {
return [
'semver, supported update' => ['semver_test', '8.1.0', '8.1.1', FALSE],
'semver, update to unsupported branch' => ['semver_test', '8.1.0', '8.2.0', TRUE],
'legacy, supported update' => ['aaa_update_test', '8.x-2.0', '8.x-2.1', FALSE],
'legacy, update to unsupported branch' => ['aaa_update_test', '8.x-2.0', '8.x-3.0', TRUE],
];
}
/**
* Tests updating to a release during pre-create.
* *
* @param string $project * @param string $project
* The project to update. * The project to update.
...@@ -34,18 +50,21 @@ class UpdateReleaseValidatorTest extends AutomaticUpdatesExtensionsKernelTestBas ...@@ -34,18 +50,21 @@ class UpdateReleaseValidatorTest extends AutomaticUpdatesExtensionsKernelTestBas
* @param bool $error_expected * @param bool $error_expected
* Whether an error is expected in the update. * Whether an error is expected in the update.
* *
* @dataProvider providerTestRelease * @dataProvider providerTestPreCreateException
*/ */
public function testRelease(string $project, string $installed_version, string $target_version, bool $error_expected) { public function testPreCreateException(string $project, string $installed_version, string $target_version, bool $error_expected): void {
$this->enableModules([$project]); $this->enableModules([$project]);
$module_info = ['version' => $installed_version, 'project' => $project]; $module_info = ['version' => $installed_version, 'project' => $project];
$this->config('update_test.settings') $this->config('update_test.settings')
->set("system_info.$project", $module_info) ->set("system_info.$project", $module_info)
->save(); ->save();
$this->setReleaseMetadataForProjects([ $this->setReleaseMetadataForProjects([
$project => __DIR__ . "/../../../fixtures/release-history/$project.1.1.xml", $project => __DIR__ . "/../../../fixtures/release-history/$project.1.1.xml",
'drupal' => __DIR__ . '/../../../../../tests/fixtures/release-history/drupal.9.8.2.xml', 'drupal' => __DIR__ . '/../../../../../tests/fixtures/release-history/drupal.9.8.2.xml',
]); ]);
if ($error_expected) { if ($error_expected) {
$expected_results = [ $expected_results = [
ValidationResult::createError( ValidationResult::createError(
...@@ -62,18 +81,90 @@ class UpdateReleaseValidatorTest extends AutomaticUpdatesExtensionsKernelTestBas ...@@ -62,18 +81,90 @@ class UpdateReleaseValidatorTest extends AutomaticUpdatesExtensionsKernelTestBas
} }
/** /**
* Data provider for testRelease(). * Data provider for testPreApplyException().
* *
* @return array[] * @return array[]
* The test cases. * The test cases.
*/ */
public function providerTestRelease() { public function providerTestPreApplyException(): array {
$fixtures_folder = __DIR__ . '/../../../fixtures/update_release_validator';
return [ return [
'semver, supported update' => ['semver_test', '8.1.0', '8.1.1', FALSE], 'semver, supported update' => ['semver_test', '8.1.0', '8.1.1', "$fixtures_folder/semver_supported_update.staged.installed.json", FALSE],
'semver, update to unsupported branch' => ['semver_test', '8.1.0', '8.2.0', TRUE], 'semver, update to unsupported branch' => ['semver_test', '8.1.0', '8.2.0', "$fixtures_folder/semver_unsupported_update.staged.installed.json", TRUE],
'legacy, supported update' => ['aaa_update_test', '8.x-2.0', '8.x-2.1', FALSE], 'legacy, supported update' => ['aaa_update_test', '8.x-2.0', '8.x-2.1', "$fixtures_folder/legacy_supported_update.staged.installed.json", FALSE],
'legacy, update to unsupported branch' => ['aaa_update_test', '8.x-2.0', '8.x-3.0', TRUE], 'legacy, update to unsupported branch' => ['aaa_update_test', '8.x-2.0', '8.x-3.0', "$fixtures_folder/legacy_unsupported_update.staged.installed.json", TRUE],
]; ];
} }
/**
* Tests updating to a release during pre-apply.
*
* @param string $project
* The project to update.
* @param string $installed_version
* The installed version of the project.
* @param string $target_version
* The target version.
* @param string $staged_installed
* Path of `staged.installed.json` file. It will be used as the virtual
* project's staged `vendor/composer/installed.json` file.
* @param bool $error_expected
* Whether an error is expected in the update.
*
* @dataProvider providerTestPreApplyException
*/
public function testPreApplyException(string $project, string $installed_version, string $target_version, string $staged_installed, bool $error_expected): void {
$this->enableModules(['aaa_automatic_updates_test', $project]);
$module_info = ['version' => $installed_version, 'project' => $project];
$aaa_automatic_updates_test_info = ['version' => '7.0.0', 'project' => 'aaa_automatic_updates_test'];
$this->config('update_test.settings')
->set("system_info.$project", $module_info)
->set("system_info.aaa_automatic_updates_test", $aaa_automatic_updates_test_info)
->save();
// Path of `active.installed.json` file. It will be used as the virtual
// project's active `vendor/composer/installed.json` file.
$active_installed = __DIR__ . '/../../../fixtures/update_release_validator/active.installed.json';
$this->assertFileIsReadable($active_installed);
$this->assertFileIsReadable($staged_installed);
$this->setReleaseMetadataForProjects([
'aaa_automatic_updates_test' => __DIR__ . "/../../../../../tests/fixtures/release-history/aaa_automatic_updates_test.9.8.2.xml",
$project => __DIR__ . "/../../../fixtures/release-history/$project.1.1.xml",
'drupal' => __DIR__ . '/../../../../../tests/fixtures/release-history/drupal.9.8.2.xml',
]);
// Copying `active.installed.json` and 'staged.installed.json' to the
// virtual project's active and staged directories respectively.
$active_dir = $this->container->get('package_manager.path_locator')->getProjectRoot();
copy($active_installed, "$active_dir/vendor/composer/installed.json");
$listener = function (PreApplyEvent $event) use ($staged_installed): void {
$stage_dir = $event->getStage()->getStageDirectory();
copy($staged_installed, $stage_dir . "/vendor/composer/installed.json");
};
$this->container->get('event_dispatcher')->addListener(PreApplyEvent::class, $listener, 1000);
if ($error_expected) {
$expected_results = [
ValidationResult::createError(
["Project $project to version " . LegacyVersionUtility::convertToSemanticVersion($target_version)],
t('Cannot update because the following project version is not in the list of installable releases.')
),
];
}
else {
$expected_results = [];
}
// Always updating aaa_automatic_updates_test to 7.0.1(valid release) along
// with the project provided for test.
$this->assertUpdateResults(
[
'aaa_automatic_updates_test' => '7.0.1',
],
$expected_results,
PreApplyEvent::class
);
}
} }
...@@ -86,7 +86,8 @@ final class ProjectInfo { ...@@ -86,7 +86,8 @@ final class ProjectInfo {
* If the project information is available, an array of releases that can be * If the project information is available, an array of releases that can be
* installed, keyed by version number; otherwise NULL. The releases are in * installed, keyed by version number; otherwise NULL. The releases are in
* descending order by version number (i.e., higher versions are listed * descending order by version number (i.e., higher versions are listed
* first). * first). The currently installed version of the project, and any older
* versions, are not considered installable releases.
* *
* @throws \RuntimeException * @throws \RuntimeException
* Thrown if there are no available releases. * Thrown if there are no available releases.
......
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