Skip to content
Snippets Groups Projects
automatic_updates.module 13.5 KiB
Newer Older
 * Contains hook implementations for Automatic Updates.
use Drupal\automatic_updates\CronUpdater;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\automatic_updates\Validation\AdminStatusCheckMessages;
use Drupal\system\Controller\DbUpdateController;
use Drupal\package_manager\Validator\ComposerExecutableValidator;
/**
 * Implements hook_help().
 */
function automatic_updates_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
      $output = '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('Automatic Updates lets you update Drupal core.') . '</p>';
      $output .= '<p>';
      $output .= t('Automatic Updates will keep Drupal secure and up-to-date by automatically installing new patch-level updates, if available, when cron runs. It also provides a user interface to check if any updates are available and install them. You can <a href=":configure-form">configure Automatic Updates</a> to install all patch-level updates, only security updates, or no updates at all, during cron. By default, only security updates are installed during cron; this requires that you <a href=":update-form">install non-security updates through the user interface</a>.', [
        ':configure-form' => Url::fromRoute('update.settings')->toString(),
        ':update-form' => Url::fromRoute('update.report_update')->toString(),
Ted Bowman's avatar
Ted Bowman committed
      $output .= '<p>' . t('Additionally, Automatic Updates periodically runs checks to ensure that updates can be installed, and will warn site administrators if problems are detected.') . '</p>';
      $output .= '<h3>' . t('Requirements') . '</h3>';
      $output .= '<p>' . t('Automatic Updates requires a Composer executable whose version satisfies <code>@version</code>, and PHP must have permission to run it. The path to the executable may be set in the <code>package_manager.settings:executables.composer</code> config setting, or it will be automatically detected.', ['@version' => ComposerExecutableValidator::MINIMUM_COMPOSER_VERSION_CONSTRAINT]) . '</p>';
      $output .= '<p id="cron-alternate-port">' . t('If your site is running on the built-in PHP web server, unattended (i.e., cron) updates may not work without one of the following workarounds:') . '</p>';
      $output .= '<ul>';
      $output .= '<li>' . t('Use a multithreaded web server, such as Apache, NGINX, or on Windows, IIS.') . '</li>';
      $output .= '<li>' . t('Run another instance of the built-in PHP web server on a different port and configure automatic updates accordingly: <code>$config["automatic_updates.settings"]["cron_port"] = $alternate_port_number;</code>') . '</li>';
      $output .= '</ul>';
      $output .= '<p>' . t('For more information, see the <a href=":automatic-updates-documentation">online documentation for the Automatic Updates module</a>.', [':automatic-updates-documentation' => 'https://www.drupal.org/docs/8/update/automatic-updates']) . '</p>';
      $output .= '<h3 id="minor-update">' . t('Updating to another minor version of Drupal') . '</h3>';
      $output .= '<p>';
      $output .= t('Automatic Updates supports updating from one minor version of Drupal core to another; for example, from Drupal 9.4.8 to Drupal 9.5.0. This is only allowed when updating via <a href=":url">the user interface</a>. Unattended background updates can only update <em>within</em> the currently installed minor version (for example, Drupal 9.4.6 to 9.4.8).', [
        ':url' => Url::fromRoute('update.report_update')->toString(),
      ]);
      $output .= '</p>';
      $output .= '<p>' . t('This is because updating from one minor version of Drupal to another is riskier than staying within the current minor version. New minor versions of Drupal introduce changes that can, in some situations, be incompatible with installed modules and themes.') . '</p>';
      $output .= '<p>' . t('Therefore, if you want to use Automatic Updates to update to another minor version of Drupal, it is strongly recommended to do a test update first, ideally on an isolated development copy of your site, before updating your production site.') . '</p>';
/**
 * Implements hook_mail().
 */
function automatic_updates_mail(string $key, array &$message, array $params): void {
  // Explicitly pass the language code to all translated strings.
  $options = [
    'langcode' => $message['langcode'],
  ];
  if ($key === 'cron_successful') {
    $message['subject'] = t("Drupal core was successfully updated", [], $options);
    $message['body'][] = t('Congratulations!', [], $options);
    $message['body'][] = t('Drupal core was automatically updated from @previous_version to @updated_version.', [
      '@previous_version' => $params['previous_version'],
      '@updated_version' => $params['updated_version'],
    ], $options);
  }
  elseif (str_starts_with($key, 'cron_failed')) {
    $message['subject'] = t("Drupal core update failed", [], $options);
    // If this is considered urgent, prefix the subject line with a call to
    // action.
    if ($params['urgent']) {
      $message['subject'] = t('URGENT: @subject', [
        '@subject' => $message['subject'],
    }

    $message['body'][] = t('Drupal core failed to update automatically from @previous_version to @target_version. The following error was logged:', [
      '@previous_version' => $params['previous_version'],
      '@target_version' => $params['target_version'],
    ], $options);
    $message['body'][] = $params['error_message'];

    // If the problem was not due to a failed apply, provide a link for the site
    // owner to do the update.
    if ($key !== 'cron_failed_apply') {
      $url = Url::fromRoute('update.report_update')
        ->setAbsolute()
        ->toString();

      if ($key === 'cron_failed_insecure') {
        $message['body'][] = t('Your site is running an insecure version of Drupal and should be updated as soon as possible. Visit @url to perform the update.', ['@url' => $url], $options);
      }
      else {
        $message['body'][] = t('No immediate action is needed, but it is recommended that you visit @url to perform the update, or at least check that everything still looks good.', ['@url' => $url], $options);
      }
    }
  elseif ($key === 'status_check_failed') {
    $message['subject'] = t('Automatic updates readiness checks failed', [], $options);

    $url = Url::fromRoute('system.status')
      ->setAbsolute()
      ->toString();
    $message['body'][] = t('Your site has failed some readiness checks for automatic updates and may not be able to receive automatic updates until further action is taken. Please visit @url for more information.', [
      '@url' => $url,
    ], $options);
  }

  // If this email was related to an unattended update, explicitly state that
  // this isn't supported yet.
  if (str_starts_with($key, 'cron_')) {
    $message['body'][] = t('This e-mail was sent by the Automatic Updates module. Unattended updates are not yet fully supported.', [], $options);
    $message['body'][] = t('If you are using this feature in production, it is strongly recommended for you to visit your site and ensure that everything still looks good.', [], $options);
function automatic_updates_page_top() {
  /** @var \Drupal\automatic_updates\Validation\AdminStatusCheckMessages $status_check_messages */
  $status_check_messages = \Drupal::classResolver(AdminStatusCheckMessages::class);
  $status_check_messages->displayAdminPageMessages();

  // @todo Rely on the route option after https://www.drupal.org/i/3236497 is
  //   committed.
  // @todo Remove 'system.batch_page.html' after
  //   https://www.drupal.org/i/3238311 is committed.
  $skip_routes = [
    'system.batch_page.html',
    'automatic_updates.confirmation_page',
    'automatic_updates.report_update',
    'automatic_updates.module_update',
  ];
  // @see auto_updates_module_implements_alter()
  $route_name = \Drupal::routeMatch()->getRouteName();
  if (!in_array($route_name, $skip_routes, TRUE) && function_exists('update_page_top')) {
    update_page_top();
  }
}

/**
 * Implements hook_module_implements_alter().
 *
 * @todo Remove after https://www.drupal.org/i/3236497 is committed.
 */
function automatic_updates_module_implements_alter(&$implementations, $hook) {
  if ($hook === 'page_top') {
    // Remove hook_page_top() implementation from the Update module. This '
    // implementation displays error messages about security releases. We call
    // this implementation in our own automatic_updates_page_top() except on our
    // own routes to avoid these messages while an update is in progress.
  if ($hook === 'cron') {
    $hook = $implementations['automatic_updates'];
    unset($implementations['automatic_updates']);
    $implementations['automatic_updates'] = $hook;
  }
}

/**
 * Implements hook_cron().
 */
function automatic_updates_cron() {
  /** @var \Drupal\automatic_updates\CronUpdater $updater */
  $updater = \Drupal::service('automatic_updates.cron_updater');
  $updater->handleCron();
  /** @var \Drupal\automatic_updates\Validation\StatusChecker $status_checker */
  $status_checker = \Drupal::service('automatic_updates.status_checker');
  $last_results = $status_checker->getResults();
  $last_run_time = $status_checker->getLastRunTime();
  // Do not run status checks more than once an hour unless there are no results
  // available.
  if ($last_results === NULL || !$last_run_time || \Drupal::time()->getRequestTime() - $last_run_time > 3600) {
  // Only try to send failure notifications if unattended updates are enabled.
  if ($updater->getMode() !== CronUpdater::DISABLED) {
    \Drupal::service('automatic_updates.status_check_mailer')
      ->sendFailureNotifications($last_results, $status_checker->getResults());
  }
 * Implements hook_modules_installed().
function automatic_updates_modules_installed() {
  // Run the status checkers if needed when any modules are installed in
  // case they provide status checkers.
  /** @var \Drupal\automatic_updates\Validation\StatusChecker $status_checker */
  $status_checker = \Drupal::service('automatic_updates.status_checker');
  $status_checker->run();
 * Implements hook_modules_uninstalled().
function automatic_updates_modules_uninstalled() {
  // Run the status checkers if needed when any modules are uninstalled in
  // case they provided status checkers.
  /** @var \Drupal\automatic_updates\Validation\StatusChecker $status_checker */
  $status_checker = \Drupal::service('automatic_updates.status_checker');
  $status_checker->run();
/**
 * Implements hook_batch_alter().
 *
 * @todo Remove this in https://www.drupal.org/i/3267817.
 */
function automatic_updates_batch_alter(array &$batch): void {
  foreach ($batch['sets'] as &$batch_set) {
    if (!empty($batch_set['finished']) && $batch_set['finished'] === [DbUpdateController::class, 'batchFinished']) {
      $batch_set['finished'] = [BatchProcessor::class, 'dbUpdateBatchFinished'];
    }
  }
}

/**
 * Implements hook_preprocess_update_project_status().
 */
function automatic_updates_preprocess_update_project_status(array &$variables) {
  $project = &$variables['project'];
  if ($project['name'] !== 'drupal') {
    return;
  }
  $updater = \Drupal::service('automatic_updates.updater');
  /** @var \Drupal\automatic_updates\ReleaseChooser $recommender */
  $recommender = \Drupal::service('automatic_updates.release_chooser');
  try {
    if ($installed_minor_release = $recommender->getLatestInInstalledMinor($updater)) {
      $supported_target_versions[] = $installed_minor_release->getVersion();
    }
    if ($next_minor_release = $recommender->getLatestInNextMinor($updater)) {
      $supported_target_versions[] = $next_minor_release->getVersion();
    }
  }
  catch (RuntimeException $exception) {
    // If for some reason we are not able to get the update recommendations
    // do not alter the report.
    watchdog_exception('automatic_updates', $exception);
    return;
  }
  $variables['#attached']['library'][] = 'automatic_updates/update_status';

  $status = &$variables['status'];
  if ($supported_target_versions && $status['label']) {
    $status['label'] = [
      '#markup' => t(
        '@label <a href=":update-form">Update now</a>', [
          '@label' => $status['label'],
          ':update-form' => Url::fromRoute('update.report_update')->toString(),
        ]),
    ];
  }
  // BEGIN: DELETE FROM CORE MERGE REQUEST
  if (empty($variables['versions'])) {
    return;
  }
  foreach ($variables['versions'] as &$themed_version) {
    $version_info = &$themed_version['#version'];
    if ($supported_target_versions && in_array($version_info['version'], $supported_target_versions, TRUE)) {
      $version_info['download_link'] = Url::fromRoute('update.report_update')->setAbsolute()->toString();
    }
    else {
      // If this version will not be displayed as an option on this module's
      // update form replace the link to download the archive file with the
      // release notes link. The release notes page will provide Composer
      // instructions. While this isn't a perfect solution the Update module twig
      // templates do not check if 'download_link' is set, so we cannot unset it
      // here.
      $themed_version['#attributes']['class'][] = 'automatic-updates-unsupported-version';
      $version_info['download_link'] = $version_info['release_link'];
    }
  }
  // END: DELETE FROM CORE MERGE REQUEST
}