Skip to content
Snippets Groups Projects
Commit 2487d9c2 authored by Ted Bowman's avatar Ted Bowman
Browse files

Issue #3254755 by tedbow, kunal.sachdev, phenaproxima: Add a seperate Update...

Issue #3254755 by tedbow, kunal.sachdev, phenaproxima: Add a seperate Update recommender and update version validator for cron updates
parent 561489b7
No related branches found
No related tags found
1 merge request!207Issue #3254755: Add method to UpdateRecommender to recommend release during cron.
Showing
with 1100 additions and 280 deletions
......@@ -6,9 +6,9 @@
*/
use Drupal\automatic_updates\BatchProcessor;
use Drupal\automatic_updates\ProjectInfo;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\automatic_updates\CronUpdater;
use Drupal\automatic_updates\UpdateRecommender;
use Drupal\automatic_updates\Validation\AdminReadinessMessages;
use Drupal\Core\Extension\ExtensionVersion;
use Drupal\Core\Form\FormStateInterface;
......@@ -141,9 +141,8 @@ function automatic_updates_form_update_manager_update_form_alter(&$form, FormSta
* Implements hook_form_FORM_ID_alter() for 'update_settings' form.
*/
function automatic_updates_form_update_settings_alter(array &$form, FormStateInterface $form_state, string $form_id) {
$recommender = new UpdateRecommender();
$drupal_project = $recommender->getProjectInfo();
$version = ExtensionVersion::createFromVersionString($drupal_project['existing_version']);
$project_info = new ProjectInfo();
$version = ExtensionVersion::createFromVersionString($project_info->getInstalledVersion());
$current_minor = $version->getMajorVersion() . '.' . $version->getMinorVersion();
// @todo In https://www.drupal.org/node/2998285 use the update XML to
// determine when the installed of core will become unsupported.
......
......@@ -26,6 +26,7 @@ services:
automatic_updates.cron_updater:
class: Drupal\automatic_updates\CronUpdater
arguments:
- '@automatic_updates.cron_release_chooser'
- '@logger.factory'
- '@config.factory'
- '@package_manager.path_locator'
......@@ -55,6 +56,21 @@ services:
- '@config.factory'
tags:
- { name: event_subscriber }
automatic_updates.cron_update_version_validator:
class: Drupal\automatic_updates\Validator\CronUpdateVersionValidator
arguments:
- '@string_translation'
- '@config.factory'
tags:
- { name: event_subscriber }
automatic_updates.release_chooser:
class: Drupal\automatic_updates\ReleaseChooser
arguments:
- '@automatic_updates.update_version_validator'
automatic_updates.cron_release_chooser:
class: Drupal\automatic_updates\ReleaseChooser
arguments:
- '@automatic_updates.cron_update_version_validator'
automatic_updates.composer_executable_validator:
class: Drupal\automatic_updates\Validator\PackageManagerReadinessCheck
arguments:
......
......@@ -42,16 +42,26 @@ class CronUpdater extends Updater {
*/
protected $logger;
/**
* The cron release chooser service.
*
* @var \Drupal\automatic_updates\ReleaseChooser
*/
protected $releaseChooser;
/**
* Constructs a CronUpdater object.
*
* @param \Drupal\automatic_updates\ReleaseChooser $release_chooser
* The cron release chooser service.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* The logger channel factory.
* @param mixed ...$arguments
* Additional arguments to pass to the parent constructor.
*/
public function __construct(LoggerChannelFactoryInterface $logger_factory, ...$arguments) {
public function __construct(ReleaseChooser $release_chooser, LoggerChannelFactoryInterface $logger_factory, ...$arguments) {
parent::__construct(...$arguments);
$this->releaseChooser = $release_chooser;
$this->logger = $logger_factory->get('automatic_updates');
}
......@@ -59,50 +69,35 @@ class CronUpdater extends Updater {
* Handles updates during cron.
*/
public function handleCron(): void {
$level = $this->configFactory->get('automatic_updates.settings')
->get('cron');
// If automatic updates are disabled, bail out.
if ($level === static::DISABLED) {
if ($this->isDisabled()) {
return;
}
$recommender = new UpdateRecommender();
try {
$recommended_release = $recommender->getRecommendedRelease(TRUE);
}
catch (\Throwable $e) {
$this->logger->error($e->getMessage());
return;
}
// If we're already up-to-date, there's nothing else we need to do.
if ($recommended_release === NULL) {
return;
$next_release = $this->releaseChooser->refresh()->getLatestInInstalledMinor();
if ($next_release) {
$this->performUpdate($next_release->getVersion());
}
}
$project = $recommender->getProjectInfo();
if (empty($project['existing_version'])) {
/**
* Performs the update.
*
* @param string $update_version
* The version to which to update.
*/
private function performUpdate(string $update_version): void {
$installed_version = (new ProjectInfo())->getInstalledVersion();
if (empty($installed_version)) {
$this->logger->error('Unable to determine the current version of Drupal core.');
return;
}
// If automatic updates are only enabled for security releases, bail out if
// the recommended release is not a security release.
if ($level === static::SECURITY && !$recommended_release->isSecurityRelease()) {
return;
}
// @todo Use the queue to add update jobs allowing jobs to span multiple
// cron runs.
$recommended_version = $recommended_release->getVersion();
// Do the bulk of the update in its own try-catch structure, so that we can
// handle any exceptions or validation errors consistently, and destroy the
// stage regardless of whether the update succeeds.
try {
$this->begin([
'drupal' => $recommended_version,
'drupal' => $update_version,
]);
$this->stage();
$this->apply();
......@@ -110,8 +105,8 @@ class CronUpdater extends Updater {
$this->logger->info(
'Drupal core has been updated from %previous_version to %update_version',
[
'%previous_version' => $project['existing_version'],
'%update_version' => $recommended_version,
'%previous_version' => $installed_version,
'%update_version' => $update_version,
]
);
}
......@@ -138,6 +133,16 @@ class CronUpdater extends Updater {
}
}
/**
* Determines if cron updates are disabled.
*
* @return bool
* TRUE if cron updates are disabled, otherwise FALSE.
*/
private function isDisabled(): bool {
return $this->configFactory->get('automatic_updates.settings')->get('cron') === static::DISABLED;
}
/**
* Generates a log message from a stage validation exception.
*
......
......@@ -4,8 +4,9 @@ namespace Drupal\automatic_updates\Form;
use Drupal\automatic_updates\BatchProcessor;
use Drupal\automatic_updates\Event\ReadinessCheckEvent;
use Drupal\automatic_updates\ProjectInfo;
use Drupal\automatic_updates\ReleaseChooser;
use Drupal\automatic_updates\Updater;
use Drupal\automatic_updates\UpdateRecommender;
use Drupal\automatic_updates\Validation\ReadinessTrait;
use Drupal\Core\Batch\BatchBuilder;
use Drupal\Core\Form\FormBase;
......@@ -19,7 +20,6 @@ use Drupal\system\SystemManager;
use Drupal\update\UpdateManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
/**
* Defines a form to update Drupal core.
......@@ -53,11 +53,11 @@ class UpdaterForm extends FormBase {
protected $eventDispatcher;
/**
* The current session.
* The release chooser service.
*
* @var \Symfony\Component\HttpFoundation\Session\SessionInterface
* @var \Drupal\automatic_updates\ReleaseChooser
*/
protected $session;
protected $releaseChooser;
/**
* Constructs a new UpdaterForm object.
......@@ -68,14 +68,14 @@ class UpdaterForm extends FormBase {
* The updater service.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher service.
* @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session
* The current session.
* @param \Drupal\automatic_updates\ReleaseChooser $release_chooser
* The release chooser service.
*/
public function __construct(StateInterface $state, Updater $updater, EventDispatcherInterface $event_dispatcher, SessionInterface $session) {
public function __construct(StateInterface $state, Updater $updater, EventDispatcherInterface $event_dispatcher, ReleaseChooser $release_chooser) {
$this->updater = $updater;
$this->state = $state;
$this->eventDispatcher = $event_dispatcher;
$this->session = $session;
$this->releaseChooser = $release_chooser;
}
/**
......@@ -93,7 +93,7 @@ class UpdaterForm extends FormBase {
$container->get('state'),
$container->get('automatic_updates.updater'),
$container->get('event_dispatcher'),
$container->get('session')
$container->get('automatic_updates.release_chooser')
);
}
......@@ -112,7 +112,7 @@ class UpdaterForm extends FormBase {
// If there's a stage ID stored in the session, try to claim the stage
// with it. If we succeed, then an update is already in progress, and the
// current session started it, so redirect them to the confirmation form.
$stage_id = $this->session->get(BatchProcessor::STAGE_ID_SESSION_KEY);
$stage_id = $this->getRequest()->getSession()->get(BatchProcessor::STAGE_ID_SESSION_KEY);
if ($stage_id) {
try {
$this->updater->claim($stage_id);
......@@ -131,10 +131,24 @@ class UpdaterForm extends FormBase {
'#theme' => 'update_last_check',
'#last' => $this->state->get('update.last_check', 0),
];
$project_info = new ProjectInfo();
$recommender = new UpdateRecommender();
try {
$recommended_release = $recommender->getRecommendedRelease(TRUE);
// @todo Until https://www.drupal.org/i/3264849 is fixed, we can only show
// one release on the form. First, try to show the latest release in the
// currently installed minor. Failing that, try to show the latest
// release in the next minor. If neither of those are available, just
// show the first available release.
$recommended_release = $this->releaseChooser->refresh()->getLatestInInstalledMinor();
if (!$recommended_release) {
$recommended_release = $this->releaseChooser->getLatestInNextMinor();
if (!$recommended_release) {
// @todo Do not list an update that can't be validated in
// https://www.drupal.org/i/3271235.
$updates = $project_info->getInstallableReleases();
$recommended_release = array_pop($updates);
}
}
}
catch (\RuntimeException $e) {
$form['message'] = [
......@@ -148,6 +162,9 @@ class UpdaterForm extends FormBase {
// If we're already up-to-date, there's nothing else we need to do.
if ($recommended_release === NULL) {
// @todo Link to the Available Updates report if there are other updates
// that are not supported by this module in
// https://www.drupal.org/i/3271235.
$this->messenger()->addMessage('No update available');
return $form;
}
......@@ -159,7 +176,7 @@ class UpdaterForm extends FormBase {
],
];
$project = $recommender->getProjectInfo();
$project = $project_info->getProjectInfo();
if (empty($project['title']) || empty($project['link'])) {
throw new \UnexpectedValueException('Expected project data to have a title and link.');
}
......@@ -187,7 +204,7 @@ class UpdaterForm extends FormBase {
'title' => [
'data' => $title,
],
'installed_version' => $project['existing_version'],
'installed_version' => $project_info->getInstalledVersion(),
'recommended_version' => [
'data' => [
// @todo Is an inline template the right tool here? Is there an Update
......
......@@ -2,13 +2,18 @@
namespace Drupal\automatic_updates;
use Composer\Semver\Comparator;
use Composer\Semver\Semver;
use Drupal\automatic_updates_9_3_shim\ProjectRelease;
use Drupal\update\UpdateManagerInterface;
/**
* Determines the recommended release of Drupal core to update to.
* Defines a class for retrieving project information from Update module.
*
* @todo Allow passing a project name to handle more than Drupal core in
* https://www.drupal.org/i/3271240.
*/
class UpdateRecommender {
class ProjectInfo {
/**
* Returns up-to-date project information for Drupal core.
......@@ -26,44 +31,76 @@ class UpdateRecommender {
*/
public function getProjectInfo(bool $refresh = FALSE): array {
$available_updates = update_get_available($refresh);
if (empty($available_updates)) {
throw new \RuntimeException('There was a problem getting update information. Try again later.');
}
$project_data = update_calculate_project_data($available_updates);
return $project_data['drupal'];
}
/**
* Returns the recommended release of Drupal core.
* Gets all releases of Drupal core to which the site can update.
*
* @param bool $refresh
* (optional) Whether to fetch the latest information about available
* updates from drupal.org. This can be an expensive operation, so defaults
* to FALSE.
*
* @return \Drupal\automatic_updates_9_3_shim\ProjectRelease|null
* A value object with information about the recommended release, or NULL
* if Drupal core is already up-to-date.
* @return \Drupal\automatic_updates_9_3_shim\ProjectRelease[]
* An array of possible update releases with release versions as keys. The
* releases are in descending order by version number (i.e., higher versions
* are listed first).
*
* @throws \RuntimeException
* Thrown if $refresh is TRUE and there are no available releases.
*
* @throws \LogicException
* If Drupal core is out of date and the recommended version of cannot be
* determined.
* @todo Remove or simplify this function in https://www.drupal.org/i/3252190.
*/
public function getRecommendedRelease(bool $refresh = FALSE): ?ProjectRelease {
public function getInstallableReleases(bool $refresh = FALSE): array {
$project = $this->getProjectInfo($refresh);
$installed_version = $this->getInstalledVersion();
// If we refreshed and we were able to get available releases we should
// always have at least have the current release stored.
if ($refresh && empty($project['releases'])) {
throw new \RuntimeException('There was a problem getting update information. Try again later.');
}
// If we're already up-to-date, there's nothing else we need to do.
if ($project['status'] === UpdateManagerInterface::CURRENT) {
return NULL;
return [];
}
// If we don't know what to recommend they update to, time to freak out.
elseif (empty($project['recommended'])) {
// If we don't know what to recommend they update to, time to freak out.
throw new \LogicException('Drupal core is out of date, but the recommended version could not be determined.');
}
$installable_releases = [];
if (Comparator::greaterThan($project['recommended'], $installed_version)) {
$release = ProjectRelease::createFromArray($project['releases'][$project['recommended']]);
$installable_releases[$release->getVersion()] = $release;
}
if (!empty($project['security updates'])) {
foreach ($project['security updates'] as $security_update) {
$release = ProjectRelease::createFromArray($security_update);
$version = $release->getVersion();
if (Comparator::greaterThan($version, $installed_version)) {
$installable_releases[$version] = $release;
}
}
}
$sorted_versions = Semver::rsort(array_keys($installable_releases));
return array_replace(array_flip($sorted_versions), $installable_releases);
}
$recommended_version = $project['recommended'];
return ProjectRelease::createFromArray($project['releases'][$recommended_version]);
/**
* Returns the installed project version, according to the Update module.
*
* @param bool $refresh
* (optional) Whether to fetch the latest information about available
* updates from drupal.org. This can be an expensive operation, so defaults
* to FALSE.
*
* @return string
* The installed project version as known to the Update module.
*/
public function getInstalledVersion(bool $refresh = FALSE): string {
$project_data = $this->getProjectInfo($refresh);
return $project_data['existing_version'];
}
}
<?php
namespace Drupal\automatic_updates;
use Composer\Semver\Semver;
use Drupal\automatic_updates\Validator\UpdateVersionValidator;
use Drupal\automatic_updates_9_3_shim\ProjectRelease;
use Drupal\Core\Extension\ExtensionVersion;
/**
* Defines a class to choose a release of Drupal core to update to.
*/
class ReleaseChooser {
use VersionParsingTrait;
/**
* The version validator service.
*
* @var \Drupal\automatic_updates\Validator\UpdateVersionValidator
*/
protected $versionValidator;
/**
* The project information fetcher.
*
* @var \Drupal\automatic_updates\ProjectInfo
*/
protected $projectInfo;
/**
* Constructs an ReleaseChooser object.
*
* @param \Drupal\automatic_updates\Validator\UpdateVersionValidator $version_validator
* The version validator.
*/
public function __construct(UpdateVersionValidator $version_validator) {
$this->versionValidator = $version_validator;
$this->projectInfo = new ProjectInfo();
}
/**
* Refreshes the project information through the Update module.
*
* @return $this
* The called object.
*/
public function refresh(): self {
$this->projectInfo->getProjectInfo(TRUE);
return $this;
}
/**
* Returns the releases that are installable.
*
* @return \Drupal\automatic_updates_9_3_shim\ProjectRelease[]
* The releases that are installable according to the version validator
* service.
*/
protected function getInstallableReleases(): array {
return array_filter(
$this->projectInfo->getInstallableReleases(),
[$this->versionValidator, 'isValidVersion'],
ARRAY_FILTER_USE_KEY
);
}
/**
* Gets the most recent release in the same minor as a specified version.
*
* @param string $version
* The full semantic version number, which must include a patch version.
*
* @return \Drupal\automatic_updates_9_3_shim\ProjectRelease|null
* The most recent release in the minor if available, otherwise NULL.
*
* @throws \InvalidArgumentException
* If the given semantic version number does not contain a patch version.
*/
protected function getMostRecentReleaseInMinor(string $version): ?ProjectRelease {
if (static::getPatchVersion($version) === NULL) {
throw new \InvalidArgumentException("The version number $version does not contain a patch version");
}
$releases = $this->getInstallableReleases();
foreach ($releases as $release) {
if (Semver::satisfies($release->getVersion(), "~$version")) {
return $release;
}
}
return NULL;
}
/**
* Gets the installed version of Drupal core.
*
* @return string
* The installed version of Drupal core.
*/
protected function getInstalledVersion(): string {
return $this->projectInfo->getInstalledVersion();
}
/**
* Gets the latest release in the currently installed minor.
*
* This will only return a release if it passes the ::isValidVersion() method
* of the version validator service injected into this class.
*
* @return \Drupal\automatic_updates_9_3_shim\ProjectRelease|null
* The latest release in the currently installed minor, if any, otherwise
* NULL.
*/
public function getLatestInInstalledMinor(): ?ProjectRelease {
return $this->getMostRecentReleaseInMinor($this->getInstalledVersion());
}
/**
* Gets the latest release in the next minor.
*
* This will only return a release if it passes the ::isValidVersion() method
* of the version validator service injected into this class.
*
* @return \Drupal\automatic_updates_9_3_shim\ProjectRelease|null
* The latest release in the next minor, if any, otherwise NULL.
*/
public function getLatestInNextMinor(): ?ProjectRelease {
$installed_version = ExtensionVersion::createFromVersionString($this->getInstalledVersion());
$next_minor = $installed_version->getMajorVersion() . '.' . (((int) $installed_version->getMinorVersion()) + 1) . '.0';
return $this->getMostRecentReleaseInMinor($next_minor);
}
}
......@@ -4,8 +4,8 @@ namespace Drupal\automatic_updates\Validation;
use Drupal\automatic_updates\CronUpdater;
use Drupal\automatic_updates\Event\ReadinessCheckEvent;
use Drupal\automatic_updates\ProjectInfo;
use Drupal\automatic_updates\Updater;
use Drupal\automatic_updates\UpdateRecommender;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface;
......@@ -102,20 +102,18 @@ class ReadinessValidationManager implements EventSubscriberInterface {
* @return $this
*/
public function run(): self {
$recommender = new UpdateRecommender();
$release = $recommender->getRecommendedRelease(TRUE);
// If updates will run during cron, use the cron updater service provided by
// this module. This will allow subscribers to ReadinessCheckEvent to run
// specific validation for conditions that only affect cron updates.
if ($this->config->get('automatic_updates.settings')->get('cron') == CronUpdater::DISABLED) {
if ($this->config->get('automatic_updates.settings')->get('cron') === CronUpdater::DISABLED) {
$stage = $this->updater;
}
else {
$stage = $this->cronUpdater;
}
$project_versions = $release ? ['drupal' => $release->getVersion()] : [];
$event = new ReadinessCheckEvent($stage, $project_versions);
$event = new ReadinessCheckEvent($stage);
// Version validators will need up-to-date project info.
(new ProjectInfo())->getProjectInfo(TRUE);
$this->eventDispatcher->dispatch($event);
$results = $event->getResults();
$this->keyValueExpirable->setWithExpire(
......
<?php
namespace Drupal\automatic_updates\Validator;
use Drupal\automatic_updates\CronUpdater;
use Drupal\automatic_updates\ProjectInfo;
use Drupal\automatic_updates\VersionParsingTrait;
use Drupal\Core\Extension\ExtensionVersion;
use Drupal\package_manager\Stage;
use Drupal\package_manager\ValidationResult;
/**
* Validates the target version of Drupal core before a cron update.
*
* @internal
* This class is an internal part of the module's cron update handling and
* should not be used by external code.
*/
final class CronUpdateVersionValidator extends UpdateVersionValidator {
use VersionParsingTrait;
/**
* {@inheritdoc}
*/
protected static function isStageSupported(Stage $stage): bool {
return $stage instanceof CronUpdater;
}
/**
* {@inheritdoc}
*/
public function getValidationResult(string $to_version_string): ?ValidationResult {
if ($result = parent::getValidationResult($to_version_string)) {
return $result;
}
$from_version_string = $this->getCoreVersion();
$to_version = ExtensionVersion::createFromVersionString($to_version_string);
$from_version = ExtensionVersion::createFromVersionString($from_version_string);
$variables = [
'@to_version' => $to_version_string,
'@from_version' => $from_version_string,
];
// @todo Return multiple validation messages and summary in
// https://www.drupal.org/project/automatic_updates/issues/3272068.
// Validate that both the from and to versions are stable releases.
if ($from_version->getVersionExtra()) {
return ValidationResult::createError([
$this->t('Drupal cannot be automatically updated during cron from its current version, @from_version, because Automatic Updates only supports updating from stable versions during cron.', $variables),
]);
}
if ($to_version->getVersionExtra()) {
// Because we do not support updating to a new minor version during
// cron it is probably impossible to update from a stable version to
// a unstable/pre-release version, but we should check this condition
// just in case.
return ValidationResult::createError([
$this->t('Drupal cannot be automatically updated during cron to the recommended version, @to_version, because Automatic Updates only supports updating to stable versions during cron.', $variables),
]);
}
if ($from_version->getMinorVersion() !== $to_version->getMinorVersion()) {
return ValidationResult::createError([
$this->t('Drupal cannot be automatically updated from its current version, @from_version, to the recommended version, @to_version, because automatic updates from one minor version to another are not supported during cron.', $variables),
]);
}
// Only updating to the next patch release is supported during cron.
$supported_patch_version = $from_version->getMajorVersion() . '.' . $from_version->getMinorVersion() . '.' . (((int) static::getPatchVersion($from_version_string)) + 1);
if ($to_version_string !== $supported_patch_version) {
return ValidationResult::createError([
$this->t('Drupal cannot be automatically updated during cron from its current version, @from_version, to the recommended version, @to_version, because Automatic Updates only supports 1 patch version update during cron.', $variables),
]);
}
// If both the from and to version numbers are valid check if the current
// settings only allow security updates during cron and if so ensure the
// update release is a security release.
$level = $this->configFactory->get('automatic_updates.settings')->get('cron');
if ($level === CronUpdater::SECURITY) {
$releases = (new ProjectInfo())->getInstallableReleases();
// @todo Remove this check and add validation to
// \Drupal\automatic_updates\Validator\UpdateVersionValidator::getValidationResult()
// to ensure the update release is always secure and supported in
// https://www.drupal.org/i/3271468.
if (!isset($releases[$to_version_string])) {
return ValidationResult::createError([
$this->t('Drupal cannot be automatically updated during cron from its current version, @from_version, to the recommended version, @to_version, because @to_version is not a valid release.', $variables),
]);
}
if (!$releases[$to_version_string]->isSecurityRelease()) {
return ValidationResult::createError([
$this->t('Drupal cannot be automatically updated during cron from its current version, @from_version, to the recommended version, @to_version, because @to_version is not a security release.', $variables),
]);
}
}
return NULL;
}
}
......@@ -2,20 +2,28 @@
namespace Drupal\automatic_updates\Validator;
use Composer\Semver\Semver;
use Composer\Semver\Comparator;
use Drupal\automatic_updates\CronUpdater;
use Drupal\automatic_updates\Event\ReadinessCheckEvent;
use Drupal\automatic_updates\ProjectInfo;
use Drupal\automatic_updates\Updater;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\package_manager\Event\PreCreateEvent;
use Drupal\package_manager\Event\PreOperationStageEvent;
use Drupal\Core\Extension\ExtensionVersion;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\package_manager\Event\PreCreateEvent;
use Drupal\package_manager\Event\PreOperationStageEvent;
use Drupal\package_manager\Event\StageEvent;
use Drupal\package_manager\Stage;
use Drupal\package_manager\ValidationResult;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Validates that core updates are within a supported version range.
*
* @internal
* This class is an internal part of the module's update handling and
* should not be used by external code.
*/
class UpdateVersionValidator implements EventSubscriberInterface {
......@@ -48,12 +56,7 @@ class UpdateVersionValidator implements EventSubscriberInterface {
* The running core version as known to the Update module.
*/
protected function getCoreVersion(): string {
// We need to call these functions separately, because
// update_get_available() will include the file that contains
// update_calculate_project_data().
$available_updates = update_get_available();
$available_updates = update_calculate_project_data($available_updates);
return $available_updates['drupal']['existing_version'];
return (new ProjectInfo())->getInstalledVersion();
}
/**
......@@ -63,122 +66,141 @@ class UpdateVersionValidator implements EventSubscriberInterface {
* The event object.
*/
public function checkUpdateVersion(PreOperationStageEvent $event): void {
$stage = $event->getStage();
// We only want to do this check if the stage belongs to Automatic Updates.
if (!$stage instanceof Updater) {
if (!static::isStageSupported($event->getStage())) {
return;
}
if ($to_version = $this->getUpdateVersion($event)) {
if ($result = $this->getValidationResult($to_version)) {
$event->addError($result->getMessages(), $result->getSummary());
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
PreCreateEvent::class => 'checkUpdateVersion',
ReadinessCheckEvent::class => 'checkUpdateVersion',
];
}
/**
* Gets the update version.
*
* @param \Drupal\package_manager\Event\StageEvent $event
* The event.
*
* @return string|null
* The version that the site will update to if any, otherwise NULL.
*/
protected function getUpdateVersion(StageEvent $event): ?string {
/** @var \Drupal\automatic_updates\Updater $updater */
$updater = $event->getStage();
if ($event instanceof ReadinessCheckEvent) {
$package_versions = $event->getPackageVersions();
// During readiness checks, we might not know the desired package
// versions, which means there's nothing to validate.
if (empty($package_versions)) {
return;
}
}
else {
// If the stage has begun its life cycle, we expect it knows the desired
// package versions.
$package_versions = $stage->getPackageVersions()['production'];
$package_versions = $updater->getPackageVersions()['production'];
}
if ($package_versions) {
// All the core packages will be updated to the same version, so it
// doesn't matter which specific package we're looking at.
$core_package_name = key($updater->getActiveComposer()->getCorePackages());
return $package_versions[$core_package_name];
}
else {
// During readiness checks we might not have a version to update to. Check
// if there are any possible updates and add a message about why we cannot
// update to that version.
// @todo Remove this code in https://www.drupal.org/i/3272326 when we add
// add a validator that will warn if cron updates will no longer work
// because the site is more than 1 patch release behind.
$project_info = new ProjectInfo();
if ($possible_releases = $project_info->getInstallableReleases()) {
$possible_release = array_pop($possible_releases);
return $possible_release->getVersion();
}
}
return NULL;
}
/**
* Determines if a version is valid.
*
* @param string $version
* The version string.
*
* @return bool
* TRUE if the version is valid (i.e., the site can update to it), otherwise
* FALSE.
*/
public function isValidVersion(string $version): bool {
return empty($this->getValidationResult($version));
}
/**
* Validates if an update to a specific version is allowed.
*
* @param string $to_version_string
* The version to update to.
*
* @return \Drupal\package_manager\ValidationResult|null
* NULL if the update is allowed, otherwise returns a validation result with
* the reason why the update is not allowed.
*/
protected function getValidationResult(string $to_version_string): ?ValidationResult {
$from_version_string = $this->getCoreVersion();
$from_version = ExtensionVersion::createFromVersionString($from_version_string);
// All the core packages will be updated to the same version, so it doesn't
// matter which specific package we're looking at.
$core_package_name = key($stage->getActiveComposer()->getCorePackages());
$to_version_string = $package_versions[$core_package_name];
$to_version = ExtensionVersion::createFromVersionString($to_version_string);
$variables = [
'@to_version' => $to_version_string,
'@from_version' => $from_version_string,
];
$from_version_extra = $from_version->getVersionExtra();
$to_version_extra = $to_version->getVersionExtra();
if (Semver::satisfies($to_version_string, "< $from_version_string")) {
$event->addError([
$this->t('Update version @to_version is lower than @from_version, downgrading is not supported.', $variables),
$from_version = ExtensionVersion::createFromVersionString($from_version_string);
// @todo Return multiple validation messages and summary in
// https://www.drupal.org/project/automatic_updates/issues/3272068.
if ($from_version->getVersionExtra() === 'dev') {
return ValidationResult::createError([
$this->t('Drupal cannot be automatically updated from its current version, @from_version, to the recommended version, @to_version, because automatic updates from a dev version to any other version are not supported.', $variables),
]);
}
elseif ($from_version_extra === 'dev') {
$event->addError([
$this->t('Drupal cannot be automatically updated from its current version, @from_version, to the recommended version, @to_version, because automatic updates from a dev version to any other version are not supported.', $variables),
if (Comparator::lessThan($to_version_string, $from_version_string)) {
return ValidationResult::createError([
$this->t('Update version @to_version is lower than @from_version, downgrading is not supported.', $variables),
]);
}
elseif ($from_version->getMajorVersion() !== $to_version->getMajorVersion()) {
$event->addError([
$to_version = ExtensionVersion::createFromVersionString($to_version_string);
if ($from_version->getMajorVersion() !== $to_version->getMajorVersion()) {
return ValidationResult::createError([
$this->t('Drupal cannot be automatically updated from its current version, @from_version, to the recommended version, @to_version, because automatic updates from one major version to another are not supported.', $variables),
]);
}
elseif ($from_version->getMinorVersion() !== $to_version->getMinorVersion()) {
if ($from_version->getMinorVersion() !== $to_version->getMinorVersion()) {
if (!$this->configFactory->get('automatic_updates.settings')->get('allow_core_minor_updates')) {
$event->addError([
return ValidationResult::createError([
$this->t('Drupal cannot be automatically updated from its current version, @from_version, to the recommended version, @to_version, because automatic updates from one minor version to another are not supported.', $variables),
]);
}
elseif ($stage instanceof CronUpdater) {
$event->addError([
$this->t('Drupal cannot be automatically updated from its current version, @from_version, to the recommended version, @to_version, because automatic updates from one minor version to another are not supported during cron.', $variables),
]);
}
}
elseif ($stage instanceof CronUpdater) {
if ($from_version_extra || $to_version_extra) {
if ($from_version_extra) {
$messages[] = $this->t('Drupal cannot be automatically updated during cron from its current version, @from_version, because Automatic Updates only supports updating from stable versions during cron.', $variables);
$event->addError($messages);
}
if ($to_version_extra) {
// Because we do not support updating to a new minor version during
// cron it is probably impossible to update from a stable version to
// a unstable/pre-release version, but we should check this condition
// just in case.
$messages[] = $this->t('Drupal cannot be automatically updated during cron to the recommended version, @to_version, because Automatic Updates only supports updating to stable versions during cron.', $variables);
$event->addError($messages);
}
}
else {
$to_patch_version = (int) $this->getPatchVersion($to_version_string);
$from_patch_version = (int) $this->getPatchVersion($from_version_string);
if ($from_patch_version + 1 !== $to_patch_version) {
$messages[] = $this->t('Drupal cannot be automatically updated during cron from its current version, @from_version, to the recommended version, @to_version, because Automatic Updates only supports 1 patch version update during cron.', $variables);
$event->addError($messages);
}
}
}
return NULL;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
PreCreateEvent::class => 'checkUpdateVersion',
ReadinessCheckEvent::class => 'checkUpdateVersion',
];
}
/**
* Gets the patch number for a version string.
* Determines if a stage is supported by this validator.
*
* @todo Move this method to \Drupal\Core\Extension\ExtensionVersion in
* https://www.drupal.org/i/3261744.
* @param \Drupal\package_manager\Stage $stage
* The stage to check.
*
* @param string $version_string
* The version string.
*
* @return string
* The patch number.
* @return bool
* TRUE if the stage is supported by this validator, otherwise FALSE.
*/
private function getPatchVersion(string $version_string): string {
$version_extra = ExtensionVersion::createFromVersionString($version_string)
->getVersionExtra();
if ($version_extra) {
$version_string = str_replace("-$version_extra", '', $version_string);
}
$version_parts = explode('.', $version_string);
return $version_parts[2];
protected static function isStageSupported(Stage $stage): bool {
// We only want to do this check if the stage belongs to Automatic Updates,
// and it is not a cron update.
// @see \Drupal\automatic_updates\Validator\CronUpdateVersionValidator
return $stage instanceof Updater && !$stage instanceof CronUpdater;
}
}
<?php
namespace Drupal\automatic_updates;
use Drupal\Core\Extension\ExtensionVersion;
/**
* Common function for parsing version traits.
*
* @internal
* This trait may be removed in patch or minor versions.
*/
trait VersionParsingTrait {
/**
* Gets the patch number from a version string.
*
* @todo Move this method to \Drupal\Core\Extension\ExtensionVersion in
* https://www.drupal.org/i/3261744.
*
* @param string $version_string
* The version string.
*
* @return string|null
* The patch number if available, otherwise NULL.
*/
protected static function getPatchVersion(string $version_string): ?string {
$version_extra = ExtensionVersion::createFromVersionString($version_string)
->getVersionExtra();
if ($version_extra) {
$version_string = str_replace("-$version_extra", '', $version_string);
}
$version_parts = explode('.', $version_string);
return count($version_parts) === 3 ? $version_parts[2] : NULL;
}
}
<?xml version="1.0" encoding="utf-8"?>
<project xmlns:dc="http://purl.org/dc/elements/1.1/">
<title>Drupal</title>
<short_name>drupal</short_name>
<dc:creator>Drupal</dc:creator>
<supported_branches>9.7.,9.8.</supported_branches>
<project_status>published</project_status>
<link>http://example.com/project/drupal</link>
<terms>
<term><name>Projects</name><value>Drupal project</value></term>
</terms>
<releases>
<release>
<name>Drupal 9.8.2</name>
<version>9.8.2</version>
<status>published</status>
<release_link>http://example.com/drupal-9-8-2-release</release_link>
<download_link>http://example.com/drupal-9-8-2.tar.gz</download_link>
<date>1250425521</date>
<terms>
<term><name>Release type</name><value>New features</value></term>
<term><name>Release type</name><value>Bug fixes</value></term>
</terms>
</release>
<release>
<name>Drupal 9.8.1</name>
<version>9.8.1</version>
<status>published</status>
<release_link>http://example.com/drupal-9-8-1-release</release_link>
<download_link>http://example.com/drupal-9-8-1.tar.gz</download_link>
<date>1250425521</date>
<terms>
<term><name>Release type</name><value>New features</value></term>
<term><name>Release type</name><value>Bug fixes</value></term>
<term><name>Release type</name><value>Security update</value></term>
</terms>
</release>
<release>
<name>Drupal 9.8.0</name>
<version>9.8.0</version>
<status>published</status>
<release_link>http://example.com/drupal-9-8-0-release</release_link>
<download_link>http://example.com/drupal-9-8-0.tar.gz</download_link>
<date>1250424521</date>
<terms>
<term><name>Release type</name><value>New features</value></term>
<term><name>Release type</name><value>Bug fixes</value></term>
<term><name>Release type</name><value>Insecure</value></term>
</terms>
</release>
<release>
<name>Drupal 9.8.0-alpha1</name>
<version>9.8.0-alpha1</version>
<status>published</status>
<release_link>http://example.com/drupal-9-8-0-alpha1-release</release_link>
<download_link>http://example.com/drupal-9-8-0-alpha1.tar.gz</download_link>
<date>1250424521</date>
<terms>
<term><name>Release type</name><value>New features</value></term>
<term><name>Release type</name><value>Bug fixes</value></term>
</terms>
</release>
<release>
<name>Drupal 9.7.1</name>
<version>9.7.1</version>
<status>published</status>
<release_link>http://example.com/drupal-9-7-1-release</release_link>
<download_link>http://example.com/drupal-9-7-1.tar.gz</download_link>
<date>1250425521</date>
<terms>
<term><name>Release type</name><value>New features</value></term>
<term><name>Release type</name><value>Bug fixes</value></term>
<term><name>Release type</name><value>Security update</value></term>
</terms>
</release>
<release>
<name>Drupal 9.7.0</name>
<version>9.7.0</version>
<status>published</status>
<release_link>http://example.com/drupal-9-7-0-release</release_link>
<download_link>http://example.com/drupal-9-7-0.tar.gz</download_link>
<date>1250424521</date>
<terms>
<term><name>Release type</name><value>New features</value></term>
<term><name>Release type</name><value>Bug fixes</value></term>
<term><name>Release type</name><value>Insecure</value></term>
</terms>
</release>
<release>
<name>Drupal 9.7.0-alpha1</name>
<version>9.7.0-alpha1</version>
<status>published</status>
<release_link>http://example.com/drupal-9-7-0-alpha1-release</release_link>
<download_link>http://example.com/drupal-9-7-0-alpha1.tar.gz</download_link>
<date>1250424521</date>
<terms>
<term><name>Release type</name><value>New features</value></term>
<term><name>Release type</name><value>Bug fixes</value></term>
</terms>
</release>
</releases>
</project>
......@@ -3,7 +3,7 @@
<title>Drupal</title>
<short_name>drupal</short_name>
<dc:creator>Drupal</dc:creator>
<supported_branches>9.8.</supported_branches>
<supported_branches>9.7.,9.8.</supported_branches>
<project_status>published</project_status>
<link>http://example.com/project/drupal</link>
<terms>
......@@ -58,5 +58,41 @@
<term><name>Release type</name><value>Bug fixes</value></term>
</terms>
</release>
<release>
<name>Drupal 9.7.1</name>
<version>9.7.1</version>
<status>published</status>
<release_link>http://example.com/drupal-9-7-1-release</release_link>
<download_link>http://example.com/drupal-9-7-1.tar.gz</download_link>
<date>1250425521</date>
<terms>
<term><name>Release type</name><value>New features</value></term>
<term><name>Release type</name><value>Bug fixes</value></term>
</terms>
</release>
<release>
<name>Drupal 9.7.0</name>
<version>9.7.0</version>
<status>published</status>
<release_link>http://example.com/drupal-9-7-0-release</release_link>
<download_link>http://example.com/drupal-9-7-0.tar.gz</download_link>
<date>1250424521</date>
<terms>
<term><name>Release type</name><value>New features</value></term>
<term><name>Release type</name><value>Bug fixes</value></term>
</terms>
</release>
<release>
<name>Drupal 9.7.0-alpha1</name>
<version>9.7.0-alpha1</version>
<status>published</status>
<release_link>http://example.com/drupal-9-7-0-alpha1-release</release_link>
<download_link>http://example.com/drupal-9-7-0-alpha1.tar.gz</download_link>
<date>1250424521</date>
<terms>
<term><name>Release type</name><value>New features</value></term>
<term><name>Release type</name><value>Bug fixes</value></term>
</terms>
</release>
</releases>
</project>
......@@ -54,7 +54,7 @@ class ReadinessValidationTest extends AutomaticUpdatesFunctionalTestBase {
*/
protected function setUp(): void {
parent::setUp();
$this->setReleaseMetadata(__DIR__ . '/../../fixtures/release-history/drupal.9.8.2.xml');
$this->setReleaseMetadata(__DIR__ . '/../../fixtures/release-history/drupal.9.8.1-security.xml');
$this->setCoreVersion('9.8.1');
$this->reportViewerUser = $this->createUser([
......
......@@ -56,7 +56,7 @@ class UpdateLockTest extends AutomaticUpdatesFunctionalTestBase {
$this->checkForMetaRefresh();
$this->assertUpdateReady('9.8.1');
$assert_session->buttonExists('Continue');
$url = parse_url($this->getSession()->getCurrentUrl(), PHP_URL_PATH);
$url = $this->getSession()->getCurrentUrl();
// Another user cannot show up and try to start an update, since the other
// user already started one.
......
......@@ -110,7 +110,7 @@ class CronUpdaterTest extends AutomaticUpdatesKernelTestBase {
'enabled, normal release' => [
CronUpdater::ALL,
"$fixture_dir/drupal.9.8.2.xml",
TRUE,
FALSE,
],
'enabled, security release' => [
CronUpdater::ALL,
......
......@@ -30,6 +30,7 @@ class ReadinessValidationManagerTest extends AutomaticUpdatesKernelTestBase {
*/
protected function setUp(): void {
parent::setUp();
$this->setCoreVersion('9.8.2');
$this->installEntitySchema('user');
$this->installSchema('user', ['users_data']);
$this->createTestValidationResults();
......@@ -222,6 +223,7 @@ class ReadinessValidationManagerTest extends AutomaticUpdatesKernelTestBase {
->install(['automatic_updates']);
// Ensure there's a simulated core release to update to.
$this->setCoreVersion('9.8.1');
$this->setReleaseMetadata(__DIR__ . '/../../../fixtures/release-history/drupal.9.8.2.xml');
// The readiness checker should raise a warning, so that the update is not
......@@ -246,7 +248,7 @@ class ReadinessValidationManagerTest extends AutomaticUpdatesKernelTestBase {
/** @var \Drupal\automatic_updates\Updater $updater */
$updater = $this->container->get('automatic_updates.updater');
$updater->begin(['drupal' => '9.8.1']);
$updater->begin(['drupal' => '9.8.2']);
$updater->stage();
$updater->apply();
$updater->destroy();
......
......@@ -3,11 +3,8 @@
namespace Drupal\Tests\automatic_updates\Kernel\ReadinessValidation;
use Drupal\automatic_updates\CronUpdater;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\package_manager\Exception\StageValidationException;
use Drupal\package_manager\ValidationResult;
use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase;
use Drupal\Tests\automatic_updates\Kernel\TestCronUpdater;
use Drupal\Tests\package_manager\Traits\PackageManagerBypassTestTrait;
use Psr\Log\Test\TestLogger;
......@@ -167,11 +164,6 @@ class UpdateVersionValidatorTest extends AutomaticUpdatesKernelTestBase {
// the update shouldn't have been started.
elseif ($expected_results) {
$this->assertUpdateStagedTimes(0);
// An exception exactly like this one should have been thrown by
// CronUpdater::dispatch(), and subsequently caught, formatted as HTML,
// and logged.
$this->assertErrorsWereLogged($expected_results);
}
// If cron updates are enabled and no validation errors were expected, the
// update should have started and nothing should have been logged.
......@@ -197,8 +189,6 @@ class UpdateVersionValidatorTest extends AutomaticUpdatesKernelTestBase {
CronUpdater::DISABLED,
[],
],
// The latest release is two patch releases ahead, so the update should be
// blocked even though the cron configuration allows it.
'security only' => [
CronUpdater::SECURITY,
[$update_disallowed],
......@@ -230,15 +220,6 @@ class UpdateVersionValidatorTest extends AutomaticUpdatesKernelTestBase {
$this->assertCheckerResultsFromManager($expected_results, TRUE);
$this->container->get('cron')->run();
$this->assertUpdateStagedTimes(0);
// If cron updates are enabled for all patch releases, the error should have
// been raised and logged.
if ($cron_setting === CronUpdater::ALL) {
$this->assertErrorsWereLogged($expected_results);
}
else {
$this->assertArrayNotHasKey(RfcLogLevel::ERROR, $this->logger->recordsByLevel);
}
}
/**
......@@ -280,7 +261,13 @@ class UpdateVersionValidatorTest extends AutomaticUpdatesKernelTestBase {
$this->config('automatic_updates.settings')
->set('cron', $cron_setting)
->save();
$this->assertCheckerResultsFromManager([], TRUE);
if ($cron_setting === CronUpdater::SECURITY) {
$expected_result = ValidationResult::createError(['Drupal cannot be automatically updated during cron from its current version, 9.8.1, to the recommended version, 9.8.2, because 9.8.2 is not a security release.']);
$this->assertCheckerResultsFromManager([$expected_result], TRUE);
}
else {
$this->assertCheckerResultsFromManager([], TRUE);
}
$this->container->get('cron')->run();
$this->assertUpdateStagedTimes((int) $will_update);
}
......@@ -298,9 +285,6 @@ class UpdateVersionValidatorTest extends AutomaticUpdatesKernelTestBase {
$dev_current_version = ValidationResult::createError([
'Drupal cannot be automatically updated from its current version, 9.8.0-dev, to the recommended version, 9.8.2, because automatic updates from a dev version to any other version are not supported.',
]);
$newer_current_version = ValidationResult::createError([
'Update version 9.8.2 is lower than 9.8.3, downgrading is not supported.',
]);
$different_major_version = ValidationResult::createError([
'Drupal cannot be automatically updated from its current version, 8.9.1, to the recommended version, 9.8.2, because automatic updates from one major version to another are not supported.',
]);
......@@ -313,81 +297,46 @@ class UpdateVersionValidatorTest extends AutomaticUpdatesKernelTestBase {
// the validation will be run with the regular updater, not the cron
// updater.
[],
[],
],
'unstable current version, security updates allowed' => [
CronUpdater::SECURITY,
'9.8.0-alpha1',
[$unstable_current_version],
// The update will not run because the latest release is not a security
// release, so nothing should be logged.
[],
],
'unstable current version, all updates allowed' => [
CronUpdater::ALL,
'9.8.0-alpha1',
[$unstable_current_version],
[$unstable_current_version],
],
'dev current version, cron disabled' => [
CronUpdater::DISABLED,
'9.8.0-dev',
[$dev_current_version],
[],
],
'dev current version, security updates allowed' => [
CronUpdater::SECURITY,
'9.8.0-dev',
[$dev_current_version],
// The update will not run because the latest release is not a security
// release, so nothing should be logged.
[],
],
'dev current version, all updates allowed' => [
CronUpdater::ALL,
'9.8.0-dev',
[$dev_current_version],
[$dev_current_version],
],
'newer current version, cron disabled' => [
CronUpdater::DISABLED,
'9.8.3',
[$newer_current_version],
[],
],
'newer current version, security updates allowed' => [
CronUpdater::SECURITY,
'9.8.3',
[$newer_current_version],
// The update will not run because the latest release is not a security
// release, so nothing should be logged.
[],
],
'newer current version, all updates allowed' => [
CronUpdater::ALL,
'9.8.3',
[$newer_current_version],
[$newer_current_version],
],
'different current major, cron disabled' => [
CronUpdater::DISABLED,
'8.9.1',
[$different_major_version],
[],
],
'different current major, security updates allowed' => [
CronUpdater::SECURITY,
'8.9.1',
[$different_major_version],
// The update will not run because the latest release is not a security
// release, so nothing should be logged.
[],
],
'different current major, all updates allowed' => [
CronUpdater::ALL,
'8.9.1',
[$different_major_version],
[$different_major_version],
],
];
}
......@@ -402,12 +351,10 @@ class UpdateVersionValidatorTest extends AutomaticUpdatesKernelTestBase {
* @param \Drupal\package_manager\ValidationResult[] $expected_results
* The validation results, if any, that should be flagged during readiness
* checks.
* @param \Drupal\package_manager\ValidationResult[] $logged_results
* The validation results, if any, that should be logged when cron is run.
*
* @dataProvider providerInvalidCronUpdate
*/
public function testInvalidCronUpdate(string $cron_setting, string $current_core_version, array $expected_results, array $logged_results): void {
public function testInvalidCronUpdate(string $cron_setting, string $current_core_version, array $expected_results): void {
$this->setCoreVersion($current_core_version);
$this->config('automatic_updates.settings')
->set('cron', $cron_setting)
......@@ -423,23 +370,6 @@ class UpdateVersionValidatorTest extends AutomaticUpdatesKernelTestBase {
// created (in which case, we expect the errors to be logged).
$this->container->get('cron')->run();
$this->assertUpdateStagedTimes(0);
if ($logged_results) {
$this->assertErrorsWereLogged($logged_results);
}
}
/**
* Asserts that validation errors were logged during a cron update.
*
* @param \Drupal\package_manager\ValidationResult[] $results
* The validation errors should have been logged.
*/
private function assertErrorsWereLogged(array $results): void {
$exception = new StageValidationException($results, 'Unable to complete the update because of errors.');
// The exception will be formatted in a specific, predictable way.
// @see \Drupal\Tests\automatic_updates\Kernel\CronUpdaterTest::testErrors()
$message = TestCronUpdater::formatValidationException($exception);
$this->assertTrue($this->logger->hasRecord($message, RfcLogLevel::ERROR));
}
}
<?php
namespace Drupal\Tests\automatic_updates\Kernel;
use Drupal\automatic_updates_9_3_shim\ProjectRelease;
/**
* @coversDefaultClass \Drupal\automatic_updates\ReleaseChooser
*
* @group automatic_updates
*/
class ReleaseChooserTest extends AutomaticUpdatesKernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['automatic_updates'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setReleaseMetadata(__DIR__ . '/../../fixtures/release-history/drupal.9.8.2-older-sec-release.xml');
}
/**
* Data provider for testReleases().
*
* @return array[]
* The test cases.
*/
public function providerReleases(): array {
return [
'installed 9.8.0, no minor support' => [
'chooser' => 'automatic_updates.release_chooser',
'minor_support' => FALSE,
'installed_version' => '9.8.0',
'current_minor' => '9.8.2',
'next_minor' => NULL,
],
'installed 9.8.0, minor support' => [
'chooser' => 'automatic_updates.release_chooser',
'minor_support' => TRUE,
'installed_version' => '9.8.0',
'current_minor' => '9.8.2',
'next_minor' => NULL,
],
'installed 9.7.0, no minor support' => [
'chooser' => 'automatic_updates.release_chooser',
'minor_support' => FALSE,
'installed_version' => '9.7.0',
'current_minor' => '9.7.1',
'next_minor' => NULL,
],
'installed 9.7.0, minor support' => [
'chooser' => 'automatic_updates.release_chooser',
'minor_support' => TRUE,
'installed_version' => '9.7.0',
'current_minor' => '9.7.1',
'next_minor' => '9.8.2',
],
'installed 9.7.2, no minor support' => [
'chooser' => 'automatic_updates.release_chooser',
'minor_support' => FALSE,
'installed_version' => '9.7.2',
'current_minor' => NULL,
'next_minor' => NULL,
],
'installed 9.7.2, minor support' => [
'chooser' => 'automatic_updates.release_chooser',
'minor_support' => TRUE,
'installed_version' => '9.7.2',
'current_minor' => NULL,
'next_minor' => '9.8.2',
],
'cron, installed 9.8.0, no minor support' => [
'chooser' => 'automatic_updates.cron_release_chooser',
'minor_support' => FALSE,
'installed_version' => '9.8.0',
'current_minor' => '9.8.1',
'next_minor' => NULL,
],
'cron, installed 9.8.0, minor support' => [
'chooser' => 'automatic_updates.cron_release_chooser',
'minor_support' => TRUE,
'installed_version' => '9.8.0',
'current_minor' => '9.8.1',
'next_minor' => NULL,
],
'cron, installed 9.7.0, no minor support' => [
'chooser' => 'automatic_updates.cron_release_chooser',
'minor_support' => FALSE,
'installed_version' => '9.7.0',
'current_minor' => '9.7.1',
'next_minor' => NULL,
],
'cron, installed 9.7.0, minor support' => [
'chooser' => 'automatic_updates.cron_release_chooser',
'minor_support' => TRUE,
'installed_version' => '9.7.0',
'current_minor' => '9.7.1',
'next_minor' => NULL,
],
'cron, installed 9.7.2, no minor support' => [
'chooser' => 'automatic_updates.cron_release_chooser',
'minor_support' => FALSE,
'installed_version' => '9.7.2',
'current_minor' => NULL,
'next_minor' => NULL,
],
'cron, installed 9.7.2, minor support' => [
'chooser' => 'automatic_updates.cron_release_chooser',
'minor_support' => TRUE,
'installed_version' => '9.7.2',
'current_minor' => NULL,
'next_minor' => NULL,
],
];
}
/**
* Tests fetching the recommended release when an update is available.
*
* @param string $chooser_service
* The ID of release chooser service to use.
* @param bool $minor_support
* Whether updates to the next minor will be allowed.
* @param string $installed_version
* The installed version of Drupal core.
* @param string|null $current_minor
* The expected release in the currently installed minor or NULL if none is
* available.
* @param string|null $next_minor
* The expected release in the next minor or NULL if none is available.
*
* @dataProvider providerReleases
*
* @covers ::getLatestInInstalledMinor
* @covers ::getLatestInNextMinor
*/
public function testReleases(string $chooser_service, bool $minor_support, string $installed_version, ?string $current_minor, ?string $next_minor): void {
$this->setCoreVersion($installed_version);
$this->config('automatic_updates.settings')->set('allow_core_minor_updates', $minor_support)->save();
/** @var \Drupal\automatic_updates\ReleaseChooser $chooser */
$chooser = $this->container->get($chooser_service);
$chooser->refresh();
$this->assertReleaseVersion($current_minor, $chooser->getLatestInInstalledMinor());
$this->assertReleaseVersion($next_minor, $chooser->getLatestInNextMinor());
}
/**
* Asserts that a project release matches a version number.
*
* @param string|null $version
* The version to check, or NULL if no version expected.
* @param \Drupal\automatic_updates_9_3_shim\ProjectRelease|null $release
* The release to check, or NULL if no release is expected.
*/
private function assertReleaseVersion(?string $version, ?ProjectRelease $release) {
if (is_null($version)) {
$this->assertNull($release);
}
else {
$this->assertNotEmpty($release);
$this->assertSame($version, $release->getVersion());
}
}
}
<?php
namespace Drupal\Tests\automatic_updates\Kernel;
use Drupal\automatic_updates\UpdateRecommender;
/**
* @covers \Drupal\automatic_updates\UpdateRecommender
*
* @group automatic_updates
*/
class UpdateRecommenderTest extends AutomaticUpdatesKernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'automatic_updates',
'package_manager',
];
/**
* Tests fetching the recommended release when an update is available.
*/
public function testUpdateAvailable(): void {
$recommender = new UpdateRecommender();
$recommended_release = $recommender->getRecommendedRelease(TRUE);
$this->assertNotEmpty($recommended_release);
$this->assertSame('9.8.2', $recommended_release->getVersion());
// Getting the recommended release again should not trigger another request.
$this->assertNotEmpty($recommender->getRecommendedRelease());
}
/**
* Tests fetching the recommended release when there is no update available.
*/
public function testNoUpdateAvailable(): void {
$this->setCoreVersion('9.8.2');
$recommender = new UpdateRecommender();
$recommended_release = $recommender->getRecommendedRelease(TRUE);
$this->assertNull($recommended_release);
// Getting the recommended release again should not trigger another request.
$this->assertNull($recommender->getRecommendedRelease());
}
}
<?php
namespace Drupal\Tests\automatic_updates\Unit;
use Drupal\automatic_updates\ProjectInfo;
use Drupal\automatic_updates_9_3_shim\ProjectRelease;
use Drupal\Tests\UnitTestCase;
use Drupal\update\UpdateManagerInterface;
/**
* @coversDefaultClass \Drupal\automatic_updates\ProjectInfo
*
* @group automatic_updates
*/
class ProjectInfoTest extends UnitTestCase {
/**
* Creates release data for testing.
*
* @return string[][]
* The release information.
*/
private static function createTestReleases(): array {
$versions = ['8.2.5', '8.2.4', '8.2.3', '8.2.3-alpha'];
foreach ($versions as $version) {
$release_arrays[$version] = [
'status' => 'published',
'version' => $version,
'release_link' => "https://example.drupal.org/project/drupal/releases/$version",
];
}
return $release_arrays;
}
/**
* Data provider for testGetInstallableReleases().
*
* @return array[][]
* The test cases.
*/
public function providerGetInstallableReleases(): array {
$release_arrays = static::createTestReleases();
foreach ($release_arrays as $version => $release_array) {
$release_objects[$version] = ProjectRelease::createFromArray($release_array);
}
return [
'current' => [
[
'status' => UpdateManagerInterface::CURRENT,
'existing_version' => '1.2.3',
],
[],
],
'1 release' => [
[
'status' => UpdateManagerInterface::NOT_CURRENT,
'existing_version' => '8.2.4',
'recommended' => '8.2.5',
'releases' => [
'8.2.5' => $release_arrays['8.2.5'],
],
],
[
'8.2.5' => $release_objects['8.2.5'],
],
],
'1 releases, also security' => [
[
'status' => UpdateManagerInterface::NOT_CURRENT,
'existing_version' => '8.2.4',
'recommended' => '8.2.5',
'releases' => [
'8.2.5' => $release_arrays['8.2.5'],
],
'security updates' => [
$release_arrays['8.2.5'],
],
],
[
'8.2.5' => $release_objects['8.2.5'],
],
],
'1 release, other security' => [
[
'status' => UpdateManagerInterface::NOT_CURRENT,
'existing_version' => '8.2.2',
'recommended' => '8.2.5',
'releases' => [
'8.2.5' => $release_arrays['8.2.5'],
],
'security updates' => [
// Set out of order security releases to ensure results are sorted.
$release_arrays['8.2.3-alpha'],
$release_arrays['8.2.3'],
$release_arrays['8.2.4'],
],
],
[
'8.2.5' => $release_objects['8.2.5'],
'8.2.4' => $release_objects['8.2.4'],
'8.2.3' => $release_objects['8.2.3'],
'8.2.3-alpha' => $release_objects['8.2.3-alpha'],
],
],
'1 releases, other security lower than current version' => [
[
'status' => UpdateManagerInterface::NOT_CURRENT,
'existing_version' => '8.2.3',
'recommended' => '8.2.5',
'releases' => [
'8.2.5' => $release_arrays['8.2.5'],
],
'security updates' => [
// Set out of order security releases to ensure results are sorted.
$release_arrays['8.2.3-alpha'],
$release_arrays['8.2.3'],
$release_arrays['8.2.4'],
],
],
[
'8.2.5' => $release_objects['8.2.5'],
'8.2.4' => $release_objects['8.2.4'],
],
],
];
}
/**
* @covers ::getInstallableReleases
*
* @param array $project_data
* The project data to return from ::getProjectInfo().
* @param \Drupal\automatic_updates_9_3_shim\ProjectRelease[] $expected_releases
* The expected releases.
*
* @dataProvider providerGetInstallableReleases
*/
public function testGetInstallableReleases(array $project_data, array $expected_releases): void {
$project_info = $this->getMockedProjectInfo($project_data);
$this->assertEqualsCanonicalizing($expected_releases, $project_info->getInstallableReleases());
}
/**
* @covers ::getInstallableReleases
*/
public function testInvalidProjectData(): void {
$release_arrays = static::createTestReleases();
$project_data = [
'status' => UpdateManagerInterface::NOT_CURRENT,
'existing_version' => '1.2.3',
'releases' => [
'8.2.5' => $release_arrays['8.2.5'],
],
'security updates' => [
$release_arrays['8.2.4'],
$release_arrays['8.2.3'],
$release_arrays['8.2.3-alpha'],
],
];
$project_info = $this->getMockedProjectInfo($project_data);
$this->expectException('LogicException');
$this->expectExceptionMessage('Drupal core is out of date, but the recommended version could not be determined.');
$project_info->getInstallableReleases();
}
/**
* @covers ::getInstalledVersion
*/
public function testGetInstalledVersion(): void {
$project_info = $this->getMockedProjectInfo(['existing_version' => '1.2.3']);
$this->assertSame('1.2.3', $project_info->getInstalledVersion());
}
/**
* Mocks a ProjectInfo object.
*
* @param array $project_data
* The project info that should be returned by the mock's ::getProjectInfo()
* method.
*
* @return \Drupal\automatic_updates\ProjectInfo
* The mocked object.
*/
private function getMockedProjectInfo(array $project_data): ProjectInfo {
$project_info = $this->getMockBuilder(ProjectInfo::class)
->onlyMethods(['getProjectInfo'])
->getMock();
$project_info->expects($this->any())
->method('getProjectInfo')
->willReturn($project_data);
return $project_info;
}
}
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