diff --git a/package_manager/package_manager.services.yml b/package_manager/package_manager.services.yml index b6fa99e3ac5a5e8d5ff7deb0248060b7d67956dc..bffff5e0a9e61379b7c8418003b76209f18d83b4 100644 --- a/package_manager/package_manager.services.yml +++ b/package_manager/package_manager.services.yml @@ -168,3 +168,6 @@ services: class: Drupal\package_manager\Validator\ComposerPatchesValidator tags: - { name: event_subscriber } + package_manager.update_processor: + class: Drupal\package_manager\PackageManagerUpdateProcessor + arguments: [ '@config.factory', '@queue', '@update.fetcher', '@state', '@private_key', '@keyvalue', '@keyvalue.expirable' ] diff --git a/package_manager/src/PackageManagerUpdateProcessor.php b/package_manager/src/PackageManagerUpdateProcessor.php new file mode 100644 index 0000000000000000000000000000000000000000..4d4f309fc526ead6f144133b6e037f13d96da706 --- /dev/null +++ b/package_manager/src/PackageManagerUpdateProcessor.php @@ -0,0 +1,98 @@ +<?php + +namespace Drupal\package_manager; + +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface; +use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; +use Drupal\Core\PrivateKey; +use Drupal\Core\Queue\QueueFactory; +use Drupal\Core\State\StateInterface; +use Drupal\update\UpdateFetcherInterface; +use Drupal\update\UpdateProcessor; + +/** + * Extends the Update module's update processor allow fetching any project. + * + * The Update module's update processor service is intended to only fetch + * information for projects in the active codebase. Although it would be + * possible to use the Update module's update processor service to fetch + * information for projects not in the active code base this would add the + * project information to Update module's cache which would result in these + * projects being returned from the Update module's global functions such as + * update_get_available(). + */ +class PackageManagerUpdateProcessor extends UpdateProcessor { + + /** + * Constructs an PackageManagerUpdateProcessor object. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. + * @param \Drupal\Core\Queue\QueueFactory $queue_factory + * The queue factory. + * @param \Drupal\update\UpdateFetcherInterface $update_fetcher + * The update fetcher service. + * @param \Drupal\Core\State\StateInterface $state_store + * The state service. + * @param \Drupal\Core\PrivateKey $private_key + * The private key factory service. + * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory + * The key/value factory. + * @param \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface $key_value_expirable_factory + * The expirable key/value factory. + */ + public function __construct(ConfigFactoryInterface $config_factory, QueueFactory $queue_factory, UpdateFetcherInterface $update_fetcher, StateInterface $state_store, PrivateKey $private_key, KeyValueFactoryInterface $key_value_factory, KeyValueExpirableFactoryInterface $key_value_expirable_factory) { + $this->updateFetcher = $update_fetcher; + $this->updateSettings = $config_factory->get('update.settings'); + $this->fetchQueue = $queue_factory->get('package_manager.update_fetch_tasks'); + $this->tempStore = $key_value_expirable_factory->get('package_manager.update'); + $this->fetchTaskStore = $key_value_factory->get('package_manager.update_fetch_task'); + $this->availableReleasesTempStore = $key_value_expirable_factory->get('package_manager.update_available_releases'); + $this->stateStore = $state_store; + $this->privateKey = $private_key; + $this->fetchTasks = []; + $this->failed = []; + } + + /** + * Gets the project data by name. + * + * @param string $name + * The project name. + * + * @return mixed[] + * The project data if any is available, otherwise NULL. + */ + public function getProjectData(string $name): ?array { + if ($this->availableReleasesTempStore->has($name)) { + return $this->availableReleasesTempStore->get($name); + } + $project_fetch_data = [ + 'name' => $name, + 'project_type' => 'unknown', + 'includes' => [], + ]; + $this->createFetchTask($project_fetch_data); + if ($this->processFetchTask($project_fetch_data)) { + // If the fetch task was successful return the project information. + return $this->availableReleasesTempStore->get($name); + } + return NULL; + } + + /** + * {@inheritdoc} + */ + public function processFetchTask($project) { + // The parent method will set 'update.last_check' which will be used to + // inform the user when the last time update information was checked. In + // order to leave this value unaffected we will reset this to it's previous + // value. + $last_check = $this->stateStore->get('update.last_check'); + $success = parent::processFetchTask($project); + $this->stateStore->set('update.last_check', $last_check); + return $success; + } + +} diff --git a/package_manager/src/ProjectInfo.php b/package_manager/src/ProjectInfo.php index c28b0927f2a8338e5814040d6362f8c8bd849a6d..fd1343a5596f66e2857927fc9da8546d6374f4e3 100644 --- a/package_manager/src/ProjectInfo.php +++ b/package_manager/src/ProjectInfo.php @@ -74,9 +74,12 @@ final class ProjectInfo { * If data about available updates cannot be retrieved. */ public function getProjectInfo(): ?array { - $available_updates = update_get_available(TRUE); + $available_updates = $this->getAvailableProjects(); $project_data = update_calculate_project_data($available_updates); - return $project_data[$this->name] ?? NULL; + if (!isset($project_data[$this->name])) { + return $available_updates[$this->name] ?? NULL; + } + return $project_data[$this->name]; } /** @@ -99,31 +102,39 @@ final class ProjectInfo { if (!$project) { return NULL; } - $available_updates = update_get_available()[$this->name]; + $available_updates = $this->getAvailableProjects()[$this->name]; if ($available_updates['project_status'] !== 'published') { throw new \RuntimeException("The project '{$this->name}' can not be updated because its status is " . $available_updates['project_status']); } + $installed_version = $this->getInstalledVersion(); - // If we're already up-to-date, there's nothing else we need to do. - if ($project['status'] === UpdateManagerInterface::CURRENT) { - return []; - } + if ($installed_version) { + // If the project is installed, and we're already up-to-date, there's + // nothing else we need to do. + if ($project['status'] === UpdateManagerInterface::CURRENT) { + return []; + } - if (empty($available_updates['releases'])) { - // If project is not current we should always have at least one release. - throw new \RuntimeException('There was a problem getting update information. Try again later.'); + if (empty($available_updates['releases'])) { + // If project is installed but not current we should always have at + // least one release. + throw new \RuntimeException('There was a problem getting update information. Try again later.'); + } } - $installed_version = $this->getInstalledVersion(); + $support_branches = explode(',', $available_updates['supported_branches']); $installable_releases = []; foreach ($available_updates['releases'] as $release_info) { $release = ProjectRelease::createFromArray($release_info); $version = $release->getVersion(); - $semantic_version = LegacyVersionUtility::convertToSemanticVersion($version); - $semantic_installed_version = LegacyVersionUtility::convertToSemanticVersion($installed_version); - if (Comparator::lessThanOrEqualTo($semantic_version, $semantic_installed_version)) { - // Stop searching for releases as soon as we find the installed version. - break; + if ($installed_version) { + $semantic_version = LegacyVersionUtility::convertToSemanticVersion($version); + $semantic_installed_version = LegacyVersionUtility::convertToSemanticVersion($installed_version); + if (Comparator::lessThanOrEqualTo($semantic_version, $semantic_installed_version)) { + // If the project is installed stop searching for releases as soon as + // we find the installed version. + break; + } } if ($this->isInstallable($release, $support_branches)) { $installable_releases[$version] = $release; @@ -141,12 +152,37 @@ final class ProjectInfo { */ public function getInstalledVersion(): ?string { if ($project_data = $this->getProjectInfo()) { - if (empty($project_data['existing_version'])) { - throw new \UnexpectedValueException("Project '{$this->name}' does not have 'existing_version' set"); - } - return $project_data['existing_version']; + return $project_data['existing_version'] ?? NULL; } return NULL; } + /** + * Gets the available projects. + * + * @return array + * The available projects keyed by project machine name in the format + * returned by update_get_available(). If the project specified in ::name is + * not returned from update_get_available() this project will be explicitly + * fetched and added the return value of this function. + * + * @see \update_get_available() + */ + private function getAvailableProjects(): array { + $available_projects = update_get_available(TRUE); + // update_get_available() will only returns projects that are in the active + // codebase. If the project specified by ::name is not returned in + // $available_projects, it means it is not in the active codebase, therefore + // we will retrieve the project information from Package Manager's own + // update processor service. + if (!isset($available_projects[$this->name])) { + /** @var \Drupal\package_manager\PackageManagerUpdateProcessor $update_processor */ + $update_processor = \Drupal::service('package_manager.update_processor'); + if ($project_data = $update_processor->getProjectData($this->name)) { + $available_projects[$this->name] = $project_data; + } + } + return $available_projects; + } + } diff --git a/package_manager/tests/src/Kernel/ComposerUtilityTest.php b/package_manager/tests/src/Kernel/ComposerUtilityTest.php index bd1d7c16f960393f6e5141d3f49aa2a2d8434192..f80331b01dc407f482d05f0c95dd3562f50671ce 100644 --- a/package_manager/tests/src/Kernel/ComposerUtilityTest.php +++ b/package_manager/tests/src/Kernel/ComposerUtilityTest.php @@ -19,7 +19,7 @@ class ComposerUtilityTest extends KernelTestBase { /** * {@inheritdoc} */ - protected static $modules = ['package_manager']; + protected static $modules = ['package_manager', 'update']; /** * {@inheritdoc} diff --git a/package_manager/tests/src/Kernel/FileSyncerFactoryTest.php b/package_manager/tests/src/Kernel/FileSyncerFactoryTest.php index b22fda8ed1c83a8bad260b31b272544b02e2845a..56aefbc94bbd3825e4e434237e00b27e12124934 100644 --- a/package_manager/tests/src/Kernel/FileSyncerFactoryTest.php +++ b/package_manager/tests/src/Kernel/FileSyncerFactoryTest.php @@ -17,7 +17,7 @@ class FileSyncerFactoryTest extends KernelTestBase { /** * {@inheritdoc} */ - protected static $modules = ['package_manager']; + protected static $modules = ['package_manager', 'update']; /** * Data provider for testFactory(). diff --git a/package_manager/tests/src/Kernel/ProjectInfoTest.php b/package_manager/tests/src/Kernel/ProjectInfoTest.php index d0e404ff77f82130df6a28fda57c6e1aa1f49297..70952df90a40c088b66c271d26505c1f3d6ca342 100644 --- a/package_manager/tests/src/Kernel/ProjectInfoTest.php +++ b/package_manager/tests/src/Kernel/ProjectInfoTest.php @@ -1,9 +1,8 @@ <?php -namespace Drupal\Tests\package_manger\Kernel; +namespace Drupal\Tests\package_manager\Kernel; use Drupal\package_manager\ProjectInfo; -use Drupal\Tests\package_manager\Kernel\PackageManagerKernelTestBase; /** * @coversDefaultClass \Drupal\package_manager\ProjectInfo @@ -118,6 +117,52 @@ class ProjectInfoTest extends PackageManagerKernelTestBase { ]; } + /** + * Tests a project that is not in the codebase. + */ + public function testNewProject(): void { + $fixtures_directory = __DIR__ . '/../../fixtures/release-history/'; + $metadata_fixtures['drupal'] = $fixtures_directory . 'drupal.9.8.2.xml'; + $metadata_fixtures['aaa_automatic_updates_test'] = $fixtures_directory . 'aaa_automatic_updates_test.9.8.2.xml'; + $this->setReleaseMetadata($metadata_fixtures); + $available = update_get_available(TRUE); + $this->assertSame(['drupal'], array_keys($available)); + $this->setReleaseMetadata($metadata_fixtures); + $state = $this->container->get('state'); + // Set the state that the update module uses to store last checked time + // ensure our calls do not affect it. + $state->set('update.last_check', 123); + $project_info = new ProjectInfo('aaa_automatic_updates_test'); + $project_data = $project_info->getProjectInfo(); + // Ensure the project information is correct. + $this->assertSame('AAA', $project_data['title']); + $all_releases = [ + '7.0.1', + '7.0.0', + '7.0.0-alpha1', + '8.x-6.2', + '8.x-6.1', + '8.x-6.0', + '8.x-6.0-alpha1', + '7.0.x-dev', + '8.x-6.x-dev', + ]; + $uninstallable_releases = ['7.0.x-dev', '8.x-6.x-dev']; + $installable_releases = array_values(array_diff($all_releases, $uninstallable_releases)); + $this->assertSame( + $all_releases, + array_keys($project_data['releases']) + ); + $this->assertSame( + $installable_releases, + array_keys($project_info->getInstallableReleases()) + ); + $this->assertNull($project_info->getInstalledVersion()); + // Ensure we have not changed the state the update module uses to store + // the last checked time. + $this->assertSame(123, $state->get('update.last_check')); + } + /** * Tests a project with a status other than "published". * diff --git a/package_manager/tests/src/Kernel/ServicesTest.php b/package_manager/tests/src/Kernel/ServicesTest.php index ef91a3c2158d3220586ff1d17970501a819a8255..db6f8628f1bc7da4a0cc40f13d1123fdf7bc930a 100644 --- a/package_manager/tests/src/Kernel/ServicesTest.php +++ b/package_manager/tests/src/Kernel/ServicesTest.php @@ -18,7 +18,7 @@ class ServicesTest extends KernelTestBase { /** * {@inheritdoc} */ - protected static $modules = ['package_manager']; + protected static $modules = ['package_manager', 'update']; /** * Tests that Package Manager's public services can be instantiated.