Verified Commit df228407 authored by godotislate's avatar godotislate
Browse files

refactor: #3125013 Refactor update.fetch.inc into a MailHandler

By: dww
By: znerol
By: berdir
By: nicxvan
By: godotislate
By: catch
By: quietone
(cherry picked from commit 4081563a)
parent 76ce09fa
Loading
Loading
Loading
Loading
Loading
+116 −0
Changes for core/modules/update/src/Hook/UpdateCronHooks.php: 116 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\update\Hook;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\Requirement\RequirementSeverity;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\State\StateInterface;
use Drupal\update\MailHandler;
use Drupal\update\UpdateManagerInterface;
use Drupal\update\UpdateProcessorInterface;
use Symfony\Component\DependencyInjection\Attribute\AutowireServiceClosure;

/**
 * Hook implementations for update.
 */
class UpdateCronHooks {

  public function __construct(
    protected readonly ConfigFactoryInterface $configFactory,
    protected readonly ModuleHandlerInterface $moduleHandler,
    protected readonly StateInterface $state,
    protected readonly TimeInterface $time,
    /**
     * @var \Closure(): \Drupal\update\MailHandler
     */
    #[AutowireServiceClosure(MailHandler::class)]
    protected readonly \Closure $mailHandler,
    /**
     * @var \Closure(): \Drupal\update\UpdateManagerInterface
     */
    #[AutowireServiceClosure(UpdateManagerInterface::class)]
    protected readonly \Closure $updateManager,
    /**
     * @var \Closure(): \Drupal\update\UpdateProcessorInterface
     */
    #[AutowireServiceClosure(UpdateProcessorInterface::class)]
    protected readonly \Closure $updateProcessor,
  ) {
  }

  /**
   * Implements hook_cron().
   */
  #[Hook('cron')]
  public function cron(): void {
    $update_config = $this->configFactory->get('update.settings');
    $frequency = $update_config->get('check.interval_days');
    $interval = 60 * 60 * 24 * $frequency;
    $last_check = $this->state->get('update.last_check', 0);
    $request_time = $this->time->getRequestTime();
    if ($request_time - $last_check > $interval) {
      // If the configured update interval has elapsed, we want to invalidate
      // the data for all projects, attempt to re-fetch, and trigger any
      // configured notifications about the new status.
      ($this->updateManager)()->refreshUpdateData();
      ($this->updateProcessor)()->fetchData();
    }
    else {
      // Otherwise, see if any individual projects are now stale or still
      // missing data, and if so, try to fetch the data.
      update_get_available(TRUE);
    }
    $last_email_notice = $this->state->get('update.last_email_notification', 0);
    if ($request_time - $last_email_notice > $interval) {
      // If configured time between notifications elapsed, send email about
      // updates possibly available.
      if ($this->notify()) {
        // Track when the last mail was successfully sent to avoid sending
        // too many emails.
        $this->state->set('update.last_email_notification', $request_time);
      }
    }
  }

  /**
   * Performs any notifications that should be done once cron fetches new data.
   *
   * This method checks the status of the site using the new data and, depending
   * on the configuration of the site, notifies administrators via email if
   * there are new releases or missing security updates.
   *
   * @return bool
   *   TRUE if any notifications were sent, otherwise FALSE.
   *
   * @see Drupal\update\Hook\UpdateRequirements::runtime()
   */
  protected function notify(): bool {
    $update_config = $this->configFactory->get('update.settings');
    $notify_all = ($update_config->get('notification.threshold') == 'all');
    $recipients = $update_config->get('notification.emails');
    if (empty($recipients)) {
      return FALSE;
    }

    $status = $this->moduleHandler->invoke('update', 'runtime_requirements');
    $items = [];
    foreach (['core', 'contrib'] as $report_type) {
      $type = 'update_' . $report_type;
      if (isset($status[$type]['severity'])
          && ($status[$type]['severity'] == RequirementSeverity::Error || ($notify_all && $status[$type]['reason'] == UpdateManagerInterface::NOT_CURRENT))) {
        $items[$report_type] = $status[$type]['reason'];
      }
    }
    if (empty($items)) {
      return FALSE;
    }

    return ($this->mailHandler)()->sendUpdateNotifications($recipients, $items);
  }

}
+1 −32
Changes for core/modules/update/src/Hook/UpdateHooks.php: 1 added line, 32 removed lines.
Original line number Diff line number Diff line
@@ -108,37 +108,6 @@ public function pageTop(): void {
    }
  }

  /**
   * Implements hook_cron().
   */
  #[Hook('cron')]
  public function cron(): void {
    $update_config = \Drupal::config('update.settings');
    $frequency = $update_config->get('check.interval_days');
    $interval = 60 * 60 * 24 * $frequency;
    $last_check = \Drupal::state()->get('update.last_check', 0);
    $request_time = \Drupal::time()->getRequestTime();
    if ($request_time - $last_check > $interval) {
      // If the configured update interval has elapsed, we want to invalidate
      // the data for all projects, attempt to re-fetch, and trigger any
      // configured notifications about the new status.
      update_refresh();
      update_fetch_data();
    }
    else {
      // Otherwise, see if any individual projects are now stale or still
      // missing data, and if so, try to fetch the data.
      update_get_available(TRUE);
    }
    $last_email_notice = \Drupal::state()->get('update.last_email_notification', 0);
    if ($request_time - $last_email_notice > $interval) {
      // If configured time between notifications elapsed, send email about
      // updates possibly available.
      \Drupal::moduleHandler()->loadInclude('update', 'inc', 'update.fetch');
      _update_cron_notify();
    }
  }

  /**
   * Implements hook_themes_installed().
   *
@@ -193,7 +162,7 @@ public function modulesUninstalled($modules): void {
   * Constructs the email notification message when the site is out of date.
   *
   * @see \Drupal\Core\Mail\MailManagerInterface::mail()
   * @see _update_cron_notify()
   * @see \Drupal\update\Hook\UpdateCronHooks::notify()
   * @see _update_message_text()
   * @see \Drupal\update\UpdateManagerInterface
   */
