diff --git a/src/Validator/VersionPolicy/SupportedBranchInstalled.php b/src/Validator/VersionPolicy/SupportedBranchInstalled.php new file mode 100644 index 0000000000000000000000000000000000000000..b3f16a020460c91b2a94626f515b60907f064019 --- /dev/null +++ b/src/Validator/VersionPolicy/SupportedBranchInstalled.php @@ -0,0 +1,110 @@ +<?php + +namespace Drupal\automatic_updates\Validator\VersionPolicy; + +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Extension\ExtensionVersion; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\Url; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * A policy rule that requires updating from a supported branch. + * + * @internal + * This is an internal part of Automatic Updates' version policy for + * Drupal core. It may be changed or removed at any time without warning. + * External code should not interact with this class. + */ +class SupportedBranchInstalled implements ContainerInjectionInterface { + + use StringTranslationTrait; + + /** + * The config factory service. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + private $configFactory; + + /** + * Constructs a SupportedBranchInstalled object. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory service. + */ + public function __construct(ConfigFactoryInterface $config_factory) { + $this->configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory') + ); + } + + /** + * Checks if the installed version of Drupal is in a supported branch. + * + * @param string $installed_version + * The installed version of Drupal. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup[] + * The error messages, if any. + */ + public function validate(string $installed_version): array { + $available_updates = update_get_available(TRUE); + + $installed = ExtensionVersion::createFromVersionString($installed_version); + $installed_major = $installed->getMajorVersion(); + $installed_minor = $installed->getMinorVersion(); + $in_supported_major = FALSE; + + $supported_branches = explode(',', $available_updates['drupal']['supported_branches']); + foreach ($supported_branches as $supported_branch) { + $supported_branch = ExtensionVersion::createFromSupportBranch($supported_branch); + + // Check if this supported branch is in the same major version as what's + // installed, since that will influence our messaging. + if ($installed_major === $supported_branch->getMajorVersion()) { + $in_supported_major = TRUE; + + // If the supported branch's major and minor versions are the same as + // the installed ones, this rule is fulfilled. + if ($installed_minor === $supported_branch->getMinorVersion()) { + return []; + } + } + } + + // By this point, we know the installed version of Drupal is not in a + // supported branch, so we'll always show this message. + $messages = [ + $this->t('The currently installed version of Drupal core, @installed_version, is not in a supported minor version. Your site will not be automatically updated during cron until it is updated to a supported minor version.', [ + '@installed_version' => $installed_version, + ]), + ]; + + // If the installed version of Drupal is in a supported major branch, an + // attended update may be possible, depending on configuration. + $allow_minor_updates = $this->configFactory->get('automatic_updates.settings') + ->get('allow_core_minor_updates'); + + if ($in_supported_major && $allow_minor_updates) { + $messages[] = $this->t('Use the <a href=":url">update form</a> to update to a supported version.', [ + ':url' => Url::fromRoute('automatic_updates.module_update')->toString(), + ]); + } + else { + $messages[] = $this->t('See the <a href=":url">available updates page</a> for available updates.', [ + ':url' => Url::fromRoute('update.status')->toString(), + ]); + } + return $messages; + } + +} diff --git a/src/Validator/VersionPolicyValidator.php b/src/Validator/VersionPolicyValidator.php index 27b27e2e9c6e7e4cba3a79d8f21f21b79221bf77..37c26e68d520d4a2591e0b15a0b9c9836a35adc9 100644 --- a/src/Validator/VersionPolicyValidator.php +++ b/src/Validator/VersionPolicyValidator.php @@ -13,6 +13,7 @@ use Drupal\automatic_updates\Validator\VersionPolicy\MajorVersionMatch; use Drupal\automatic_updates\Validator\VersionPolicy\MinorUpdatesEnabled; use Drupal\automatic_updates\Validator\VersionPolicy\StableReleaseInstalled; use Drupal\automatic_updates\Validator\VersionPolicy\ForbidDevSnapshot; +use Drupal\automatic_updates\Validator\VersionPolicy\SupportedBranchInstalled; use Drupal\automatic_updates\Validator\VersionPolicy\TargetSecurityRelease; use Drupal\automatic_updates\Validator\VersionPolicy\TargetVersionInstallable; use Drupal\automatic_updates\Validator\VersionPolicy\TargetVersionStable; @@ -88,6 +89,8 @@ final class VersionPolicyValidator implements EventSubscriberInterface { // If cron updates are enabled, the installed version must be stable; // no alphas, betas, or RCs. $rules[] = StableReleaseInstalled::class; + // It must also be in a supported branch. + $rules[] = SupportedBranchInstalled::class; // If the target version is known, more rules apply. if ($target_version) { @@ -145,10 +148,19 @@ final class VersionPolicyValidator implements EventSubscriberInterface { $messages = $this->validateVersion($stage, $target_version); if ($messages) { - $summary = $this->t('Updating from Drupal @installed_version to @target_version is not allowed.', [ - '@installed_version' => $this->getInstalledVersion(), - '@target_version' => $target_version, - ]); + $installed_version = $this->getInstalledVersion(); + + if ($target_version) { + $summary = $this->t('Updating from Drupal @installed_version to @target_version is not allowed.', [ + '@installed_version' => $installed_version, + '@target_version' => $target_version, + ]); + } + else { + $summary = $this->t('Updating from Drupal @installed_version is not allowed.', [ + '@installed_version' => $installed_version, + ]); + } $event->addError($messages, $summary); } } diff --git a/tests/src/Kernel/ReadinessValidation/VersionPolicy/SupportedBranchInstalledTest.php b/tests/src/Kernel/ReadinessValidation/VersionPolicy/SupportedBranchInstalledTest.php new file mode 100644 index 0000000000000000000000000000000000000000..b8cfaf34b08fc68fb57023043891c78691389329 --- /dev/null +++ b/tests/src/Kernel/ReadinessValidation/VersionPolicy/SupportedBranchInstalledTest.php @@ -0,0 +1,93 @@ +<?php + +namespace Drupal\Tests\automatic_updates\Kernel\ReadinessValidation\VersionPolicy; + +use Drupal\automatic_updates\Validator\VersionPolicy\SupportedBranchInstalled; +use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase; + +/** + * @covers \Drupal\automatic_updates\Validator\VersionPolicy\SupportedBranchInstalled + * + * @group automatic_updates + */ +class SupportedBranchInstalledTest extends AutomaticUpdatesKernelTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['automatic_updates']; + + /** + * Data provider for ::testSupportedBranchInstalled(). + * + * @return array[] + * Sets of arguments to pass to the test method. + */ + public function providerSupportedBranchInstalled(): array { + return [ + 'supported minor installed' => [ + '9.8.0', + [FALSE, TRUE], + [], + ], + // These two cases test a supported major version, but unsupported minor + // version. + 'supported major installed, minor updates forbidden' => [ + '9.6.1', + [FALSE], + [ + 'The currently installed version of Drupal core, 9.6.1, is not in a supported minor version. Your site will not be automatically updated during cron until it is updated to a supported minor version.', + 'See the <a href="/admin/reports/updates">available updates page</a> for available updates.', + ], + ], + 'supported major installed, minor updates allowed' => [ + '9.6.1', + [TRUE], + [ + 'The currently installed version of Drupal core, 9.6.1, is not in a supported minor version. Your site will not be automatically updated during cron until it is updated to a supported minor version.', + 'Use the <a href="/admin/modules/automatic-update">update form</a> to update to a supported version.', + ], + ], + 'unsupported version installed' => [ + '8.9.0', + [FALSE, TRUE], + [ + 'The currently installed version of Drupal core, 8.9.0, is not in a supported minor version. Your site will not be automatically updated during cron until it is updated to a supported minor version.', + 'See the <a href="/admin/reports/updates">available updates page</a> for available updates.', + ], + ], + ]; + } + + /** + * Tests that the installed version of Drupal must be in a supported branch. + * + * @param string $installed_version + * The installed version of Drupal core. + * @param bool[] $allow_minor_updates + * The values of the `allow_core_minor_updates` config setting that should + * be tested. + * @param string[] $expected_errors + * The expected error messages, if any. + * + * @dataProvider providerSupportedBranchInstalled + */ + public function testSupportedBranchInstalled(string $installed_version, array $allow_minor_updates, array $expected_errors): void { + $this->setCoreVersion($installed_version); + $this->setReleaseMetadata([ + 'drupal' => __DIR__ . '/../../../../fixtures/release-history/drupal.9.8.2.xml', + ]); + + $rule = SupportedBranchInstalled::create($this->container); + + foreach ($allow_minor_updates as $setting) { + $this->config('automatic_updates.settings') + ->set('allow_core_minor_updates', $setting) + ->save(); + + $actual_errors = array_map('strval', $rule->validate($installed_version)); + $this->assertSame($expected_errors, $actual_errors); + } + } + +} diff --git a/tests/src/Kernel/ReadinessValidation/VersionPolicyValidatorTest.php b/tests/src/Kernel/ReadinessValidation/VersionPolicyValidatorTest.php index d69cd59fcc6d7347cb242e7087b8507f7cc16bb0..15d01b7841c2845fe7b9bddbe05cc87b5bcf56b9 100644 --- a/tests/src/Kernel/ReadinessValidation/VersionPolicyValidatorTest.php +++ b/tests/src/Kernel/ReadinessValidation/VersionPolicyValidatorTest.php @@ -120,6 +120,41 @@ class VersionPolicyValidatorTest extends AutomaticUpdatesKernelTestBase { ]), ], ], + // These three cases prove that updating from an unsupported minor version + // will raise a readiness error if unattended updates are enabled. + // Furthermore, if an error is raised, the messaging will vary depending + // on whether attended updates across minor versions are allowed. (Note + // that the target version will not be automatically detected because the + // release metadata used in these cases doesn't have any 9.7.x releases.) + 'update from unsupported minor, cron disabled' => [ + '9.7.1', + "$metadata_dir/drupal.9.8.1-security.xml", + [CronUpdater::DISABLED], + [], + ], + 'update from unsupported minor, cron enabled, minor updates forbidden' => [ + '9.7.1', + "$metadata_dir/drupal.9.8.1-security.xml", + [CronUpdater::SECURITY, CronUpdater::ALL], + [ + $this->createValidationResult('9.7.1', NULL, [ + 'The currently installed version of Drupal core, 9.7.1, is not in a supported minor version. Your site will not be automatically updated during cron until it is updated to a supported minor version.', + 'See the <a href="/admin/reports/updates">available updates page</a> for available updates.', + ]), + ], + ], + 'update from unsupported minor, cron enabled, minor updates allowed' => [ + '9.7.1', + "$metadata_dir/drupal.9.8.1-security.xml", + [CronUpdater::SECURITY, CronUpdater::ALL], + [ + $this->createValidationResult('9.7.1', NULL, [ + 'The currently installed version of Drupal core, 9.7.1, is not in a supported minor version. Your site will not be automatically updated during cron until it is updated to a supported minor version.', + 'Use the <a href="/admin/modules/automatic-update">update form</a> to update to a supported version.', + ]), + ], + TRUE, + ], ]; } @@ -137,16 +172,20 @@ class VersionPolicyValidatorTest extends AutomaticUpdatesKernelTestBase { * \Drupal\automatic_updates\CronUpdater::ALL. * @param \Drupal\package_manager\ValidationResult[] $expected_results * The expected validation results. + * @param bool $allow_minor_updates + * (optional) Whether or not attended updates across minor updates are + * allowed. Defaults to FALSE. * * @dataProvider providerReadinessCheck */ - public function testReadinessCheck(string $installed_version, string $release_metadata, array $cron_modes, array $expected_results): void { + public function testReadinessCheck(string $installed_version, string $release_metadata, array $cron_modes, array $expected_results, bool $allow_minor_updates = FALSE): void { $this->setCoreVersion($installed_version); $this->setReleaseMetadata(['drupal' => $release_metadata]); foreach ($cron_modes as $cron_mode) { $this->config('automatic_updates.settings') ->set('cron', $cron_mode) + ->set('allow_core_minor_updates', $allow_minor_updates) ->save(); $this->assertCheckerResultsFromManager($expected_results, TRUE); @@ -282,6 +321,17 @@ class VersionPolicyValidatorTest extends AutomaticUpdatesKernelTestBase { [], TRUE, ], + // If attended updates across minor versions are allowed, it's okay to + // update from an unsupported minor version. + 'attended update from unsupported minor allowed' => [ + ['automatic_updates.updater'], + '9.7.9', + "$metadata_dir/drupal.9.8.1-security.xml", + [CronUpdater::SECURITY, CronUpdater::ALL], + ['drupal' => '9.8.1'], + [], + TRUE, + ], // Unattended updates to unstable versions are not allowed. 'unattended update to unstable version' => [ ['automatic_updates.cron_updater'], @@ -295,6 +345,37 @@ class VersionPolicyValidatorTest extends AutomaticUpdatesKernelTestBase { ]), ], ], + // Unattended updates from an unsupported minor are never allowed, but + // the messaging will vary depending on whether attended updates across + // minor versions are allowed. + 'unattended update from unsupported minor, minor updates forbidden' => [ + ['automatic_updates.cron_updater'], + '9.7.9', + "$metadata_dir/drupal.9.8.1-security.xml", + [CronUpdater::SECURITY, CronUpdater::ALL], + ['drupal' => '9.8.1'], + [ + $this->createValidationResult('9.7.9', '9.8.1', [ + 'The currently installed version of Drupal core, 9.7.9, is not in a supported minor version. Your site will not be automatically updated during cron until it is updated to a supported minor version.', + 'See the <a href="/admin/reports/updates">available updates page</a> for available updates.', + ]), + ], + FALSE, + ], + 'unattended update from unsupported minor, minor updates allowed' => [ + ['automatic_updates.cron_updater'], + '9.7.9', + "$metadata_dir/drupal.9.8.1-security.xml", + [CronUpdater::SECURITY, CronUpdater::ALL], + ['drupal' => '9.8.1'], + [ + $this->createValidationResult('9.7.9', '9.8.1', [ + 'The currently installed version of Drupal core, 9.7.9, is not in a supported minor version. Your site will not be automatically updated during cron until it is updated to a supported minor version.', + 'Use the <a href="/admin/modules/automatic-update">update form</a> to update to a supported version.', + ]), + ], + TRUE, + ], ]; } @@ -358,19 +439,26 @@ class VersionPolicyValidatorTest extends AutomaticUpdatesKernelTestBase { * * @param string $installed_version * The installed version of Drupal core. - * @param string $target_version - * The target version of Drupal core. + * @param string|null $target_version + * The target version of Drupal core, or NULL if it's not known. * @param string[] $messages * The error messages that the result should contain. * * @return \Drupal\package_manager\ValidationResult * A validation error object with the appropriate summary. */ - private function createValidationResult(string $installed_version, string $target_version, array $messages): ValidationResult { - $summary = t('Updating from Drupal @installed_version to @target_version is not allowed.', [ - '@installed_version' => $installed_version, - '@target_version' => $target_version, - ]); + private function createValidationResult(string $installed_version, ?string $target_version, array $messages): ValidationResult { + if ($target_version) { + $summary = t('Updating from Drupal @installed_version to @target_version is not allowed.', [ + '@installed_version' => $installed_version, + '@target_version' => $target_version, + ]); + } + else { + $summary = t('Updating from Drupal @installed_version is not allowed.', [ + '@installed_version' => $installed_version, + ]); + } return ValidationResult::createError($messages, $summary); }