diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml index 79fa2e8136728b60c17a51b153e73fc10abfb1d9..9f0e7fc912df72204be37674e0cf2cfe529e4969 100644 --- a/automatic_updates.services.yml +++ b/automatic_updates.services.yml @@ -6,6 +6,8 @@ services: - '@datetime.time' - '@event_dispatcher' - '@automatic_updates.updater' + - '@automatic_updates.cron_updater' + - '@config.factory' - 24 automatic_updates.updater: class: Drupal\automatic_updates\Updater diff --git a/src/Event/ReadinessCheckEvent.php b/src/Event/ReadinessCheckEvent.php index 2b8b9a6f86a4d023f8a631be64fcad0c6e04a929..ebe5706594977110deed28761510bfef390b9835 100644 --- a/src/Event/ReadinessCheckEvent.php +++ b/src/Event/ReadinessCheckEvent.php @@ -33,13 +33,24 @@ class ReadinessCheckEvent extends PreOperationStageEvent { * * @param \Drupal\automatic_updates\Updater $updater * The updater service. - * @param string[] $package_versions - * (optional) The desired package versions to update to, keyed by package + * @param string[] $project_versions + * (optional) The versions of the packages to update to, keyed by package * name. */ - public function __construct(Updater $updater, array $package_versions = []) { + public function __construct(Updater $updater, array $project_versions = []) { parent::__construct($updater); - $this->packageVersions = $package_versions; + if ($project_versions) { + if (count($project_versions) !== 1 || !array_key_exists('drupal', $project_versions)) { + throw new \InvalidArgumentException("Currently only updates to Drupal core are supported."); + } + $core_packages = $this->getStage()->getActiveComposer()->getCorePackageNames(); + // Update all core packages to the same version. + $package_versions = array_fill(0, count($core_packages), $project_versions['drupal']); + $this->packageVersions = array_combine($core_packages, $package_versions); + } + else { + $this->packageVersions = []; + } } /** diff --git a/src/Form/UpdaterForm.php b/src/Form/UpdaterForm.php index 42eba8fcf002a33ef7f91603b8724587dd8011d8..52db117b9ecf438a5d6cfe22cce6a5249aab3226 100644 --- a/src/Form/UpdaterForm.php +++ b/src/Form/UpdaterForm.php @@ -3,8 +3,10 @@ namespace Drupal\automatic_updates\Form; use Drupal\automatic_updates\BatchProcessor; +use Drupal\automatic_updates\Event\ReadinessCheckEvent; use Drupal\automatic_updates\Updater; use Drupal\automatic_updates\UpdateRecommender; +use Drupal\automatic_updates\Validation\ReadinessTrait; use Drupal\automatic_updates\Validation\ReadinessValidationManager; use Drupal\Core\Batch\BatchBuilder; use Drupal\Core\Form\FormBase; @@ -15,6 +17,7 @@ use Drupal\Core\Url; use Drupal\system\SystemManager; use Drupal\update\UpdateManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * Defines a form to update Drupal core. @@ -24,6 +27,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface; */ class UpdaterForm extends FormBase { + use ReadinessTrait; + /** * The updater service. * @@ -45,6 +50,13 @@ class UpdaterForm extends FormBase { */ protected $readinessValidationManager; + /** + * The event dispatcher service. + * + * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface + */ + protected $eventDispatcher; + /** * Constructs a new UpdaterForm object. * @@ -54,11 +66,14 @@ class UpdaterForm extends FormBase { * The updater service. * @param \Drupal\automatic_updates\Validation\ReadinessValidationManager $readiness_validation_manager * The readiness validation manager service. + * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher + * The event dispatcher service. */ - public function __construct(StateInterface $state, Updater $updater, ReadinessValidationManager $readiness_validation_manager) { + public function __construct(StateInterface $state, Updater $updater, ReadinessValidationManager $readiness_validation_manager, EventDispatcherInterface $event_dispatcher) { $this->updater = $updater; $this->state = $state; $this->readinessValidationManager = $readiness_validation_manager; + $this->eventDispatcher = $event_dispatcher; } /** @@ -75,7 +90,8 @@ class UpdaterForm extends FormBase { return new static( $container->get('state'), $container->get('automatic_updates.updater'), - $container->get('automatic_updates.readiness_validation_manager') + $container->get('automatic_updates.readiness_validation_manager'), + $container->get('event_dispatcher') ); } @@ -182,17 +198,18 @@ class UpdaterForm extends FormBase { ], ]; - // @todo Add a hasErrors() or getErrors() method to - // ReadinessValidationManager to make validation more introspectable. - // Re-running the readiness checks now should mean that when we display - // cached errors in automatic_updates_page_top(), we'll see errors that - // were raised during this run, instead of any previously cached results. - $errors = $this->readinessValidationManager->run() - ->getResults(SystemManager::REQUIREMENT_ERROR); - - if (empty($errors)) { + $results = $this->getReadinessErrors($recommended_release->getVersion()); + if (empty($results)) { $form['actions'] = $this->actions($form_state); } + else { + $this->messenger()->addError($this->getFailureMessageForSeverity(SystemManager::REQUIREMENT_ERROR)); + foreach ($results as $result) { + $messages = $result->getMessages(); + $message = count($messages) === 1 ? $messages[0] : $result->getSummary(); + $this->messenger()->addError($message); + } + } return $form; } @@ -256,4 +273,19 @@ class UpdaterForm extends FormBase { batch_set($batch); } + /** + * Gets validation errors before an update begins. + * + * @param string $update_version + * The version of Drupal to which we will update. + * + * @return \Drupal\package_manager\ValidationResult[] + * The error validation results. + */ + private function getReadinessErrors(string $update_version): array { + $event = new ReadinessCheckEvent($this->updater, ['drupal' => $update_version]); + $this->eventDispatcher->dispatch($event); + return $event->getResults(SystemManager::REQUIREMENT_ERROR); + } + } diff --git a/src/Validation/AdminReadinessMessages.php b/src/Validation/AdminReadinessMessages.php index 6dd6b10c28ae894d4aed5ad4969d69a654c8856b..8f07e13353c8b8731ee76d4b110734394d0d7f90 100644 --- a/src/Validation/AdminReadinessMessages.php +++ b/src/Validation/AdminReadinessMessages.php @@ -140,6 +140,9 @@ final class AdminReadinessMessages implements ContainerInjectionInterface { 'update.settings', 'system.status', 'update.confirmation_page', + 'automatic_updates.report_update', + 'automatic_updates.module_update', + 'automatic_updates.theme_update', ]; return !in_array($this->currentRouteMatch->getRouteName(), $disabled_routes, TRUE); } diff --git a/src/Validation/ReadinessValidationManager.php b/src/Validation/ReadinessValidationManager.php index 158c3ea6dc1e03f74077c4e97780475f6e6abefa..0a09a3034f38f804ea761249657e95fde7a54234 100644 --- a/src/Validation/ReadinessValidationManager.php +++ b/src/Validation/ReadinessValidationManager.php @@ -2,10 +2,12 @@ namespace Drupal\automatic_updates\Validation; +use Drupal\automatic_updates\CronUpdater; use Drupal\automatic_updates\Event\ReadinessCheckEvent; use Drupal\automatic_updates\Updater; use Drupal\automatic_updates\UpdateRecommender; use Drupal\Component\Datetime\TimeInterface; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -50,6 +52,20 @@ class ReadinessValidationManager { */ protected $updater; + /** + * The cron updater service. + * + * @var \Drupal\automatic_updates\CronUpdater + */ + protected $cronUpdater; + + /** + * The config factory service. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $config; + /** * Constructs a ReadinessValidationManager. * @@ -61,14 +77,20 @@ class ReadinessValidationManager { * The event dispatcher service. * @param \Drupal\automatic_updates\Updater $updater * The updater service. + * @param \Drupal\automatic_updates\CronUpdater $cron_updater + * The cron updater service. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config + * The config factory service. * @param int $results_time_to_live * The number of hours to store results. */ - public function __construct(KeyValueExpirableFactoryInterface $key_value_expirable_factory, TimeInterface $time, EventDispatcherInterface $dispatcher, Updater $updater, int $results_time_to_live) { + public function __construct(KeyValueExpirableFactoryInterface $key_value_expirable_factory, TimeInterface $time, EventDispatcherInterface $dispatcher, Updater $updater, CronUpdater $cron_updater, ConfigFactoryInterface $config, int $results_time_to_live) { $this->keyValueExpirable = $key_value_expirable_factory->get('automatic_updates'); $this->time = $time; $this->eventDispatcher = $dispatcher; $this->updater = $updater; + $this->cronUpdater = $cron_updater; + $this->config = $config; $this->resultsTimeToLive = $results_time_to_live; } @@ -78,20 +100,20 @@ class ReadinessValidationManager { * @return $this */ public function run(): self { - $composer = $this->updater->getActiveComposer(); - $recommender = new UpdateRecommender(); $release = $recommender->getRecommendedRelease(TRUE); - if ($release) { - $core_packages = $composer->getCorePackageNames(); - // Update all core packages to the same version. - $package_versions = array_fill(0, count($core_packages), $release->getVersion()); - $package_versions = array_combine($core_packages, $package_versions); + // If updates will run during cron, use the cron updater service provided by + // this module. This will allow subscribers to ReadinessCheckEvent to run + // specific validation for conditions that only affect cron updates. + if ($this->config->get('automatic_updates.settings')->get('cron') == CronUpdater::DISABLED) { + $stage = $this->updater; } else { - $package_versions = []; + $stage = $this->cronUpdater; } - $event = new ReadinessCheckEvent($this->updater, $package_versions); + + $project_versions = $release ? ['drupal' => $release->getVersion()] : []; + $event = new ReadinessCheckEvent($stage, $project_versions); $this->eventDispatcher->dispatch($event); $results = $event->getResults(); $this->keyValueExpirable->setWithExpire( @@ -116,10 +138,16 @@ class ReadinessValidationManager { $listeners = $this->eventDispatcher->getListeners($event_name); $string = ''; foreach ($listeners as $listener) { - /** @var object $object */ - $object = $listener[0]; - $method = $listener[1]; - $string .= '-' . get_class($object) . '::' . $method; + if (is_array($listener)) { + $string .= is_object($listener[0]) ? get_class($listener[0]) : $listener[0]; + $string .= $listener[1]; + } + elseif (is_object($listener)) { + $string .= "-" . get_class($listener); + } + elseif (is_string($listener)) { + $string .= "-$listener"; + } } return $string; } diff --git a/tests/src/Kernel/ReadinessValidation/ReadinessValidationManagerTest.php b/tests/src/Kernel/ReadinessValidation/ReadinessValidationManagerTest.php index c45e4a11ddd207b44c7789fa9eee1c6b340baeeb..ced48e45b18277661cc033326927252831e7fca7 100644 --- a/tests/src/Kernel/ReadinessValidation/ReadinessValidationManagerTest.php +++ b/tests/src/Kernel/ReadinessValidation/ReadinessValidationManagerTest.php @@ -2,7 +2,9 @@ namespace Drupal\Tests\automatic_updates\Kernel\ReadinessValidation; +use Drupal\automatic_updates\CronUpdater; use Drupal\automatic_updates\Event\ReadinessCheckEvent; +use Drupal\automatic_updates\Updater; use Drupal\automatic_updates_test\ReadinessChecker\TestChecker1; use Drupal\automatic_updates_test2\ReadinessChecker\TestChecker2; use Drupal\system\SystemManager; @@ -210,4 +212,25 @@ class ReadinessValidationManagerTest extends AutomaticUpdatesKernelTestBase { $this->assertCheckerResultsFromManager($expected_results); } + /** + * Tests the Automatic Updates cron setting changes which stage class is used. + */ + public function testCronSetting(): void { + $this->enableModules(['automatic_updates']); + $stage_class = NULL; + $listener = function (ReadinessCheckEvent $event) use (&$stage_class): void { + $stage_class = get_class($event->getStage()); + }; + $event_dispatcher = $this->container->get('event_dispatcher'); + $event_dispatcher->addListener(ReadinessCheckEvent::class, $listener); + $this->container->get('automatic_updates.readiness_validation_manager')->run(); + // By default, updates will be enabled on cron. + $this->assertSame(CronUpdater::class, $stage_class); + $this->config('automatic_updates.settings') + ->set('cron', CronUpdater::DISABLED) + ->save(); + $this->container->get('automatic_updates.readiness_validation_manager')->run(); + $this->assertSame(Updater::class, $stage_class); + } + }