+4 −5
Changes for core/modules/update/src/Hook/UpdateRequirements.php: 4 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -39,12 +39,11 @@ public function __construct(
   * attribute, which is an integer constant to indicate why the given status
   * is being returned (UPDATE_NOT_SECURE, UPDATE_NOT_CURRENT, or
   * UPDATE_UNKNOWN). This is used for generating the appropriate email
   * notification messages during update_cron(), and might be useful for other
   * modules that invoke update_runtime_requirements() to find out if the site
   * is up to date or not.
   * notification messages during cron runs, and might be useful for other
   * modules to find out if the site is up to date or not.
   *
   * @see _update_message_text()
   * @see _update_cron_notify()
   * @see \Drupal\update\Hook\UpdateCronHooks::notify()
   * @see \Drupal\update\UpdateManagerInterface
   */
  #[Hook('runtime_requirements')]
@@ -100,7 +99,7 @@ public function runtime(): array {
   *   An array to be included in the nested $requirements array.
   *
   * @see hook_requirements()
   * @see update_requirements()
   * @see Drupal\update\Hook\UpdateRequirements::runtime()
   * @see update_calculate_project_data()
   */
  protected function requirementCheck($project, $type): array {
+67 −0
Changes for core/modules/update/src/MailHandler.php: 67 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\update;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\Attribute\AutowireServiceClosure;

/**
 * A service to handle assembly and dispatch of Update Status mail messages.
 *
 * @internal
 */
class MailHandler {

  public function __construct(
    protected readonly EntityTypeManagerInterface $entityTypeManager,
    protected readonly LanguageManagerInterface $languageManager,
    protected readonly TimeInterface $time,
    /**
     * @var \Closure(): \Drupal\Core\Mail\MailManagerInterface
     */
    #[AutowireServiceClosure('plugin.manager.mail')]
    protected readonly \Closure $mailManager,
  ) {
  }

  /**
   * Sends update notification mails to a list of recipients.
   *
   * @param string[] $recipients
   *   A list of recipient mail addresses.
   * @param array{'core'?: int, 'contrib'?: int} $items
   *   The update status entries for core and contrib projects. Allowed values
   *   are defined as constants on UpdateManagerInterface.
   *
   * @return bool
   *   TRUE if any email notifications were sent, otherwise FALSE.
   *
   * @see \Drupal\update\UpdateManagerInterface
   */
  public function sendUpdateNotifications(array $recipients, array $items): bool {
    $results = [];
    $default_langcode = $this->languageManager->getDefaultLanguage()->getId();
    $storage = $this->entityTypeManager->getStorage('user');
    $accounts_by_email = [];
    foreach ($storage->loadByProperties(['mail' => $recipients]) as $account) {
      assert($account instanceof AccountInterface);
      $accounts_by_email[$account->getEmail()] = $account;
    }
    foreach ($recipients as $recipient) {
      // If the recipient is a registered user with a language preference, use
      // the recipient's preferred language. Otherwise, use the system default
      // language.
      $recipient_account = $accounts_by_email[$recipient] ?? NULL;
      $langcode = $recipient_account ? $recipient_account->getPreferredLangcode() : $default_langcode;
      $message = ($this->mailManager)()->mail('update', 'status_notify', $recipient, $langcode, $items);
      $results[] = $message['result'] ?? NULL;
    }
    return count(array_filter($results)) > 0;
  }

}
+1 −1
Changes for core/modules/update/src/ProjectSecurityRequirement.php: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -9,7 +9,7 @@
/**
 * Class for generating a project's security requirement.
 *
 * @see update_requirements()
 * @see Drupal\update\Hook\UpdateRequirements::runtime()
 *
 * @internal
 *   This class implements logic to determine security coverage for Drupal core
Loading