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
Loading
Loading
Loading
Loading
+3 −4
Original line number Diff line number Diff line
@@ -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.
+16 −0
Original line number Diff line number Diff line
@@ -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:
+38 −33
Original line number Diff line number Diff line
@@ -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);
    $next_release = $this->releaseChooser->refresh()->getLatestInInstalledMinor();
    if ($next_release) {
      $this->performUpdate($next_release->getVersion());
    }
    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;
  }

    $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.
   *
+32 −15
Original line number Diff line number Diff line
@@ -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
+106 −0
Original line number Diff line number Diff line
@@ -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'];
  }

}
Loading