diff --git a/core/modules/auto_updates/auto_updates.info.yml b/core/modules/auto_updates/auto_updates.info.yml index 1914ec34e7..6e24fb72ea 100644 --- a/core/modules/auto_updates/auto_updates.info.yml +++ b/core/modules/auto_updates/auto_updates.info.yml @@ -1,7 +1,6 @@ name: 'Automatic Updates' type: module -description: 'Experimental module to develop automatic updates. Currently the module does not provide update functionality.' -configure: auto_updates.settings +description: 'Experimental module to develop automatic updates. Currently the module provides checks for update readiness but does not yet provide update functionality.' package: Core (Experimental) version: VERSION hidden: true diff --git a/core/modules/auto_updates/auto_updates.install b/core/modules/auto_updates/auto_updates.install index 003ddfd995..eabd5969f0 100644 --- a/core/modules/auto_updates/auto_updates.install +++ b/core/modules/auto_updates/auto_updates.install @@ -18,34 +18,34 @@ function auto_updates_requirements($phase) { /** @var \Drupal\auto_updates\ReadinessChecker\ReadinessCheckerManager $checker_manager */ $checker_manager = \Drupal::service('auto_updates.readiness_checker_manager'); - $requirements['auto_updates_readiness']['title'] = t('Update readiness checks'); + $requirement['title'] = t('Update readiness checks'); $readiness_check_url = Url::fromRoute('auto_updates.update_readiness'); $last_check_timestamp = $checker_manager->getMostRecentRunTime(); if ($last_check_timestamp === NULL) { - $requirements['auto_updates_readiness']['severity'] = REQUIREMENT_WARNING; + $requirement['severity'] = REQUIREMENT_WARNING; // @todo Link "automatic updates" to documentation in // https://www.drupal.org/node/3168405. - $requirements['auto_updates_readiness']['value'] = t('Your site has never checked if it is ready to apply automatic updates.'); + $requirement['value'] = t('Your site has never checked if it is ready to apply automatic updates.'); if ($readiness_check_url->access()) { - $requirements['auto_updates_readiness']['description'] = t('Run readiness checks manually.', [ + $requirement['description'] = t('Run readiness checks now.', [ ':link' => $readiness_check_url->toString(), ]); } } elseif (!$checker_manager->hasRunRecently()) { - $requirements['auto_updates_readiness']['severity'] = REQUIREMENT_WARNING; + $requirement['severity'] = REQUIREMENT_WARNING; $time_ago = \Drupal::service('date.formatter')->formatTimeDiffSince($last_check_timestamp); // @todo Link "automatic updates" to documentation in // https://www.drupal.org/node/3168405. - $requirements['auto_updates_readiness']['value'] = t('Your site has not recently checked if it is ready to apply automatic updates.'); + $requirement['value'] = t('Your site has not recently checked if it is ready to apply automatic updates.'); if ($readiness_check_url->access()) { - $requirements['auto_updates_readiness']['description'] = t('Readiness checks were last run @time ago. Run readiness checks now.', [ + $requirement['description'] = t('Readiness checks were last run @time ago. Run readiness checks now.', [ '@time' => $time_ago, ':url' => $readiness_check_url->toString(), ]); } else { - $requirements['auto_updates_readiness']['description'] = t('Readiness checks were last run @time ago.', ['@time' => $time_ago]); + $requirement['description'] = t('Readiness checks were last run @time ago.', ['@time' => $time_ago]); } } else { @@ -53,15 +53,15 @@ function auto_updates_requirements($phase) { $warning_results = $checker_manager->getWarnings(); $checker_results = array_merge($error_results, $warning_results); if (!empty($checker_results)) { - $requirements['auto_updates_readiness']['severity'] = $error_results ? REQUIREMENT_ERROR : REQUIREMENT_WARNING; - $requirements['auto_updates_readiness']['value'] = new PluralTranslatableMarkup(count($checker_results), '@count check failed:', '@count checks failed:'); - $requirements['auto_updates_readiness']['description'] = [ + $requirement['severity'] = $error_results ? REQUIREMENT_ERROR : REQUIREMENT_WARNING; + $requirement['value'] = new PluralTranslatableMarkup(count($checker_results), '@count check failed:', '@count checks failed:'); + $requirement['description'] = [ '#theme' => 'item_list', '#items' => $checker_results, ]; } else { - $requirements['auto_updates_readiness'] += [ + $requirement += [ 'severity' => REQUIREMENT_OK, // @todo Link "automatic updates" to documentation in // https://www.drupal.org/node/3168405. @@ -69,5 +69,5 @@ function auto_updates_requirements($phase) { ]; } } - return $requirements; + return ['auto_updates_readiness' => $requirement]; } diff --git a/core/modules/auto_updates/auto_updates.module b/core/modules/auto_updates/auto_updates.module index 099592bcd5..712026d703 100644 --- a/core/modules/auto_updates/auto_updates.module +++ b/core/modules/auto_updates/auto_updates.module @@ -13,8 +13,7 @@ function auto_updates_page_top(array &$page_top) { /** @var \Drupal\Core\Routing\AdminContext $admin_context */ $admin_context = \Drupal::service('router.admin_context'); - $route_match = \Drupal::routeMatch(); - if ($admin_context->isAdminRoute($route_match->getRouteObject()) && \Drupal::currentUser()->hasPermission('administer site configuration')) { + if ($admin_context->isAdminRoute() && \Drupal::currentUser()->hasPermission('administer site configuration')) { $disabled_routes = [ 'update.theme_update', 'system.theme_install', @@ -28,15 +27,16 @@ function auto_updates_page_top(array &$page_top) { 'update.confirmation_page', ]; // These routes don't need additional nagging. - if (in_array($route_match->getRouteName(), $disabled_routes, TRUE)) { + if (in_array(\Drupal::routeMatch()->getRouteName(), $disabled_routes, TRUE)) { return; } /** @var \Drupal\auto_updates\ReadinessChecker\ReadinessCheckerManager $checker_manager */ $checker_manager = \Drupal::service('auto_updates.readiness_checker_manager'); if (!$checker_manager->hasRunRecently()) { - $readiness_settings = Url::fromRoute('auto_updates.settings'); - \Drupal::messenger()->addError(t('Your site has not recently run an update readiness check. Administer automatic updates and run readiness checks manually.', [ - ':url' => $readiness_settings->toString(), + /** @var \Drupal\Core\Path\CurrentPathStack $current_path */ + $current_path = \Drupal::service('path.current'); + \Drupal::messenger()->addError(t('Your site has not recently run an update readiness check. Run readiness checks now.', [ + ':url' => Url::fromRoute('auto_updates.update_readiness')->setOption('query', ['destination' => $current_path->getPath()])->toString(), ])); } $results = $checker_manager->getErrors(); @@ -64,7 +64,6 @@ function auto_updates_page_top(array &$page_top) { * Implements hook_cron(). */ function auto_updates_cron() { - $request_time = \Drupal::time()->getRequestTime(); /** @var \Drupal\auto_updates\ReadinessChecker\ReadinessCheckerManager $checker_manager */ $checker_manager = \Drupal::service('auto_updates.readiness_checker_manager'); $checker_manager->getErrors(); diff --git a/core/modules/auto_updates/auto_updates.routing.yml b/core/modules/auto_updates/auto_updates.routing.yml index b9ec3d6efc..7ddecdd4df 100644 --- a/core/modules/auto_updates/auto_updates.routing.yml +++ b/core/modules/auto_updates/auto_updates.routing.yml @@ -1,12 +1,3 @@ -auto_updates.status: - path: '/admin/reports/auto_updates' - defaults: - _controller: '\Drupal\auto_updates\Controller\ReadinessCheckerController::status' - _title: 'Update readiness status' - requirements: - _permission: 'administer software updates' - options: - _admin_route: TRUE auto_updates.update_readiness: path: '/admin/reports/auto_updates/readiness' defaults: @@ -14,5 +5,3 @@ auto_updates.update_readiness: _title: 'Update readiness checking' requirements: _permission: 'administer software updates' - options: - _admin_route: TRUE diff --git a/core/modules/auto_updates/auto_updates.services.yml b/core/modules/auto_updates/auto_updates.services.yml index a13ff965f1..9555c77d71 100644 --- a/core/modules/auto_updates/auto_updates.services.yml +++ b/core/modules/auto_updates/auto_updates.services.yml @@ -3,7 +3,7 @@ services: class: Drupal\auto_updates\ReadinessChecker\DiskSpace arguments: ['%app.root%'] tags: - - { name: readiness_checker} + - { name: readiness_checker } auto_updates.readiness_checker_manager: class: Drupal\auto_updates\ReadinessChecker\ReadinessCheckerManager arguments: ['@keyvalue.expirable', '@config.factory', '@datetime.time'] diff --git a/core/modules/auto_updates/src/Controller/ReadinessCheckerController.php b/core/modules/auto_updates/src/Controller/ReadinessCheckerController.php index cf133e6d17..bb0b5d7f8c 100644 --- a/core/modules/auto_updates/src/Controller/ReadinessCheckerController.php +++ b/core/modules/auto_updates/src/Controller/ReadinessCheckerController.php @@ -4,14 +4,11 @@ use Drupal\auto_updates\ReadinessChecker\ReadinessCheckerManager; use Drupal\Core\Controller\ControllerBase; -use Drupal\Core\Datetime\DateFormatter; use Drupal\Core\StringTranslation\TranslationInterface; -use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\HttpFoundation\RedirectResponse; /** - * A controller for running Readiness Checkers. + * A controller for running readiness checkers. * * @internal * Controller classes are internal. @@ -25,13 +22,6 @@ class ReadinessCheckerController extends ControllerBase { */ protected $checkerManager; - /** - * The date formatter. - * - * @var \Drupal\Core\Datetime\DateFormatterInterface - */ - protected $dateFormatter; - /** * ReadinessCheckerController constructor. * @@ -39,13 +29,10 @@ class ReadinessCheckerController extends ControllerBase { * The readiness checker manager. * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation * The string translation service. - * @param \Drupal\Core\Datetime\DateFormatter $date_formatter - * The date formatter service. */ - public function __construct(ReadinessCheckerManager $checker_manager, TranslationInterface $string_translation, DateFormatter $date_formatter) { + public function __construct(ReadinessCheckerManager $checker_manager, TranslationInterface $string_translation) { $this->checkerManager = $checker_manager; $this->setStringTranslation($string_translation); - $this->dateFormatter = $date_formatter; } /** @@ -54,62 +41,25 @@ public function __construct(ReadinessCheckerManager $checker_manager, Translatio public static function create(ContainerInterface $container) { return new static( $container->get('auto_updates.readiness_checker_manager'), - $container->get('string_translation'), - $container->get('date.formatter') + $container->get('string_translation') ); } - /** - * Displays a status report for automatic update readiness. - * - * @return mixed[] - * The status report render array. - */ - public function status() { - $last_check_timestamp = $this->checkerManager->getMostRecentRunTime(); - - $readiness_messages = $last_check_timestamp === NULL ? - $this->t('Readiness checks have never been run.') - : $this->t('Readiness checks were last run @time ago.', ['@time' => $this->dateFormatter->formatTimeDiffSince($last_check_timestamp)]); - $return['last_run']['#markup'] = $readiness_messages . ' ' . - $this->t('Manually run the readiness checks.', - [ - ':url' => Url::fromRoute('auto_updates.update_readiness')->toString(), - ] - ); - $error_results = $this->checkerManager->getErrors(); - $warning_results = $this->checkerManager->getWarnings(); - $checker_results = array_merge($error_results, $warning_results); - if ($checker_results) { - $return['status']['#markup'] = $this->formatPlural(count($checker_results), '@count check failed:', '@count checks failed:'); - $return['results'] = [ - '#theme' => 'item_list', - '#items' => $checker_results, - ]; - } - else { - $return['status'] = [ - '#type' => 'container', - '#markup' => $this->t('Your site is ready for automatic updates.'), - ]; - } - - return $return; - } - /** * Run the readiness checkers. * * @return \Symfony\Component\HttpFoundation\RedirectResponse * A redirect to the automatic updates settings page. */ - public function run(): RedirectResponse { - if (!array_filter($this->checkerManager->getErrors(TRUE))) { + public function run() { + if (!$this->checkerManager->getErrors(TRUE) && !$this->checkerManager->getWarnings()) { // @todo Link "automatic updates" to documentation in // https://www.drupal.org/node/3168405. + // If there are no messages from the readiness checkers display a message + // that site is ready. If there are messages the page will display them. $this->messenger()->addStatus($this->t('No issues found. Your site is ready for automatic updates')); } - return $this->redirect('auto_updates.status'); + return $this->redirect('system.status'); } } diff --git a/core/modules/auto_updates/src/ReadinessChecker/DiskSpace.php b/core/modules/auto_updates/src/ReadinessChecker/DiskSpace.php index 3606f3667b..3b9d54731c 100644 --- a/core/modules/auto_updates/src/ReadinessChecker/DiskSpace.php +++ b/core/modules/auto_updates/src/ReadinessChecker/DiskSpace.php @@ -15,7 +15,20 @@ class DiskSpace extends FileSystemBase { */ protected const MINIMUM_DISK_SPACE = 1073741824; - protected const MEGABYTE_DIVISOR = 1000000; + /** + * Gets the free disk space. + * + * @param string $path + * The path to check. + * + * @throws \RuntimeException + * Thrown if the call to disk_free_space() fails. + */ + protected static function getFreeSpace(string $path) { + if (!disk_free_space($path)) { + throw new \RuntimeException('disk_free_space() failed.'); + } + } /** * {@inheritdoc} @@ -28,32 +41,40 @@ public function getWarnings(): array { * {@inheritdoc} */ public function getErrors(): array { - if ($messages = parent::getErrors()) { - // @todo Reimplemenet so that every class that extends FileSystem base - // will not the same error. - return $messages; + $has_valid_root = $this->hasValidRootPath(); + $has_valid_vendor = $this->hasValidVendorPatch(); + if (!$has_valid_root && !$has_valid_vendor) { + return [$this->t('Free disk space cannot be determined because the web root and vendor directories could not be located.')]; + } + elseif (!$has_valid_root) { + return [$this->t('Free disk space cannot be determined because the web root directory could not be located.')]; + } + if (!$this->hasValidVendorPatch()) { + return [$this->t('Free disk space cannot be determined because the vendor directory could not be located.')]; } $messages = []; - $minimum_megabytes = static::MINIMUM_DISK_SPACE / static::MEGABYTE_DIVISOR; - if (!$this->areSameLogicalDisk($this->getRootPath(), $this->getVendorPath())) { + $minimum_megabytes = static::MINIMUM_DISK_SPACE / 1000000; + $root_path = $this->getRootPath(); + $vendor_path = $this->getVendorPath(); + if (!$this->areSameLogicalDisk($root_path, $vendor_path)) { // If the root and vendor paths are not on the same logical disk check // that each have at least half of the minimum required disk space. - if (disk_free_space($this->getRootPath()) < (static::MINIMUM_DISK_SPACE / 2)) { + if (static::getFreeSpace($root_path) < (static::MINIMUM_DISK_SPACE / 2)) { $messages[] = $this->t('Drupal root filesystem "@root" has insufficient space. There must be at least @space megabytes free.', [ - '@root' => $this->getRootPath(), - '@space' => $minimum_megabytes, + '@root' => $root_path, + '@space' => $minimum_megabytes / 2, ]); } - if (disk_free_space($this->getVendorPath()) < (static::MINIMUM_DISK_SPACE / 2)) { + if (static::getFreeSpace($vendor_path) < (static::MINIMUM_DISK_SPACE / 2)) { $messages[] = $this->t('Vendor filesystem "@vendor" has insufficient space. There must be at least @space megabytes free.', [ - '@vendor' => $this->getVendorPath(), - '@space' => $minimum_megabytes, + '@vendor' => $vendor_path, + '@space' => $minimum_megabytes / 2, ]); } } - elseif (disk_free_space($this->getRootPath()) < static::MINIMUM_DISK_SPACE) { + elseif (static::getFreeSpace($root_path) < static::MINIMUM_DISK_SPACE) { $messages[] = $this->t('Logical disk "@root" has insufficient space. There must be at least @space megabytes free.', [ - '@root' => $this->getRootPath(), + '@root' => $root_path, '@space' => $minimum_megabytes, ]); } diff --git a/core/modules/auto_updates/src/ReadinessChecker/FileSystemBase.php b/core/modules/auto_updates/src/ReadinessChecker/FileSystemBase.php index 529b2b31a8..8e58430035 100644 --- a/core/modules/auto_updates/src/ReadinessChecker/FileSystemBase.php +++ b/core/modules/auto_updates/src/ReadinessChecker/FileSystemBase.php @@ -6,6 +6,9 @@ /** * Base class for file system checkers. + * + * Readiness checkers that require knowing the web root and/or vendor + * directories to perform their checks should extend this class. */ abstract class FileSystemBase implements ReadinessCheckerInterface { use StringTranslationTrait; @@ -28,23 +31,27 @@ public function __construct(string $app_root) { } /** - * {@inheritdoc} + * Determines if a valid root path can be located. + * + * @return bool + * TRUE if a valid root path can be determined, otherwise false. */ - public function getErrors(): array { - $messages = []; - if (!file_exists(implode(DIRECTORY_SEPARATOR, [$this->getRootPath(), 'core', 'core.api.php']))) { - $messages[] = $this->t('The web root could not be located.'); - } - if (!file_exists($this->getVendorPath() . DIRECTORY_SEPARATOR . 'autoload.php')) { - $messages[] = $this->t('Vendor folder "@vendor" is not a valid directory. Alternate vendor folder locations are not currently supported.', [ - '@vendor' => $this->getVendorPath(), - ]); - } - return $messages; + protected function hasValidRootPath() { + return file_exists(implode(DIRECTORY_SEPARATOR, [$this->getRootPath(), 'core', 'core.api.php'])); + } + + /** + * Determines if a valid vendor path can be located. + * + * @return bool + * TRUE if a valid root path can be determined, otherwise false. + */ + protected function hasValidVendorPatch() { + return file_exists($this->getVendorPath() . DIRECTORY_SEPARATOR . 'autoload.php'); } /** - * Gets the root file path. + * Gets the absolute path at which Drupal is installed. * * @return string * The root file path. @@ -83,7 +90,7 @@ protected function areSameLogicalDisk(string $root, string $vendor): bool { $root_statistics = stat($root); $vendor_statistics = stat($vendor); if ($root_statistics === FALSE || $vendor_statistics === FALSE) { - throw new \Exception('Unable to determine if the root and vendor directories are on the same logic disk.'); + throw new \RuntimeException('Unable to determine if the root and vendor directories are on the same logic disk.'); } return $root_statistics['dev'] === $vendor_statistics['dev']; } diff --git a/core/modules/auto_updates/src/ReadinessChecker/ReadinessCheckerManager.php b/core/modules/auto_updates/src/ReadinessChecker/ReadinessCheckerManager.php index cf745b23fe..c389e770ad 100644 --- a/core/modules/auto_updates/src/ReadinessChecker/ReadinessCheckerManager.php +++ b/core/modules/auto_updates/src/ReadinessChecker/ReadinessCheckerManager.php @@ -14,14 +14,14 @@ class ReadinessCheckerManager { /** * Time (in seconds) since the last check after which we generate a warning. * - * Defaults to 1 day. + * The value is equal to 1 day. */ private const LAST_CHECKED_WARNING = 60 * 60 * 24; /** * The key/value storage. * - * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface + * @var \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface */ protected $keyValueExpirable; @@ -77,7 +77,7 @@ public function __construct(KeyValueExpirableFactoryInterface $key_value_expirab * * @return $this */ - public function addChecker(ReadinessCheckerInterface $checker, $priority = 0): ReadinessCheckerManager { + public function addChecker(ReadinessCheckerInterface $checker, int $priority = 0): ReadinessCheckerManager { $this->checkersByPriority[$priority][] = $checker; ksort($this->checkersByPriority); return $this; @@ -87,7 +87,8 @@ public function addChecker(ReadinessCheckerInterface $checker, $priority = 0): R * Runs readiness checks. * * @param bool $refresh - * Whether to refresh the results. + * (optional) Whether to refresh the results, defaults FALSE. If FALSE then + * cached results will be returned if available. * * @return string[][] * A nested array of readiness check messages. The top level array is keyed @@ -134,7 +135,7 @@ protected function run(bool $refresh = FALSE): array { * The timestamp of the most recently completed run, or NULL if no run has * been completed. */ - public function getMostRecentRunTime(): int { + public function getMostRecentRunTime():?int { return $this->keyValueExpirable->get('readiness_check_timestamp'); } @@ -156,7 +157,7 @@ protected function getSortedCheckers(): array { * Gets the current checker service Ids. * * @return string - * A concatenated list of checker service Ids delimited by '::'. + * A concatenated list of checker service IDs delimited by '::'. */ protected function getCurrentCheckerIds(): string { $service_ids = []; diff --git a/core/modules/auto_updates/tests/src/Functional/ReadinessCheckerTest.php b/core/modules/auto_updates/tests/src/Functional/ReadinessCheckerTest.php index 932d930967..4f6ef3d10e 100644 --- a/core/modules/auto_updates/tests/src/Functional/ReadinessCheckerTest.php +++ b/core/modules/auto_updates/tests/src/Functional/ReadinessCheckerTest.php @@ -143,7 +143,7 @@ public function testReadinessCheckAfterInstall(): void { $this->assertReadinessReportMatches('1 check failed: 😿Oh no! A hacker now owns your files!'); // Confirm the new message is displayed after running the checkers manually. - $this->drupalGet('admin/config/auto_updates'); + $this->drupalGet('admin/reports/auto_updates'); $this->clickLink('run the readiness checks'); $assert->pageTextContains('Security has been compromised. "pass123" was a bad password!'); $assert->pageTextNotContains('😿Oh no! A hacker now owns your files!'); diff --git a/core/modules/update/update.info.yml b/core/modules/update/update.info.yml index 0c363f44c2..20b8eef0a5 100644 --- a/core/modules/update/update.info.yml +++ b/core/modules/update/update.info.yml @@ -3,6 +3,5 @@ type: module description: 'Checks for available updates, and can securely install or update modules and themes via a web interface.' version: VERSION package: Core -configure: update.settings dependencies: - drupal:file