<?php /** * @file * Contains hook implementations for Automatic Updates. */ use Drupal\automatic_updates\BatchProcessor; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\automatic_updates\Validation\AdminReadinessMessages; use Drupal\Core\Url; 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) { case 'help.page.automatic_updates': $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(), ]); $output .= '</p>'; $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>' . 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 .= '<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>'; return $output; } } /** * 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'], ]; switch ($key) { case '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); break; case 'cron_failed': $message['subject'] = t("Drupal core update failed", [], $options); $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']; break; } // 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); } } /** * Implements hook_page_top(). */ function automatic_updates_page_top() { /** @var \Drupal\automatic_updates\Validation\AdminReadinessMessages $readiness_messages */ $readiness_messages = \Drupal::classResolver(AdminReadinessMessages::class); $readiness_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. unset($implementations['update']); } if ($hook === 'cron') { // Whatever mofo. $hook = $implementations['automatic_updates']; unset($implementations['automatic_updates']); $implementations['automatic_updates'] = $hook; } } /** * Implements hook_cron(). */ function automatic_updates_cron() { \Drupal::service('automatic_updates.cron_updater')->handleCron(); /** @var \Drupal\automatic_updates\Validation\ReadinessValidationManager $checker_manager */ $checker_manager = \Drupal::service('automatic_updates.readiness_validation_manager'); $last_results = $checker_manager->getResults(); $last_run_time = $checker_manager->getLastRunTime(); // Do not run readiness 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) { $checker_manager->run(); } } /** * Implements hook_modules_installed(). */ function automatic_updates_modules_installed() { // Run the readiness checkers if needed when any modules are installed in // case they provide readiness checker services. /** @var \Drupal\automatic_updates\Validation\ReadinessValidationManager $checker_manager */ $checker_manager = \Drupal::service('automatic_updates.readiness_validation_manager'); $checker_manager->run(); } /** * Implements hook_modules_uninstalled(). */ function automatic_updates_modules_uninstalled() { // Run the readiness checkers if needed when any modules are uninstalled in // case they provided readiness checker services. /** @var \Drupal\automatic_updates\Validation\ReadinessValidationManager $checker_manager */ $checker_manager = \Drupal::service('automatic_updates.readiness_validation_manager'); $checker_manager->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'); $supported_target_versions = []; /** @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 }