diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml index d7b7982665a9822bfaa504146d4a48744bd4c55e..88a9249f5c67ca98e6f0307aea58e103eac93b58 100644 --- a/automatic_updates.services.yml +++ b/automatic_updates.services.yml @@ -76,6 +76,11 @@ services: arguments: ['%app.root%', '%site.path%', '@file_system', '@stream_wrapper_manager'] tags: - { name: event_subscriber } + automatic_updates.update_version_subscriber: + class: Drupal\automatic_updates\Event\UpdateVersionSubscriber + arguments: ['@module_handler'] + tags: + - { name: event_subscriber } automatic_updates.composer_executable_validator: class: Drupal\automatic_updates\Validation\ComposerExecutableValidator arguments: ['@automatic_updates.exec_finder'] diff --git a/src/Event/PreStartEvent.php b/src/Event/PreStartEvent.php index e8fecadb02c07647debaecdfce62fd0d2b556dfc..f5b32b9f4de72152295ccc06a7fe9e8be246dfc5 100644 --- a/src/Event/PreStartEvent.php +++ b/src/Event/PreStartEvent.php @@ -9,4 +9,32 @@ class PreStartEvent extends UpdateEvent { use ExcludedPathsTrait; + /** + * The desired package versions to update to, keyed by package name. + * + * @var string[] + */ + protected $packageVersions; + + /** + * Constructs a PreStartEvent. + * + * @param string[] $package_versions + * (optional) The desired package versions to update to, keyed by package + * name. + */ + public function __construct(array $package_versions = []) { + $this->packageVersions = $package_versions; + } + + /** + * Returns the desired package versions to update to. + * + * @return string[] + * The desired package versions to update to, keyed by package name. + */ + public function getPackageVersions(): array { + return $this->packageVersions; + } + } diff --git a/src/Event/UpdateVersionSubscriber.php b/src/Event/UpdateVersionSubscriber.php new file mode 100644 index 0000000000000000000000000000000000000000..5029b66d2f942e6f7626281a5076342acd2f5c44 --- /dev/null +++ b/src/Event/UpdateVersionSubscriber.php @@ -0,0 +1,74 @@ +<?php + +namespace Drupal\automatic_updates\Event; + +use Drupal\automatic_updates\AutomaticUpdatesEvents; +use Drupal\automatic_updates\Validation\ValidationResult; +use Drupal\Core\Extension\ExtensionVersion; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Validates that core updates are within a supported version range. + */ +class UpdateVersionSubscriber implements EventSubscriberInterface { + + use StringTranslationTrait; + + /** + * Constructs an UpdateVersionSubscriber. + * + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler service. + */ + public function __construct(ModuleHandlerInterface $module_handler) { + // Load procedural functions needed for ::getCoreVersion(). + $module_handler->loadInclude('update', 'inc', 'update.compare'); + } + + /** + * Returns the running core version, according to the Update module. + * + * @return string + * The running core version as known to the Update module. + */ + protected function getCoreVersion(): string { + $available_updates = update_calculate_project_data(update_get_available()); + return $available_updates['drupal']['existing_version']; + } + + /** + * Validates that core is not being updated to another minor or major version. + * + * @param \Drupal\automatic_updates\Event\PreStartEvent $event + * The event object. + */ + public function checkUpdateVersion(PreStartEvent $event): void { + $from_version = ExtensionVersion::createFromVersionString($this->getCoreVersion()); + $to_version = ExtensionVersion::createFromVersionString($event->getPackageVersions()['drupal/core']); + + if ($from_version->getMajorVersion() !== $to_version->getMajorVersion()) { + $error = ValidationResult::createError([ + $this->t('Updating from one major version to another is not supported.'), + ]); + $event->addValidationResult($error); + } + elseif ($from_version->getMinorVersion() !== $to_version->getMinorVersion()) { + $error = ValidationResult::createError([ + $this->t('Updating from one minor version to another is not supported.'), + ]); + $event->addValidationResult($error); + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + return [ + AutomaticUpdatesEvents::PRE_START => 'checkUpdateVersion', + ]; + } + +} diff --git a/src/Updater.php b/src/Updater.php index 57588de8b298fad4eab6196bf348e9332ffc2ed8..843b5d6fe7f7b0782dbb8809ae295aa0c063b708 100644 --- a/src/Updater.php +++ b/src/Updater.php @@ -166,10 +166,12 @@ class Updater { if (count($project_versions) !== 1 || !array_key_exists('drupal', $project_versions)) { throw new \InvalidArgumentException("Currently only updates to Drupal core are supported."); } - $packages[] = 'drupal/core:' . $project_versions['drupal']; + $packages = [ + 'drupal/core' => $project_versions['drupal'], + ]; $stage_key = $this->createActiveStage($packages); /** @var \Drupal\automatic_updates\Event\PreStartEvent $event */ - $event = $this->dispatchUpdateEvent(new PreStartEvent(), AutomaticUpdatesEvents::PRE_START); + $event = $this->dispatchUpdateEvent(new PreStartEvent($packages), AutomaticUpdatesEvents::PRE_START); $this->beginner->begin(static::getActiveDirectory(), static::getStageDirectory(), $this->getExclusions($event)); return $stage_key; } @@ -216,9 +218,6 @@ class Updater { public function commit(): void { /** @var \Drupal\automatic_updates\Event\PreCommitEvent $event */ $event = $this->dispatchUpdateEvent(new PreCommitEvent(), AutomaticUpdatesEvents::PRE_COMMIT); - // @todo Pass excluded paths into the committer once - // https://github.com/php-tuf/composer-stager/pull/14 is in a tagged release - // of Composer Stager. $this->committer->commit($this->getStageDirectory(), static::getActiveDirectory(), $this->getExclusions($event)); } @@ -255,12 +254,17 @@ class Updater { * The active update ID. */ private function createActiveStage(array $package_versions): string { + $requirements = []; + foreach ($package_versions as $package_name => $version) { + $requirements[] = "$package_name:$version"; + } + $value = static::STATE_KEY . microtime(); $this->state->set( static::STATE_KEY, [ 'id' => $value, - 'package_versions' => $package_versions, + 'package_versions' => $requirements, ] ); return $value; diff --git a/tests/src/Functional/ExclusionsTest.php b/tests/src/Functional/ExclusionsTest.php index 26d8a26cdaad47b236c9bc4ceaacf8921ea8e686..cf0a77990f850901f346bc6debfab259ec5b0b9e 100644 --- a/tests/src/Functional/ExclusionsTest.php +++ b/tests/src/Functional/ExclusionsTest.php @@ -15,7 +15,7 @@ class ExclusionsTest extends BrowserTestBase { /** * {@inheritdoc} */ - protected static $modules = ['automatic_updates_test']; + protected static $modules = ['automatic_updates_test', 'update_test']; /** * {@inheritdoc} @@ -40,6 +40,22 @@ class ExclusionsTest extends BrowserTestBase { $settings['file_private_path'] = 'files/private'; new Settings($settings); + // Updater::begin() will trigger update validators, such as + // \Drupal\automatic_updates\Event\UpdateVersionSubscriber, that need to + // fetch release metadata. We need to ensure that those HTTP request(s) + // succeed, so set them up to point to our fake release metadata. + $this->config('update_test.settings') + ->set('xml_map', [ + 'drupal' => '0.0', + ]) + ->save(); + $this->config('update.settings') + ->set('fetch.url', $this->baseUrl . '/automatic-update-test') + ->save(); + $this->config('update_test.settings') + ->set('system_info.#all.version', '9.8.0') + ->save(); + $updater->begin(['drupal' => '9.8.1']); $this->assertFileDoesNotExist("$stage_dir/sites/default/settings.php"); $this->assertFileDoesNotExist("$stage_dir/sites/default/settings.local.php"); diff --git a/tests/src/Kernel/UpdaterTest.php b/tests/src/Kernel/UpdaterTest.php index 8f8ab0ecb28e461e8cdda72a7feefd6c3220a207..110eb4bf94aba13e6900e274b2f7ae1ec0695f5a 100644 --- a/tests/src/Kernel/UpdaterTest.php +++ b/tests/src/Kernel/UpdaterTest.php @@ -3,6 +3,10 @@ namespace Drupal\Tests\automatic_updates\Kernel; use Drupal\KernelTests\KernelTestBase; +use GuzzleHttp\Client; +use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\Psr7\Response; +use GuzzleHttp\Psr7\Utils; use Prophecy\Argument; /** @@ -17,14 +21,28 @@ class UpdaterTest extends KernelTestBase { */ protected static $modules = [ 'automatic_updates', - 'update', + 'automatic_updates_test', 'composer_stager_bypass', + 'update', + 'update_test', ]; /** * Tests that correct versions are staged after calling ::begin(). */ public function testCorrectVersionsStaged() { + // Ensure that the HTTP client will fetch our fake release metadata. + $release_data = Utils::tryFopen(__DIR__ . '/../../fixtures/release-history/drupal.0.0.xml', 'r'); + $response = new Response(200, [], Utils::streamFor($release_data)); + $handler = new MockHandler([$response]); + $client = new Client(['handler' => $handler]); + $this->container->set('http_client', $client); + + // Set the running core version to 9.8.0. + $this->config('update_test.settings') + ->set('system_info.#all.version', '9.8.0') + ->save(); + $this->container->get('automatic_updates.updater')->begin([ 'drupal' => '9.8.1', ]);