diff --git a/automatic_updates_extensions/src/Validator/UpdateReleaseValidator.php b/automatic_updates_extensions/src/Validator/UpdateReleaseValidator.php index 600afe7dbde773a6052b129b10e4f322198ba95c..115479d19ddf2822f204a0bf2f256232bc9994f7 100644 --- a/automatic_updates_extensions/src/Validator/UpdateReleaseValidator.php +++ b/automatic_updates_extensions/src/Validator/UpdateReleaseValidator.php @@ -6,6 +6,7 @@ use Drupal\automatic_updates\ProjectInfo; use Drupal\automatic_updates_extensions\ExtensionUpdater; use Drupal\automatic_updates\LegacyVersionUtility; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\package_manager\Event\PreApplyEvent; use Drupal\package_manager\Event\PreCreateEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -20,6 +21,83 @@ final class UpdateReleaseValidator implements EventSubscriberInterface { 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. * @@ -41,33 +119,12 @@ final class UpdateReleaseValidator implements EventSubscriberInterface { $project_name = $package_parts[1]; // 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. - $releases = (new ProjectInfo($project_name))->getInstallableReleases(); - $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) { + if (!$this->isSupportedRelease($project_name, $sematic_version)) { $messages[] = $this->t('Project @project_name to version @version', [ '@project_name' => $project_name, '@version' => $sematic_version, ]); } - } } if ($messages) { @@ -86,6 +143,7 @@ final class UpdateReleaseValidator implements EventSubscriberInterface { public static function getSubscribedEvents() { return [ PreCreateEvent::class => 'checkRelease', + PreApplyEvent::class => 'checkStagedReleases', ]; } diff --git a/automatic_updates_extensions/tests/fixtures/update_release_validator/active.installed.json b/automatic_updates_extensions/tests/fixtures/update_release_validator/active.installed.json new file mode 100644 index 0000000000000000000000000000000000000000..4e077b457077d987dbbfdc465ca7ba5cf56f9454 --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/update_release_validator/active.installed.json @@ -0,0 +1,35 @@ +{ + "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" + } + ] +} diff --git a/automatic_updates_extensions/tests/fixtures/update_release_validator/legacy_supported_update.staged.installed.json b/automatic_updates_extensions/tests/fixtures/update_release_validator/legacy_supported_update.staged.installed.json new file mode 100644 index 0000000000000000000000000000000000000000..2738b13cd4c4473cefc4bbb92666a684b032ca38 --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/update_release_validator/legacy_supported_update.staged.installed.json @@ -0,0 +1,25 @@ +{ + "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" + } + ] +} diff --git a/automatic_updates_extensions/tests/fixtures/update_release_validator/legacy_unsupported_update.staged.installed.json b/automatic_updates_extensions/tests/fixtures/update_release_validator/legacy_unsupported_update.staged.installed.json new file mode 100644 index 0000000000000000000000000000000000000000..d7ef4fbab2db1a0a2c19a56b80bdfe08d9243834 --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/update_release_validator/legacy_unsupported_update.staged.installed.json @@ -0,0 +1,25 @@ +{ + "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" + } + ] +} diff --git a/automatic_updates_extensions/tests/fixtures/update_release_validator/semver_supported_update.staged.installed.json b/automatic_updates_extensions/tests/fixtures/update_release_validator/semver_supported_update.staged.installed.json new file mode 100644 index 0000000000000000000000000000000000000000..2ed8f728c71e4946e0ef6e8f8c365528a930793d --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/update_release_validator/semver_supported_update.staged.installed.json @@ -0,0 +1,25 @@ +{ + "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" + } + ] +} diff --git a/automatic_updates_extensions/tests/fixtures/update_release_validator/semver_unsupported_update.staged.installed.json b/automatic_updates_extensions/tests/fixtures/update_release_validator/semver_unsupported_update.staged.installed.json new file mode 100644 index 0000000000000000000000000000000000000000..0d4ca73efa51008ce73638238e45db7731bfbc0e --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/update_release_validator/semver_unsupported_update.staged.installed.json @@ -0,0 +1,25 @@ +{ + "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" + } + ] +} diff --git a/automatic_updates_extensions/tests/src/Functional/UpdaterFormTest.php b/automatic_updates_extensions/tests/src/Functional/UpdaterFormTest.php index 1e7e9fd9ccd31ff3cd0c29cf0b8c5f40e6cf5182..ccb42646a28ade83dbd035f7740a41c45d464ed0 100644 --- a/automatic_updates_extensions/tests/src/Functional/UpdaterFormTest.php +++ b/automatic_updates_extensions/tests/src/Functional/UpdaterFormTest.php @@ -47,8 +47,8 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase { */ public function providerSuccessfulUpdate() { return [ - 'maintiance_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 on, semver' => [TRUE, 'semver_test', '8.1.0', '8.1.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 { * @dataProvider providerSuccessfulUpdate */ 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 - // read composer.json from the staging area, which won't exist because - // Package Manager is bypassed. - $this->disableValidators(['automatic_updates.validator.scaffold_file_permissions']); + // Disable the scaffold file permissions and target release validators + // because they will try to read composer.json from the staging area, + // which won't exist because Package Manager is bypassed. + $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->updateProject = $project_name; diff --git a/automatic_updates_extensions/tests/src/Kernel/Validator/UpdateReleaseValidatorTest.php b/automatic_updates_extensions/tests/src/Kernel/Validator/UpdateReleaseValidatorTest.php index 4b0714b0bbe88b60d7ba6b9107b67d70048d696e..f2d36d54133228ca9406b39781933d42f752c4ef 100644 --- a/automatic_updates_extensions/tests/src/Kernel/Validator/UpdateReleaseValidatorTest.php +++ b/automatic_updates_extensions/tests/src/Kernel/Validator/UpdateReleaseValidatorTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\automatic_updates_extensions\Kernel\Validator; use Drupal\automatic_updates\LegacyVersionUtility; +use Drupal\package_manager\Event\PreApplyEvent; use Drupal\package_manager\Event\PreCreateEvent; use Drupal\package_manager\ValidationResult; use Drupal\Tests\automatic_updates_extensions\Kernel\AutomaticUpdatesExtensionsKernelTestBase; @@ -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 * The project to update. @@ -34,18 +50,21 @@ class UpdateReleaseValidatorTest extends AutomaticUpdatesExtensionsKernelTestBas * @param bool $error_expected * 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]); + $module_info = ['version' => $installed_version, 'project' => $project]; $this->config('update_test.settings') ->set("system_info.$project", $module_info) ->save(); + $this->setReleaseMetadataForProjects([ $project => __DIR__ . "/../../../fixtures/release-history/$project.1.1.xml", 'drupal' => __DIR__ . '/../../../../../tests/fixtures/release-history/drupal.9.8.2.xml', ]); + if ($error_expected) { $expected_results = [ ValidationResult::createError( @@ -62,18 +81,90 @@ class UpdateReleaseValidatorTest extends AutomaticUpdatesExtensionsKernelTestBas } /** - * Data provider for testRelease(). + * Data provider for testPreApplyException(). * * @return array[] * The test cases. */ - public function providerTestRelease() { + public function providerTestPreApplyException(): array { + $fixtures_folder = __DIR__ . '/../../../fixtures/update_release_validator'; 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], + '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', "$fixtures_folder/semver_unsupported_update.staged.installed.json", TRUE], + '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', "$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 + ); + } + } diff --git a/src/ProjectInfo.php b/src/ProjectInfo.php index c725d7ca39aafe5c33f6707b2e23794dd2ffb466..09d0ed8eafdf7908164596600097325b1e572eaf 100644 --- a/src/ProjectInfo.php +++ b/src/ProjectInfo.php @@ -86,7 +86,8 @@ final class ProjectInfo { * If the project information is available, an array of releases that can be * installed, keyed by version number; otherwise NULL. The releases are in * 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 * Thrown if there are no available releases.