diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml index cce9d1c7260d870f3d2e8b230c58da57f3b8de7a..f831d0741ad669d6f75b13bf6e1d2eb5cc442283 100644 --- a/automatic_updates.services.yml +++ b/automatic_updates.services.yml @@ -46,6 +46,14 @@ services: - '@automatic_updates.path_locator' tags: - { name: event_subscriber } + automatic_updates.pending_updates_validator: + class: Drupal\automatic_updates\Validator\PendingUpdatesValidator + arguments: + - '%app.root%' + - '@update.post_update_registry' + - '@string_translation' + tags: + - { name: event_subscriber } automatic_updates.validator.file_system_permissions: class: Drupal\automatic_updates\Validator\WritableFileSystemValidator arguments: diff --git a/src/Validator/PendingUpdatesValidator.php b/src/Validator/PendingUpdatesValidator.php new file mode 100644 index 0000000000000000000000000000000000000000..02c3d686a0f7844d6e98a66cdf80937426d5fd54 --- /dev/null +++ b/src/Validator/PendingUpdatesValidator.php @@ -0,0 +1,84 @@ +<?php + +namespace Drupal\automatic_updates\Validator; + +use Drupal\automatic_updates\AutomaticUpdatesEvents; +use Drupal\automatic_updates\Event\UpdateEvent; +use Drupal\automatic_updates\Validation\ValidationResult; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\StringTranslation\TranslationInterface; +use Drupal\Core\Update\UpdateRegistry; +use Drupal\Core\Url; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Validates that there are no pending database updates. + */ +class PendingUpdatesValidator implements EventSubscriberInterface { + + use StringTranslationTrait; + + /** + * The Drupal root. + * + * @var string + */ + protected $appRoot; + + /** + * The update registry service. + * + * @var \Drupal\Core\Update\UpdateRegistry + */ + protected $updateRegistry; + + /** + * Constructs an PendingUpdatesValidator object. + * + * @param string $app_root + * The Drupal root. + * @param \Drupal\Core\Update\UpdateRegistry $update_registry + * The update registry service. + * @param \Drupal\Core\StringTranslation\TranslationInterface $translation + * The translation service. + */ + public function __construct(string $app_root, UpdateRegistry $update_registry, TranslationInterface $translation) { + $this->appRoot = $app_root; + $this->updateRegistry = $update_registry; + $this->setStringTranslation($translation); + } + + /** + * Validates that there are no pending database updates. + * + * @param \Drupal\automatic_updates\Event\UpdateEvent $event + * The update event. + */ + public function checkPendingUpdates(UpdateEvent $event) { + require_once $this->appRoot . '/core/includes/install.inc'; + require_once $this->appRoot . '/core/includes/update.inc'; + + drupal_load_updates(); + $hook_updates = update_get_update_list(); + $post_updates = $this->updateRegistry->getPendingUpdateFunctions(); + + if ($hook_updates || $post_updates) { + $message = $this->t('Some modules have database schema updates to install. You should run the <a href=":update">database update script</a> immediately.', [ + ':update' => Url::fromRoute('system.db_update')->toString(), + ]); + $error = ValidationResult::createError([$message]); + $event->addValidationResult($error); + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + return [ + AutomaticUpdatesEvents::PRE_START => 'checkPendingUpdates', + AutomaticUpdatesEvents::READINESS_CHECK => 'checkPendingUpdates', + ]; + } + +} diff --git a/tests/fixtures/db_update.php b/tests/fixtures/db_update.php new file mode 100644 index 0000000000000000000000000000000000000000..dcebeff41012ad40820b3e17880b40c74f31eb3c --- /dev/null +++ b/tests/fixtures/db_update.php @@ -0,0 +1,12 @@ +<?php + +/** + * @file + * Contains a fake database update function for testing. + */ + +/** + * Here is a fake update. + */ +function automatic_updates_update_50000() { +} diff --git a/tests/fixtures/post_update.php b/tests/fixtures/post_update.php new file mode 100644 index 0000000000000000000000000000000000000000..a77909b6d4db212fd19041e4a379d68dcfae4b5c --- /dev/null +++ b/tests/fixtures/post_update.php @@ -0,0 +1,12 @@ +<?php + +/** + * @file + * Contains a fake post-update function for testing. + */ + +/** + * Here is a fake post-update. + */ +function automatic_updates_post_update_test() { +} diff --git a/tests/src/Kernel/ReadinessValidation/PendingUpdatesValidatorTest.php b/tests/src/Kernel/ReadinessValidation/PendingUpdatesValidatorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a304b7aad798a4121982ba98c158f37c1f5f4e98 --- /dev/null +++ b/tests/src/Kernel/ReadinessValidation/PendingUpdatesValidatorTest.php @@ -0,0 +1,89 @@ +<?php + +namespace Drupal\Tests\automatic_updates\Kernel\ReadinessValidation; + +use Drupal\automatic_updates\Event\UpdateEvent; +use Drupal\automatic_updates\Validation\ValidationResult; +use Drupal\KernelTests\KernelTestBase; +use Drupal\Tests\automatic_updates\Traits\ValidationTestTrait; + +/** + * @covers \Drupal\automatic_updates\Validator\PendingUpdatesValidator + * + * @group automatic_updates + */ +class PendingUpdatesValidatorTest extends KernelTestBase { + + use ValidationTestTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'automatic_updates', + 'package_manager', + 'system', + 'update', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + // Make the update system think that all of System's post-update functions + // have run. Since kernel tests don't normally install modules and register + // their updates, we need to do this so that the validator is being tested + // from a clean, fully up-to-date state. + $updates = $this->container->get('update.post_update_registry') + ->getPendingUpdateFunctions(); + + $this->container->get('keyvalue') + ->get('post_update') + ->set('existing_updates', $updates); + } + + /** + * Tests that no error is raised if there are no pending updates. + */ + public function testNoPendingUpdates(): void { + $event = new UpdateEvent(); + $this->container->get('automatic_updates.pending_updates_validator') + ->checkPendingUpdates($event); + $this->assertEmpty($event->getResults()); + } + + /** + * Tests that an error is raised if there are pending schema updates. + */ + public function testPendingUpdateHook(): void { + require __DIR__ . '/../../../fixtures/db_update.php'; + + $this->container->get('keyvalue') + ->get('system.schema') + ->set('automatic_updates', \Drupal::CORE_MINIMUM_SCHEMA_VERSION); + + $result = ValidationResult::createError(['Some modules have database schema updates to install. You should run the <a href="/update.php">database update script</a> immediately.']); + + $event = new UpdateEvent(); + $this->container->get('automatic_updates.pending_updates_validator') + ->checkPendingUpdates($event); + $this->assertValidationResultsEqual([$result], $event->getResults()); + } + + /** + * Tests that an error is raised if there are pending post-updates. + */ + public function testPendingPostUpdate(): void { + require __DIR__ . '/../../../fixtures/post_update.php'; + + $result = ValidationResult::createError(['Some modules have database schema updates to install. You should run the <a href="/update.php">database update script</a> immediately.']); + + $event = new UpdateEvent(); + $this->container->get('automatic_updates.pending_updates_validator') + ->checkPendingUpdates($event); + $this->assertValidationResultsEqual([$result], $event->getResults()); + } + +}