From 23d7e5fbf6840bce94c7ac83703f1301478f2312 Mon Sep 17 00:00:00 2001 From: Guy Sartorelli Date: Tue, 14 Dec 2021 10:08:48 +1300 Subject: [PATCH] fix: Don't rely on inaccessible packages when fetching update info. Composer wants to use information about all dependencies when reporting the most up-to-date version that meets the project's stability/version constraints and doesn't conflict with any other dependencies. If a dependency is inaccessible (e.g. private repositories or IP-restricted hosting) composer will fail to declare a version candidate, which results in missing information even for some accessible packages. By ommitting inaccessible repositories, we do at least get _some_ version information, even if there is a change of compatability issues. --- README.md | 5 +- src/DriverReflection.php | 74 ++++++++++++++++++++++ src/Extensions/ComposerLoaderExtension.php | 73 +++++++++++++++++++-- 3 files changed, 144 insertions(+), 8 deletions(-) create mode 100644 src/DriverReflection.php diff --git a/README.md b/README.md index 7104e29..b447338 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,10 @@ will need to have the necessary permissions to access the private VCS repositori update information about necessary updates to the module. If the process looking for available updates fails (for example, due to an authentication failure against a private -repository) the process will fail gracefully and allow the rest of the report generation to continue. +repository) the process will fail gracefully and allow the rest of the report generation to continue. However, this +can result in incomplete information being fetched about non-private repositories due to the way composer checks for +conflicts between packages. For this reason, if you cannot supply authentication details for private repositories, +you should mark those repositories as inaccessible as per the documentation in the [SilverStripe Maintenance module](https://github.com/bringyourownideas/silverstripe-maintenance#private-repositories). Users on the [Common Web Platform](https://cwp.govt.nz) will currently not be able to retrieve information about updates to private repositories. diff --git a/src/DriverReflection.php b/src/DriverReflection.php new file mode 100644 index 0000000..dc2e8c1 --- /dev/null +++ b/src/DriverReflection.php @@ -0,0 +1,74 @@ +getRepoConfig(), $io, $config); + try { + $driver->initialize(); + } catch (RuntimeException $e) { + // no-op - this is probably caused due to insufficient permissions when trying to create /var/www/.ssh + // but since we're just getting the driver as a shortcut to getting the repository name, we can ignore this for now. + } + return $driver; + } + + foreach ($drivers as $driver) { + if ($driver::supports($io, $config, static::getRepoField($repo, $reflectedRepo, 'url'))) { + $driver = new $driver($repo->getRepoConfig(), $io, $config); + try { + $driver->initialize(); + } catch (RuntimeException $e) { + // no-op - this is probably caused due to insufficient permissions when trying to create /var/www/.ssh + // but since we're just getting the driver as a shortcut to getting the repository name, we can ignore this for now. + } + return $driver; + } + } + + foreach ($drivers as $driver) { + if ($driver::supports($io, $config, static::getRepoField($repo, $reflectedRepo, 'url'), true)) { + $driver = new $driver($repo->getRepoConfig(), $io, $config); + try { + $driver->initialize(); + } catch (RuntimeException $e) { + // no-op - this is probably caused due to insufficient permissions when trying to create /var/www/.ssh + // but since we're just getting the driver as a shortcut to getting the repository name, we can ignore this for now. + } + return $driver; + } + } + } + + public static function getSshUrl($driver) + { + $reflectedDriver = new ReflectionObject($driver); + if ($reflectedDriver->hasMethod('generateSshUrl')) { + $reflectedMethod = $reflectedDriver->getMethod('generateSshUrl'); + $reflectedMethod->setAccessible(true); + return $reflectedMethod->invoke($driver); + } + return null; + } + + protected static function getRepoField(VcsRepository $repo, ReflectionObject $reflectedRepo, string $field) + { + $reflectedUrl = $reflectedRepo->getProperty($field); + $reflectedUrl->setAccessible(true); + return $reflectedUrl->getValue($repo); + } +} diff --git a/src/Extensions/ComposerLoaderExtension.php b/src/Extensions/ComposerLoaderExtension.php index 7ce9614..e32b0fa 100644 --- a/src/Extensions/ComposerLoaderExtension.php +++ b/src/Extensions/ComposerLoaderExtension.php @@ -2,6 +2,8 @@ namespace BringYourOwnIdeas\UpdateChecker\Extensions; +use BringYourOwnIdeas\Maintenance\Tasks\UpdatePackageInfoTask; +use BringYourOwnIdeas\UpdateChecker\DriverReflection; use Composer\Composer; use Composer\Factory; use Composer\IO\NullIO; @@ -10,6 +12,9 @@ use Composer\Repository\BaseRepository; use Composer\Repository\CompositeRepository; use Composer\Repository\RepositoryInterface; +use Composer\Repository\RepositoryManager; +use Composer\Repository\Vcs\VcsDriverInterface; +use Composer\Repository\VcsRepository; use SilverStripe\Core\Environment; use SilverStripe\Core\Extension; @@ -120,17 +125,71 @@ public function onAfterBuild() } $originalDir = getcwd(); - - if ($originalDir !== BASE_PATH) { - chdir(BASE_PATH); + chdir(BASE_PATH); + /** @var Composer $composer */ + $composer = Factory::create($io = new NullIO()); + + // Don't include inaccessible repositories. + $inaccessiblePackages = (array)UpdatePackageInfoTask::config()->get('inaccessible_packages'); + $inaccessibleHosts = (array)UpdatePackageInfoTask::config()->get('inaccessible_repository_hosts'); + if (!empty($inaccessiblePackages) || !empty($inaccessibleHosts)) { + $oldManager = $composer->getRepositoryManager(); + $manager = new RepositoryManager( + $io, + $composer->getConfig(), + $composer->getEventDispatcher(), + Factory::createRemoteFilesystem($io, $composer->getConfig()) + ); + $manager->setLocalRepository($oldManager->getLocalRepository()); + foreach ($oldManager->getRepositories() as $repo) { + if ($repo instanceof VcsRepository) { + /** @var VcsDriverInterface $driver */ + $driver = DriverReflection::getDriverWithoutException($repo, $io, $composer->getConfig()); + $sshUrl = DriverReflection::getSshUrl($driver); + $sourceURL = $driver->getUrl(); + $package = $this->findPackageByUrl($sourceURL); + if (!$package && $sshUrl) { + $package = $this->findPackageByUrl($sshUrl); + } + // Don't add the repository if we can confirm it's inaccessible. + // Otherwise the UpdateChecker will attempt to fetch packages using the VcsDriver. + if ( + ($package && in_array($package->name, $inaccessiblePackages)) + || in_array(parse_url($sourceURL, PHP_URL_HOST), $inaccessibleHosts) + || ($sshUrl && in_array(preg_replace('/^([^@]+@)?([^:]+):.*/', '$2', $sshUrl), $inaccessibleHosts)) + ) { + continue; + } + } + $manager->addRepository($repo); + } + $composer->setRepositoryManager($manager); } - /** @var Composer $composer */ - $composer = Factory::create(new NullIO()); $this->setComposer($composer); + chdir($originalDir); + } - if ($originalDir !== BASE_PATH) { - chdir($originalDir); + public function findPackageByUrl(string $url, bool $includeDev = true) + { + $lock = $this->owner->getLock(); + foreach ($lock->packages as $package) { + if (isset($package->source->url) && $package->source->url === $url) { + return $package; + } + if (isset($package->dist->url) && $package->dist->url === $url) { + return $package; + } + } + if ($includeDev) { + foreach ($lock->{'packages-dev'} as $package) { + if (isset($package->source->url) && $package->source->url === $url) { + return $package; + } + if (isset($package->dist->url) && $package->dist->url === $url) { + return $package; + } + } } } }