From c1f2964105f75d2123783b1a649d6c0260316785 Mon Sep 17 00:00:00 2001 From: Brian French Date: Wed, 19 Jul 2023 08:38:21 -0700 Subject: [PATCH 01/13] Allow executing binaries which are not marked executable via shell proxies (#11557) --- src/Composer/Installer/BinaryInstaller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Installer/BinaryInstaller.php b/src/Composer/Installer/BinaryInstaller.php index ba50fe17f..43254af61 100644 --- a/src/Composer/Installer/BinaryInstaller.php +++ b/src/Composer/Installer/BinaryInstaller.php @@ -401,7 +401,7 @@ if [ -n "\$bashSource" ]; then fi fi -"\${dir}/$binFile" "\$@" +sh "\${dir}/$binFile" "\$@" PROXY; } From e7016b00a9d7f5be76655e82c6968fc289fb9768 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 20 Jul 2023 12:52:28 +0200 Subject: [PATCH 02/13] Upgrade to react/promise 3.0.0 (#10429) --- composer.json | 2 +- composer.lock | 29 ++++++++++--------- phpstan/baseline.neon | 10 +++---- src/Composer/Downloader/ArchiveDownloader.php | 1 + src/Composer/Downloader/DownloadManager.php | 6 ++++ .../Downloader/DownloaderInterface.php | 6 ++++ src/Composer/Downloader/GitDownloader.php | 2 ++ src/Composer/Downloader/SvnDownloader.php | 3 ++ src/Composer/Downloader/VcsDownloader.php | 4 +++ src/Composer/Downloader/ZipDownloader.php | 2 ++ .../Installer/InstallationManager.php | 19 +++++++----- src/Composer/Installer/InstallerInterface.php | 6 ++++ src/Composer/Installer/LibraryInstaller.php | 3 ++ .../Package/Version/VersionGuesser.php | 5 ++-- .../Repository/ComposerRepository.php | 11 ++++++- src/Composer/Util/Filesystem.php | 1 + src/Composer/Util/HttpDownloader.php | 5 +++- src/Composer/Util/Loop.php | 16 +++++----- src/Composer/Util/ProcessExecutor.php | 1 + src/Composer/Util/SyncHelper.php | 1 + .../Test/Downloader/ZipDownloaderTest.php | 4 +-- .../Test/Util/ProcessExecutorTest.php | 1 - 22 files changed, 93 insertions(+), 45 deletions(-) diff --git a/composer.json b/composer.json index 36dbfe22d..70f488506 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ "symfony/filesystem": "^5.4 || ^6.0 || ^7", "symfony/finder": "^5.4 || ^6.0 || ^7", "symfony/process": "^5.4 || ^6.0 || ^7", - "react/promise": "^2.8", + "react/promise": "^2.8 || ^3", "composer/pcre": "^2.1 || ^3.1", "symfony/polyfill-php73": "^1.24", "symfony/polyfill-php80": "^1.24", diff --git a/composer.lock b/composer.lock index 39b48033b..6b3ca48a6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c50c89580fa044b7523cb55c2d557c87", + "content-hash": "4bceaf933dcf6bc05808134e78d21496", "packages": [ { "name": "composer/ca-bundle", @@ -692,23 +692,24 @@ }, { "name": "react/promise", - "version": "v2.10.0", + "version": "v3.0.0", "source": { "type": "git", "url": "https://github.com/reactphp/promise.git", - "reference": "f913fb8cceba1e6644b7b90c4bfb678ed8a3ef38" + "reference": "c86753c76fd3be465d93b308f18d189f01a22be4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/f913fb8cceba1e6644b7b90c4bfb678ed8a3ef38", - "reference": "f913fb8cceba1e6644b7b90c4bfb678ed8a3ef38", + "url": "https://api.github.com/repos/reactphp/promise/zipball/c86753c76fd3be465d93b308f18d189f01a22be4", + "reference": "c86753c76fd3be465d93b308f18d189f01a22be4", "shasum": "" }, "require": { - "php": ">=5.4.0" + "php": ">=7.1.0" }, "require-dev": { - "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.36" + "phpstan/phpstan": "1.10.20 || 1.4.10", + "phpunit/phpunit": "^9.5 || ^7.5" }, "type": "library", "autoload": { @@ -752,7 +753,7 @@ ], "support": { "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v2.10.0" + "source": "https://github.com/reactphp/promise/tree/v3.0.0" }, "funding": [ { @@ -760,7 +761,7 @@ "type": "open_collective" } ], - "time": "2023-05-02T15:15:43+00:00" + "time": "2023-07-11T16:12:49+00:00" }, { "name": "seld/jsonlint", @@ -2034,16 +2035,16 @@ "packages-dev": [ { "name": "phpstan/phpstan", - "version": "1.10.25", + "version": "1.10.26", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "578f4e70d117f9a90699324c555922800ac38d8c" + "reference": "5d660cbb7e1b89253a47147ae44044f49832351f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/578f4e70d117f9a90699324c555922800ac38d8c", - "reference": "578f4e70d117f9a90699324c555922800ac38d8c", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/5d660cbb7e1b89253a47147ae44044f49832351f", + "reference": "5d660cbb7e1b89253a47147ae44044f49832351f", "shasum": "" }, "require": { @@ -2092,7 +2093,7 @@ "type": "tidelift" } ], - "time": "2023-07-06T12:11:37+00:00" + "time": "2023-07-19T12:44:37+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", diff --git a/phpstan/baseline.neon b/phpstan/baseline.neon index 19f0d228f..cad1d17ff 100644 --- a/phpstan/baseline.neon +++ b/phpstan/baseline.neon @@ -1665,6 +1665,11 @@ parameters: count: 3 path: ../src/Composer/Downloader/FileDownloader.php + - + message: "#^Strict comparison using \\=\\=\\= between null and Composer\\\\Util\\\\Http\\\\Response will always evaluate to false\\.$#" + count: 1 + path: ../src/Composer/Downloader/FileDownloader.php + - message: "#^Parameter \\#3 \\$cwd of method Composer\\\\Util\\\\ProcessExecutor\\:\\:execute\\(\\) expects string\\|null, string\\|false given\\.$#" count: 5 @@ -2305,11 +2310,6 @@ parameters: count: 1 path: ../src/Composer/Installer/InstallationManager.php - - - message: "#^Only booleans are allowed in an if condition, React\\\\Promise\\\\PromiseInterface\\|null given\\.$#" - count: 1 - path: ../src/Composer/Installer/InstallationManager.php - - message: "#^Only booleans are allowed in an if condition, Symfony\\\\Component\\\\Console\\\\Helper\\\\ProgressBar\\|null given\\.$#" count: 1 diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index a6648bcdf..6de51ee58 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -216,6 +216,7 @@ abstract class ArchiveDownloader extends FileDownloader * * @param string $file Extracted file * @param string $path Directory + * @phpstan-return PromiseInterface * * @throws \UnexpectedValueException If can not extract downloaded file to path */ diff --git a/src/Composer/Downloader/DownloadManager.php b/src/Composer/Downloader/DownloadManager.php index 7f7ad2c3a..e4adfdf37 100644 --- a/src/Composer/Downloader/DownloadManager.php +++ b/src/Composer/Downloader/DownloadManager.php @@ -174,6 +174,7 @@ class DownloadManager * @param PackageInterface $package package instance * @param string $targetDir target dir * @param PackageInterface|null $prevPackage previous package instance in case of updates + * @phpstan-return PromiseInterface * * @throws \InvalidArgumentException if package have no urls to download from * @throws \RuntimeException @@ -241,6 +242,7 @@ class DownloadManager * @param PackageInterface $package package instance * @param string $targetDir target dir * @param PackageInterface|null $prevPackage previous package instance in case of updates + * @phpstan-return PromiseInterface */ public function prepare(string $type, PackageInterface $package, string $targetDir, ?PackageInterface $prevPackage = null): PromiseInterface { @@ -258,6 +260,7 @@ class DownloadManager * * @param PackageInterface $package package instance * @param string $targetDir target dir + * @phpstan-return PromiseInterface * * @throws \InvalidArgumentException if package have no urls to download from * @throws \RuntimeException @@ -279,6 +282,7 @@ class DownloadManager * @param PackageInterface $initial initial package version * @param PackageInterface $target target package version * @param string $targetDir target dir + * @phpstan-return PromiseInterface * * @throws \InvalidArgumentException if initial package is not installed */ @@ -328,6 +332,7 @@ class DownloadManager * * @param PackageInterface $package package instance * @param string $targetDir target dir + * @phpstan-return PromiseInterface */ public function remove(PackageInterface $package, string $targetDir): PromiseInterface { @@ -347,6 +352,7 @@ class DownloadManager * @param PackageInterface $package package instance * @param string $targetDir target dir * @param PackageInterface|null $prevPackage previous package instance in case of updates + * @phpstan-return PromiseInterface */ public function cleanup(string $type, PackageInterface $package, string $targetDir, ?PackageInterface $prevPackage = null): PromiseInterface { diff --git a/src/Composer/Downloader/DownloaderInterface.php b/src/Composer/Downloader/DownloaderInterface.php index 8e135d725..8cb86cdbb 100644 --- a/src/Composer/Downloader/DownloaderInterface.php +++ b/src/Composer/Downloader/DownloaderInterface.php @@ -34,6 +34,7 @@ interface DownloaderInterface * This should do any network-related tasks to prepare for an upcoming install/update * * @param string $path download path + * @phpstan-return PromiseInterface */ public function download(PackageInterface $package, string $path, ?PackageInterface $prevPackage = null): PromiseInterface; @@ -49,6 +50,7 @@ interface DownloaderInterface * @param PackageInterface $package package instance * @param string $path download path * @param PackageInterface $prevPackage previous package instance in case of an update + * @phpstan-return PromiseInterface */ public function prepare(string $type, PackageInterface $package, string $path, ?PackageInterface $prevPackage = null): PromiseInterface; @@ -57,6 +59,7 @@ interface DownloaderInterface * * @param PackageInterface $package package instance * @param string $path download path + * @phpstan-return PromiseInterface */ public function install(PackageInterface $package, string $path): PromiseInterface; @@ -66,6 +69,7 @@ interface DownloaderInterface * @param PackageInterface $initial initial package * @param PackageInterface $target updated package * @param string $path download path + * @phpstan-return PromiseInterface */ public function update(PackageInterface $initial, PackageInterface $target, string $path): PromiseInterface; @@ -74,6 +78,7 @@ interface DownloaderInterface * * @param PackageInterface $package package instance * @param string $path download path + * @phpstan-return PromiseInterface */ public function remove(PackageInterface $package, string $path): PromiseInterface; @@ -88,6 +93,7 @@ interface DownloaderInterface * @param PackageInterface $package package instance * @param string $path download path * @param PackageInterface $prevPackage previous package instance in case of an update + * @phpstan-return PromiseInterface */ public function cleanup(string $type, PackageInterface $package, string $path, ?PackageInterface $prevPackage = null): PromiseInterface; } diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index 05901a9b2..0840219d0 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -536,6 +536,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface } /** + * @phpstan-return PromiseInterface * @throws \RuntimeException */ protected function discardChanges(string $path): PromiseInterface @@ -551,6 +552,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface } /** + * @phpstan-return PromiseInterface * @throws \RuntimeException */ protected function stashChanges(string $path): PromiseInterface diff --git a/src/Composer/Downloader/SvnDownloader.php b/src/Composer/Downloader/SvnDownloader.php index daa93dfe9..be180d63d 100644 --- a/src/Composer/Downloader/SvnDownloader.php +++ b/src/Composer/Downloader/SvnDownloader.php @@ -230,6 +230,9 @@ class SvnDownloader extends VcsDownloader return "Could not retrieve changes between $fromReference and $toReference due to missing revision information"; } + /** + * @phpstan-return PromiseInterface + */ protected function discardChanges(string $path): PromiseInterface { if (0 !== $this->process->execute('svn revert -R .', $output, $path)) { diff --git a/src/Composer/Downloader/VcsDownloader.php b/src/Composer/Downloader/VcsDownloader.php index 15f3aeccd..a1c5979d5 100644 --- a/src/Composer/Downloader/VcsDownloader.php +++ b/src/Composer/Downloader/VcsDownloader.php @@ -259,6 +259,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa * if false (remove) the changes should be assumed to be lost if the operation is not aborted * * @throws \RuntimeException in case the operation must be aborted + * @phpstan-return PromiseInterface */ protected function cleanChanges(PackageInterface $package, string $path, bool $update): PromiseInterface { @@ -286,6 +287,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa * @param string $path download path * @param string $url package url * @param PackageInterface|null $prevPackage previous package (in case of an update) + * @phpstan-return PromiseInterface */ abstract protected function doDownload(PackageInterface $package, string $path, string $url, ?PackageInterface $prevPackage = null): PromiseInterface; @@ -295,6 +297,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa * @param PackageInterface $package package instance * @param string $path download path * @param string $url package url + * @phpstan-return PromiseInterface */ abstract protected function doInstall(PackageInterface $package, string $path, string $url): PromiseInterface; @@ -305,6 +308,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa * @param PackageInterface $target updated package * @param string $path download path * @param string $url package url + * @phpstan-return PromiseInterface */ abstract protected function doUpdate(PackageInterface $initial, PackageInterface $target, string $path, string $url): PromiseInterface; diff --git a/src/Composer/Downloader/ZipDownloader.php b/src/Composer/Downloader/ZipDownloader.php index 851e70de1..9d0f35353 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -105,6 +105,7 @@ class ZipDownloader extends ArchiveDownloader * * @param string $file File to extract * @param string $path Path where to extract file + * @phpstan-return PromiseInterface */ private function extractWithSystemUnzip(PackageInterface $package, string $file, string $path): PromiseInterface { @@ -194,6 +195,7 @@ class ZipDownloader extends ArchiveDownloader * * @param string $file File to extract * @param string $path Path where to extract file + * @phpstan-return PromiseInterface */ private function extractWithZipArchive(PackageInterface $package, string $file, string $path): PromiseInterface { diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index d171139e9..f92a117ba 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -180,7 +180,7 @@ class InstallationManager */ public function execute(InstalledRepositoryInterface $repo, array $operations, bool $devMode = true, bool $runScripts = true, bool $downloadOnly = false): void { - /** @var array */ + /** @var array> */ $cleanupPromises = []; $signalHandler = SignalHandler::create([SignalHandler::SIGINT, SignalHandler::SIGTERM, SignalHandler::SIGHUP], function (string $signal, SignalHandler $handler) use (&$cleanupPromises) { @@ -237,8 +237,8 @@ class InstallationManager /** * @param OperationInterface[] $operations List of operations to execute in this batch - * @param array $cleanupPromises * @param OperationInterface[] $allOperations Complete list of operations to be executed in the install job, used for event listeners + * @phpstan-param array> $cleanupPromises */ private function downloadAndExecuteBatch(InstalledRepositoryInterface $repo, array $operations, array &$cleanupPromises, bool $devMode, bool $runScripts, bool $downloadOnly, array $allOperations): void { @@ -275,7 +275,7 @@ class InstallationManager if ($opType !== 'uninstall') { $promise = $installer->download($package, $initialPackage); - if ($promise) { + if (null !== $promise) { $promises[] = $promise; } } @@ -322,8 +322,8 @@ class InstallationManager /** * @param OperationInterface[] $operations List of operations to execute in this batch - * @param array $cleanupPromises * @param OperationInterface[] $allOperations Complete list of operations to be executed in the install job, used for event listeners + * @phpstan-param array> $cleanupPromises */ private function executeBatch(InstalledRepositoryInterface $repo, array $operations, array $cleanupPromises, bool $devMode, bool $runScripts, array $allOperations): void { @@ -413,7 +413,7 @@ class InstallationManager } /** - * @param PromiseInterface[] $promises + * @param array> $promises */ private function waitOnPromises(array $promises): void { @@ -440,7 +440,7 @@ class InstallationManager /** * Executes download operation. * - * $param PackageInterface $package + * @phpstan-return PromiseInterface|null */ public function download(PackageInterface $package): ?PromiseInterface { @@ -455,6 +455,7 @@ class InstallationManager * * @param InstalledRepositoryInterface $repo repository in which to check * @param InstallOperation $operation operation instance + * @phpstan-return PromiseInterface|null */ public function install(InstalledRepositoryInterface $repo, InstallOperation $operation): ?PromiseInterface { @@ -471,6 +472,7 @@ class InstallationManager * * @param InstalledRepositoryInterface $repo repository in which to check * @param UpdateOperation $operation operation instance + * @phpstan-return PromiseInterface|null */ public function update(InstalledRepositoryInterface $repo, UpdateOperation $operation): ?PromiseInterface { @@ -509,6 +511,7 @@ class InstallationManager * * @param InstalledRepositoryInterface $repo repository in which to check * @param UninstallOperation $operation operation instance + * @phpstan-return PromiseInterface|null */ public function uninstall(InstalledRepositoryInterface $repo, UninstallOperation $operation): ?PromiseInterface { @@ -638,8 +641,8 @@ class InstallationManager } /** - * @param array $cleanupPromises * @return void + * @phpstan-param array> $cleanupPromises */ private function runCleanup(array $cleanupPromises): void { @@ -648,7 +651,7 @@ class InstallationManager $this->loop->abortJobs(); foreach ($cleanupPromises as $cleanup) { - $promises[] = new \React\Promise\Promise(static function ($resolve, $reject) use ($cleanup): void { + $promises[] = new \React\Promise\Promise(static function ($resolve) use ($cleanup): void { $promise = $cleanup(); if (!$promise instanceof PromiseInterface) { $resolve(); diff --git a/src/Composer/Installer/InstallerInterface.php b/src/Composer/Installer/InstallerInterface.php index bdf42ec42..7c92e91d4 100644 --- a/src/Composer/Installer/InstallerInterface.php +++ b/src/Composer/Installer/InstallerInterface.php @@ -48,6 +48,7 @@ interface InstallerInterface * @param PackageInterface $package package instance * @param PackageInterface $prevPackage previous package instance in case of an update * @return PromiseInterface|null + * @phpstan-return PromiseInterface|null */ public function download(PackageInterface $package, ?PackageInterface $prevPackage = null); @@ -63,6 +64,7 @@ interface InstallerInterface * @param PackageInterface $package package instance * @param PackageInterface $prevPackage previous package instance in case of an update * @return PromiseInterface|null + * @phpstan-return PromiseInterface|null */ public function prepare(string $type, PackageInterface $package, ?PackageInterface $prevPackage = null); @@ -72,6 +74,7 @@ interface InstallerInterface * @param InstalledRepositoryInterface $repo repository in which to check * @param PackageInterface $package package instance * @return PromiseInterface|null + * @phpstan-return PromiseInterface|null */ public function install(InstalledRepositoryInterface $repo, PackageInterface $package); @@ -83,6 +86,7 @@ interface InstallerInterface * @param PackageInterface $target updated version * @throws InvalidArgumentException if $initial package is not installed * @return PromiseInterface|null + * @phpstan-return PromiseInterface|null */ public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target); @@ -92,6 +96,7 @@ interface InstallerInterface * @param InstalledRepositoryInterface $repo repository in which to check * @param PackageInterface $package package instance * @return PromiseInterface|null + * @phpstan-return PromiseInterface|null */ public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package); @@ -106,6 +111,7 @@ interface InstallerInterface * @param PackageInterface $package package instance * @param PackageInterface $prevPackage previous package instance in case of an update * @return PromiseInterface|null + * @phpstan-return PromiseInterface|null */ public function cleanup(string $type, PackageInterface $package, ?PackageInterface $prevPackage = null); diff --git a/src/Composer/Installer/LibraryInstaller.php b/src/Composer/Installer/LibraryInstaller.php index d87c6e3dc..0626fb189 100644 --- a/src/Composer/Installer/LibraryInstaller.php +++ b/src/Composer/Installer/LibraryInstaller.php @@ -272,6 +272,7 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface /** * @return PromiseInterface|null + * @phpstan-return PromiseInterface|null */ protected function installCode(PackageInterface $package) { @@ -282,6 +283,7 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface /** * @return PromiseInterface|null + * @phpstan-return PromiseInterface|null */ protected function updateCode(PackageInterface $initial, PackageInterface $target) { @@ -316,6 +318,7 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface /** * @return PromiseInterface|null + * @phpstan-return PromiseInterface|null */ protected function removeCode(PackageInterface $package) { diff --git a/src/Composer/Package/Version/VersionGuesser.php b/src/Composer/Package/Version/VersionGuesser.php index 46116f0b7..2b2b19706 100644 --- a/src/Composer/Package/Version/VersionGuesser.php +++ b/src/Composer/Package/Version/VersionGuesser.php @@ -322,9 +322,8 @@ class VersionGuesser $prettyVersion = 'dev-' . $candidateVersion; if ($length === 0) { foreach ($promises as $promise) { - if ($promise instanceof CancellablePromiseInterface) { - $promise->cancel(); - } + // to support react/promise 2.x we wrap the promise in a resolve() call for safety + \React\Promise\resolve($promise)->cancel(); } } } diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 0efb02e96..f06fcf5c2 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -1070,6 +1070,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return ['namesFound' => $namesFound, 'packages' => $packages]; } + /** + * @phpstan-return PromiseInterface + */ private function startCachedAsyncDownload(string $fileName, ?string $packageName = null): PromiseInterface { if (null === $this->lazyProvidersUrl) { @@ -1598,6 +1601,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } } + /** + * @phpstan-return PromiseInterface|true> true if the response was a 304 and the cache is fresh, otherwise it returns the decoded json + */ private function asyncFetchFile(string $filename, string $cacheKey, ?string $lastModifiedTime = null): PromiseInterface { if ('' === $filename) { @@ -1610,7 +1616,10 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito if (isset($this->freshMetadataUrls[$filename]) && $lastModifiedTime) { // make it look like we got a 304 response - return \React\Promise\resolve(true); + /** @var PromiseInterface $promise */ + $promise = \React\Promise\resolve(true); + + return $promise; } $httpDownloader = $this->httpDownloader; diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index e0f008d14..d5ec81050 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -133,6 +133,7 @@ class Filesystem * * @throws \RuntimeException * @return PromiseInterface + * @phpstan-return PromiseInterface */ public function removeDirectoryAsync(string $directory) { diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index 58a64b663..e4034d2ae 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -28,7 +28,7 @@ use React\Promise\PromiseInterface; /** * @author Jordi Boggiano * @phpstan-type Request array{url: non-empty-string, options: mixed[], copyTo: string|null} - * @phpstan-type Job array{id: int, status: int, request: Request, sync: bool, origin: string, resolve?: callable, reject?: callable, curl_id?: int, response?: Response, exception?: TransportException} + * @phpstan-type Job array{id: int, status: int, request: Request, sync: bool, origin: string, resolve?: callable, reject?: callable, curl_id?: int, response?: Response, exception?: \Throwable} */ class HttpDownloader { @@ -123,6 +123,7 @@ class HttpDownloader * although not all options are supported when using the default curl downloader * @throws TransportException * @return PromiseInterface + * @phpstan-return PromiseInterface */ public function add(string $url, array $options = []) { @@ -164,6 +165,7 @@ class HttpDownloader * although not all options are supported when using the default curl downloader * @throws TransportException * @return PromiseInterface + * @phpstan-return PromiseInterface */ public function addCopy(string $url, string $to, array $options = []) { @@ -199,6 +201,7 @@ class HttpDownloader /** * @phpstan-param Request $request * @return array{Job, PromiseInterface} + * @phpstan-return array{Job, PromiseInterface} */ private function addJob(array $request, bool $sync = false): array { diff --git a/src/Composer/Util/Loop.php b/src/Composer/Util/Loop.php index a1abed9a5..ca24e69b4 100644 --- a/src/Composer/Util/Loop.php +++ b/src/Composer/Util/Loop.php @@ -25,7 +25,7 @@ class Loop private $httpDownloader; /** @var ProcessExecutor|null */ private $processExecutor; - /** @var PromiseInterface[][] */ + /** @var array>> */ private $currentPromises = []; /** @var int */ private $waitIndex = 0; @@ -52,18 +52,17 @@ class Loop } /** - * @param PromiseInterface[] $promises - * @param ?ProgressBar $progress + * @param array> $promises + * @param ProgressBar|null $progress */ public function wait(array $promises, ?ProgressBar $progress = null): void { - /** @var \Exception|null */ $uncaught = null; \React\Promise\all($promises)->then( static function (): void { }, - static function ($e) use (&$uncaught): void { + static function (\Throwable $e) use (&$uncaught): void { $uncaught = $e; } ); @@ -107,7 +106,7 @@ class Loop } unset($this->currentPromises[$waitIndex]); - if ($uncaught) { + if (null !== $uncaught) { throw $uncaught; } } @@ -116,9 +115,8 @@ class Loop { foreach ($this->currentPromises as $promiseGroup) { foreach ($promiseGroup as $promise) { - if ($promise instanceof CancellablePromiseInterface) { - $promise->cancel(); - } + // to support react/promise 2.x we wrap the promise in a resolve() call for safety + \React\Promise\resolve($promise)->cancel(); } } } diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index a8f69eceb..25e4c903b 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -155,6 +155,7 @@ class ProcessExecutor * * @param string|list $command the command to execute * @param string $cwd the working directory + * @phpstan-return PromiseInterface */ public function executeAsync($command, ?string $cwd = null): PromiseInterface { diff --git a/src/Composer/Util/SyncHelper.php b/src/Composer/Util/SyncHelper.php index ffb51af6d..9a7398cc0 100644 --- a/src/Composer/Util/SyncHelper.php +++ b/src/Composer/Util/SyncHelper.php @@ -58,6 +58,7 @@ class SyncHelper * Waits for a promise to resolve * * @param Loop $loop Loop instance which you can get from $composer->getLoop() + * @phpstan-param PromiseInterface|null $promise */ public static function await(Loop $loop, ?PromiseInterface $promise = null): void { diff --git a/tests/Composer/Test/Downloader/ZipDownloaderTest.php b/tests/Composer/Test/Downloader/ZipDownloaderTest.php index 56aa7a04d..ced6c0992 100644 --- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php @@ -314,7 +314,7 @@ class ZipDownloaderTest extends TestCase } /** - * @param ?\React\Promise\PromiseInterface $promise + * @param ?\React\Promise\PromiseInterface $promise */ private function wait($promise): void { @@ -329,7 +329,7 @@ class ZipDownloaderTest extends TestCase $e = $ex; }); - if ($e) { + if ($e !== null) { throw $e; } } diff --git a/tests/Composer/Test/Util/ProcessExecutorTest.php b/tests/Composer/Test/Util/ProcessExecutorTest.php index c42f7de1d..a8455c7ab 100644 --- a/tests/Composer/Test/Util/ProcessExecutorTest.php +++ b/tests/Composer/Test/Util/ProcessExecutorTest.php @@ -120,7 +120,6 @@ class ProcessExecutorTest extends TestCase $process = new ProcessExecutor($buffer = new BufferIO('', StreamOutput::VERBOSITY_DEBUG)); $process->enableAsync(); $start = microtime(true); - /** @var Promise $promise */ $promise = $process->executeAsync('sleep 2'); $this->assertEquals(1, $process->countActiveJobs()); $promise->cancel(); From 4b210d916e859262f20e68998ec8756d8d0a0b09 Mon Sep 17 00:00:00 2001 From: Athos Ribeiro Date: Fri, 21 Jul 2023 10:39:20 +0200 Subject: [PATCH 03/13] Add support for phpunit 10 (#11532) * Use static test data providers Using non-static methods as a data providers was deprecated in phpunit 10. * Rename abstract test class Abstract test case classes with Test suffix are deprecated in PHPUnit 10. We also change the ArchiverTest file name to match the new class name (ArchiverTestCase). * https://github.com/sebastianbergmann/phpunit/issues/5132 --- tests/Composer/Test/Command/HomeCommandTest.php | 2 +- tests/Composer/Test/Command/SuggestsCommandTest.php | 2 +- tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php | 2 +- .../Package/Archiver/{ArchiverTest.php => ArchiverTestCase.php} | 2 +- tests/Composer/Test/Package/Archiver/PharArchiverTest.php | 2 +- tests/Composer/Test/Package/Archiver/ZipArchiverTest.php | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) rename tests/Composer/Test/Package/Archiver/{ArchiverTest.php => ArchiverTestCase.php} (96%) diff --git a/tests/Composer/Test/Command/HomeCommandTest.php b/tests/Composer/Test/Command/HomeCommandTest.php index 1755255a5..fd37e2f89 100644 --- a/tests/Composer/Test/Command/HomeCommandTest.php +++ b/tests/Composer/Test/Command/HomeCommandTest.php @@ -57,7 +57,7 @@ class HomeCommandTest extends TestCase $this->assertSame(trim($expected), trim($appTester->getDisplay(true))); } - public function useCaseProvider(): Generator + public static function useCaseProvider(): Generator { yield 'Invalid or missing repository URL' => [ [ diff --git a/tests/Composer/Test/Command/SuggestsCommandTest.php b/tests/Composer/Test/Command/SuggestsCommandTest.php index 217f41157..4d64d4bf6 100644 --- a/tests/Composer/Test/Command/SuggestsCommandTest.php +++ b/tests/Composer/Test/Command/SuggestsCommandTest.php @@ -129,7 +129,7 @@ class SuggestsCommandTest extends TestCase self::assertSame(trim($expected), trim($appTester->getDisplay(true))); } - public function provideSuggest(): \Generator + public static function provideSuggest(): \Generator { yield 'with lockfile, show suggested' => [ true, diff --git a/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php b/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php index 1d4a7d4b5..ca07aede4 100644 --- a/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php +++ b/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php @@ -21,7 +21,7 @@ use Composer\Test\Mock\FactoryMock; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; -class ArchiveManagerTest extends ArchiverTest +class ArchiveManagerTest extends ArchiverTestCase { /** * @var ArchiveManager diff --git a/tests/Composer/Test/Package/Archiver/ArchiverTest.php b/tests/Composer/Test/Package/Archiver/ArchiverTestCase.php similarity index 96% rename from tests/Composer/Test/Package/Archiver/ArchiverTest.php rename to tests/Composer/Test/Package/Archiver/ArchiverTestCase.php index 867bf11e7..9e2cb1a3a 100644 --- a/tests/Composer/Test/Package/Archiver/ArchiverTest.php +++ b/tests/Composer/Test/Package/Archiver/ArchiverTestCase.php @@ -17,7 +17,7 @@ use Composer\Util\Filesystem; use Composer\Util\ProcessExecutor; use Composer\Package\CompletePackage; -abstract class ArchiverTest extends TestCase +abstract class ArchiverTestCase extends TestCase { /** * @var \Composer\Util\Filesystem diff --git a/tests/Composer/Test/Package/Archiver/PharArchiverTest.php b/tests/Composer/Test/Package/Archiver/PharArchiverTest.php index 02f779b46..1d6e68e2e 100644 --- a/tests/Composer/Test/Package/Archiver/PharArchiverTest.php +++ b/tests/Composer/Test/Package/Archiver/PharArchiverTest.php @@ -15,7 +15,7 @@ namespace Composer\Test\Package\Archiver; use Composer\Package\Archiver\PharArchiver; use Composer\Util\Platform; -class PharArchiverTest extends ArchiverTest +class PharArchiverTest extends ArchiverTestCase { public function testTarArchive(): void { diff --git a/tests/Composer/Test/Package/Archiver/ZipArchiverTest.php b/tests/Composer/Test/Package/Archiver/ZipArchiverTest.php index c672f4f29..80c7bb95e 100644 --- a/tests/Composer/Test/Package/Archiver/ZipArchiverTest.php +++ b/tests/Composer/Test/Package/Archiver/ZipArchiverTest.php @@ -16,7 +16,7 @@ use Composer\Util\Platform; use ZipArchive; use Composer\Package\Archiver\ZipArchiver; -class ZipArchiverTest extends ArchiverTest +class ZipArchiverTest extends ArchiverTestCase { /** * @dataProvider provideGitignoreExcludeNegationTestCases From ce876e7a6f11c07e72c0a6897d002bcd7eb89f37 Mon Sep 17 00:00:00 2001 From: "Attia A. Ahmed" Date: Fri, 21 Jul 2023 11:58:54 +0300 Subject: [PATCH 04/13] Fix broken junctions leading to installation failure on Windows (#11550) --- src/Composer/Util/Filesystem.php | 6 ++++ tests/Composer/Test/Util/FilesystemTest.php | 33 +++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index d5ec81050..19e0efa8a 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -785,6 +785,12 @@ class Filesystem if (!is_dir($target)) { throw new IOException(sprintf('Cannot junction to "%s" as it is not a directory.', $target), 0, null, $target); } + + // Removing any previously junction to ensure clean execution. + if (!is_dir($junction) || $this->isJunction($junction)) { + @rmdir($junction); + } + $cmd = sprintf( 'mklink /J %s %s', ProcessExecutor::escape(str_replace('/', DIRECTORY_SEPARATOR, $junction)), diff --git a/tests/Composer/Test/Util/FilesystemTest.php b/tests/Composer/Test/Util/FilesystemTest.php index e5ea5b7a9..83a28d75b 100644 --- a/tests/Composer/Test/Util/FilesystemTest.php +++ b/tests/Composer/Test/Util/FilesystemTest.php @@ -12,6 +12,7 @@ namespace Composer\Test\Util; +use Composer\Util\Platform; use Composer\Util\Filesystem; use Composer\Test\TestCase; @@ -317,6 +318,38 @@ class FilesystemTest extends TestCase $this->assertDirectoryDoesNotExist($junction, $junction . ' is not a directory'); } + public function testOverrideJunctions(): void + { + if (!Platform::isWindows()) { + $this->markTestSkipped('Only runs on windows'); + } + + @mkdir($this->workingDir.'/real/nesting/testing', 0777, true); + $fs = new Filesystem(); + + $old_target = $this->workingDir.'/real/nesting/testing'; + $target = $this->workingDir.'/real/../real/nesting'; + $junction = $this->workingDir.'/junction'; + + // Override non-broken junction + $fs->junction($old_target, $junction); + $fs->junction($target, $junction); + + $this->assertTrue($fs->isJunction($junction), $junction.': is a junction'); + $this->assertTrue($fs->isJunction($target.'/../../junction'), $target.'/../../junction: is a junction'); + + //Remove junction + $this->assertTrue($fs->removeJunction($junction), $junction . ' has been removed'); + + // Override broken junction + $fs->junction($old_target, $junction); + $fs->removeDirectory($old_target); + $fs->junction($target, $junction); + + $this->assertTrue($fs->isJunction($junction), $junction.': is a junction'); + $this->assertTrue($fs->isJunction($target.'/../../junction'), $target.'/../../junction: is a junction'); + } + public function testCopy(): void { @mkdir($this->workingDir . '/foo/bar', 0777, true); From 8f49166ec6bc3512404b3be49551ab2da3b50971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Z=C3=BClke?= Date: Fri, 21 Jul 2023 11:06:03 +0200 Subject: [PATCH 05/13] Fix lib-curl-openssl parsing for SecureTransport (#11534) On macOS, if libcurl is built against SecureTransport, the platform repository will contain an invalid package name: % composer show --platform | grep curl ext-curl 8.2.7 The curl PHP extension lib-curl 8.1.2 The curl library lib-curl-(securetransport) openssl 3.1.1 curl (securetransport) openss... lib-curl-libssh2 1.11.0 curl libssh2 version lib-curl-zlib 1.2.11 curl zlib version This change fixes it: % bin/composer show --platform | grep curl lib-curl 8.1.2 The curl library lib-curl-libssh2 1.11.0 curl libssh2 version lib-curl-securetransport 3.1.1 curl (securetransport) openssl ... lib-curl-zlib 1.2.11 curl zlib version (second column width difference comes from the Composer dev version number) --- .../Repository/PlatformRepository.php | 7 ++- .../Repository/PlatformRepositoryTest.php | 52 ++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/Composer/Repository/PlatformRepository.php b/src/Composer/Repository/PlatformRepository.php index 43cc4eb98..9ab0f6c6b 100644 --- a/src/Composer/Repository/PlatformRepository.php +++ b/src/Composer/Repository/PlatformRepository.php @@ -238,7 +238,12 @@ class PlatformRepository extends ArrayRepository $parsedVersion = Version::parseOpenssl($sslMatches['version'], $isFips); $this->addLibrary($name.'-openssl'.($isFips ? '-fips' : ''), $parsedVersion, 'curl OpenSSL version ('.$parsedVersion.')', [], $isFips ? ['curl-openssl'] : []); } else { - $this->addLibrary($name.'-'.$library, $sslMatches['version'], 'curl '.$library.' version ('.$sslMatches['version'].')', ['curl-openssl']); + if ($library === '(securetransport) openssl') { + $shortlib = 'securetransport'; + } else { + $shortlib = $library; + } + $this->addLibrary($name.'-'.$shortlib, $sslMatches['version'], 'curl '.$library.' version ('.$sslMatches['version'].')', ['curl-openssl']); } } diff --git a/tests/Composer/Test/Repository/PlatformRepositoryTest.php b/tests/Composer/Test/Repository/PlatformRepositoryTest.php index 0d897003e..9e791a776 100644 --- a/tests/Composer/Test/Repository/PlatformRepositoryTest.php +++ b/tests/Composer/Test/Repository/PlatformRepositoryTest.php @@ -369,7 +369,6 @@ libSSH Version => libssh2/1.4.1', 'curl: libssh not libssh2' => [ 'curl', ' - curl cURL support => enabled @@ -412,6 +411,57 @@ libSSH Version => libssh/0.9.3/openssl/zlib', ], [['curl_version', [], ['version' => '7.68.0']]], ], + 'curl: SecureTransport' => [ + 'curl', + ' +curl + +cURL support => enabled +cURL Information => 8.1.2 +Age => 10 +Features +AsynchDNS => Yes +CharConv => No +Debug => No +GSS-Negotiate => No +IDN => Yes +IPv6 => Yes +krb4 => No +Largefile => Yes +libz => Yes +NTLM => Yes +NTLMWB => Yes +SPNEGO => Yes +SSL => Yes +SSPI => No +TLS-SRP => Yes +HTTP2 => Yes +GSSAPI => Yes +KERBEROS5 => Yes +UNIX_SOCKETS => Yes +PSL => No +HTTPS_PROXY => Yes +MULTI_SSL => Yes +BROTLI => Yes +ALTSVC => Yes +HTTP3 => No +UNICODE => No +ZSTD => Yes +HSTS => Yes +GSASL => No +Protocols => dict, file, ftp, ftps, gopher, gophers, http, https, imap, imaps, ldap, ldaps, mqtt, pop3, pop3s, rtmp, rtmpe, rtmps, rtmpt, rtmpte, rtmpts, rtsp, scp, sftp, smb, smbs, smtp, smtps, telnet, tftp +Host => aarch64-apple-darwin22.4.0 +SSL Version => (SecureTransport) OpenSSL/3.1.1 +ZLib Version => 1.2.11 +libSSH Version => libssh2/1.11.0', + [ + 'lib-curl' => '8.1.2', + 'lib-curl-securetransport' => ['3.1.1', ['lib-curl-openssl']], + 'lib-curl-zlib' => '1.2.11', + 'lib-curl-libssh2' => '1.11.0', + ], + [['curl_version', [], ['version' => '8.1.2']]], + ], 'date' => [ 'date', ' From cf8ea3c70e90146d3c6e0d7bb51101ac46484d1b Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 21 Jul 2023 10:09:32 +0100 Subject: [PATCH 06/13] GitHubDriver: better handle empty composer.json file (#11552) --- src/Composer/Repository/Vcs/GitHubDriver.php | 2 +- .../Test/Repository/Vcs/GitHubDriverTest.php | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php index 122b03fa7..44766a185 100644 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -310,7 +310,7 @@ class GitHubDriver extends VcsDriver $resource = $this->getContents($resource['git_url'])->decodeJson(); } - if (empty($resource['content']) || $resource['encoding'] !== 'base64' || !($content = base64_decode($resource['content']))) { + if (!isset($resource['content']) || $resource['encoding'] !== 'base64' || false === ($content = base64_decode($resource['content']))) { throw new \RuntimeException('Could not retrieve ' . $file . ' for '.$identifier); } diff --git a/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php index 8431a1b49..aa454e894 100644 --- a/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php @@ -401,6 +401,34 @@ class GitHubDriverTest extends TestCase ]; } + public function testGetEmptyFileContent(): void + { + $repoUrl = 'http://github.com/composer/packagist'; + + $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $io->expects($this->any()) + ->method('isInteractive') + ->will($this->returnValue(true)); + + $httpDownloader = $this->getHttpDownloaderMock($io, $this->config); + $httpDownloader->expects( + [ + ['url' => 'https://api.github.com/repos/composer/packagist', 'body' => '{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist", "archived": true}'], + ['url' => 'https://api.github.com/repos/composer/packagist/contents/composer.json?ref=main', 'body' => '{"encoding":"base64","content":""}'], + ], + true + ); + + $repoConfig = [ + 'url' => $repoUrl, + ]; + + $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, $httpDownloader, $this->getProcessExecutorMock()); + $gitHubDriver->initialize(); + + $this->assertSame('', $gitHubDriver->getFileContent('composer.json', 'main')); + } + /** * @param string|object $object * @param mixed $value From 9eb9e0dcb38f725629869dbed24aca78a647d1eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=B6ller?= Date: Fri, 21 Jul 2023 11:10:15 +0200 Subject: [PATCH 07/13] Fix: Require tests on PHP 8.2 to pass (#11554) --- .github/workflows/continuous-integration.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 1e3f99b46..82dbc8d22 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -30,6 +30,7 @@ jobs: - "7.4" - "8.0" - "8.1" + - "8.2" dependencies: [locked] os: [ubuntu-latest] experimental: [false] @@ -57,11 +58,11 @@ jobs: - php-version: "8.2" dependencies: lowest-ignore os: ubuntu-latest - experimental: true + experimental: false - php-version: "8.2" dependencies: highest-ignore os: ubuntu-latest - experimental: true + experimental: false steps: - name: "Checkout" From 07f706e57de283d152bfba1af0dc25b660cffc5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Z=C3=BClke?= Date: Fri, 21 Jul 2023 11:28:36 +0200 Subject: [PATCH 08/13] Fix 'composer show --platform ' erroring if no composer.json is present (#11533) Sort of related to #11046 (although this is not a regression, but didn't work before, either) --- src/Composer/Command/ShowCommand.php | 4 ++-- tests/Composer/Test/Command/ShowCommandTest.php | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 603a26e9c..50fd058aa 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -815,7 +815,7 @@ EOT $io->write('homepage : ' . $package->getHomepage()); $io->write('source : ' . sprintf('[%s] %s %s', $package->getSourceType(), $package->getSourceUrl(), $package->getSourceReference())); $io->write('dist : ' . sprintf('[%s] %s %s', $package->getDistType(), $package->getDistUrl(), $package->getDistReference())); - if ($installedRepo->hasPackage($package)) { + if (!PlatformRepository::isPlatformPackage($package->getName()) && $installedRepo->hasPackage($package)) { $path = $this->requireComposer()->getInstallationManager()->getInstallPath($package); if (is_string($path)) { $io->write('path : ' . realpath($path)); @@ -976,7 +976,7 @@ EOT ]; } - if ($installedRepo->hasPackage($package)) { + if (!PlatformRepository::isPlatformPackage($package->getName()) && $installedRepo->hasPackage($package)) { $path = $this->requireComposer()->getInstallationManager()->getInstallPath($package); if (is_string($path)) { $path = realpath($path); diff --git a/tests/Composer/Test/Command/ShowCommandTest.php b/tests/Composer/Test/Command/ShowCommandTest.php index 0e1bf0ada..4dbf25872 100644 --- a/tests/Composer/Test/Command/ShowCommandTest.php +++ b/tests/Composer/Test/Command/ShowCommandTest.php @@ -288,12 +288,19 @@ vendor/package 1.1.0 ! 1.2.0", trim($appTester->getDispla unlink('./composer.json'); unlink('./auth.json'); + // listing packages $appTester = $this->getApplicationTester(); $appTester->run(['command' => 'show', '-p' => true]); $output = trim($appTester->getDisplay(true)); foreach (Regex::matchAll('{^(\w+)}m', $output)->matches as $m) { self::assertTrue(PlatformRepository::isPlatformPackage((string) $m[1])); } + + // getting a single package + $appTester->run(['command' => 'show', '-p' => true, 'package' => 'php']); + $appTester->assertCommandIsSuccessful(); + $appTester->run(['command' => 'show', '-p' => true, '-f' => 'json', 'package' => 'php']); + $appTester->assertCommandIsSuccessful(); } public function testOutdatedWithZeroMajor(): void From 3e9c148b63aeeb089e155ef5c1e070fb15eda00d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Z=C3=BClke?= Date: Fri, 21 Jul 2023 11:29:38 +0200 Subject: [PATCH 09/13] Fix trailing whitespace in 'composer show -N' (#11536) The name column was always padded to maximum width, even if no other columns were printed. This makes it difficult to use the output e.g. in pipelines. Fixed for all possible columns, and with tests for two cases (regular show and show outdated). --- src/Composer/Command/ShowCommand.php | 21 ++++++---- .../Composer/Test/Command/ShowCommandTest.php | 41 +++++++++++++++++++ 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 50fd058aa..ea5a9db3b 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -442,6 +442,10 @@ EOT $exitCode = 0; $viewData = []; $viewMetaData = []; + + $writeVersion = false; + $writeDescription = false; + foreach (['platform' => true, 'locked' => true, 'available' => false, 'installed' => true] as $type => $showVersion) { if (isset($packages[$type])) { ksort($packages[$type]); @@ -616,14 +620,14 @@ EOT $io->writeError(''); $io->writeError('Direct dependencies required in composer.json:'); if (\count($directDeps) > 0) { - $this->printPackages($io, $directDeps, $indent, $versionFits, $latestFits, $descriptionFits, $width, $versionLength, $nameLength, $latestLength); + $this->printPackages($io, $directDeps, $indent, $writeVersion && $versionFits, $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength); } else { $io->writeError('Everything up to date'); } $io->writeError(''); $io->writeError('Transitive dependencies not required in composer.json:'); if (\count($transitiveDeps) > 0) { - $this->printPackages($io, $transitiveDeps, $indent, $versionFits, $latestFits, $descriptionFits, $width, $versionLength, $nameLength, $latestLength); + $this->printPackages($io, $transitiveDeps, $indent, $writeVersion && $versionFits, $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength); } else { $io->writeError('Everything up to date'); } @@ -631,7 +635,7 @@ EOT if ($writeLatest && \count($packages) === 0) { $io->writeError('All your direct dependencies are up to date'); } else { - $this->printPackages($io, $packages, $indent, $versionFits, $latestFits, $descriptionFits, $width, $versionLength, $nameLength, $latestLength); + $this->printPackages($io, $packages, $indent, $writeVersion && $versionFits, $writeLatest && $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength); } } @@ -649,15 +653,18 @@ EOT */ private function printPackages(IOInterface $io, array $packages, string $indent, bool $writeVersion, bool $writeLatest, bool $writeDescription, int $width, int $versionLength, int $nameLength, int $latestLength): void { + $padName = $writeVersion || $writeLatest || $writeDescription; + $padVersion = $writeLatest || $writeDescription; + $padLatest = $writeDescription; foreach ($packages as $package) { $link = $package['source'] ?? $package['homepage'] ?? ''; if ($link !== '') { - $io->write($indent . ''.$package['name'].''. str_repeat(' ', $nameLength - strlen($package['name'])), false); + $io->write($indent . ''.$package['name'].''. str_repeat(' ', ($padName ? $nameLength - strlen($package['name']) : 0)), false); } else { - $io->write($indent . str_pad($package['name'], $nameLength, ' '), false); + $io->write($indent . str_pad($package['name'], ($padName ? $nameLength : 0), ' '), false); } if (isset($package['version']) && $writeVersion) { - $io->write(' ' . str_pad($package['version'], $versionLength, ' '), false); + $io->write(' ' . str_pad($package['version'], ($padVersion ? $versionLength : 0), ' '), false); } if (isset($package['latest']) && isset($package['latest-status']) && $writeLatest) { $latestVersion = $package['latest']; @@ -666,7 +673,7 @@ EOT if (!$io->isDecorated()) { $latestVersion = str_replace(['up-to-date', 'semver-safe-update', 'update-possible'], ['=', '!', '~'], $updateStatus) . ' ' . $latestVersion; } - $io->write(' <' . $style . '>' . str_pad($latestVersion, $latestLength, ' ') . '', false); + $io->write(' <' . $style . '>' . str_pad($latestVersion, ($padLatest ? $latestLength : 0), ' ') . '', false); } if (isset($package['description']) && $writeDescription) { $description = strtok($package['description'], "\r\n"); diff --git a/tests/Composer/Test/Command/ShowCommandTest.php b/tests/Composer/Test/Command/ShowCommandTest.php index 4dbf25872..c3b719618 100644 --- a/tests/Composer/Test/Command/ShowCommandTest.php +++ b/tests/Composer/Test/Command/ShowCommandTest.php @@ -404,4 +404,45 @@ available: installed: vendor/installed 2.0.0 description of installed package', $output); } + + public function testNameOnlyPrintsNoTrailingWhitespace(): void + { + $this->initTempComposer([ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + // CAUTION: package names matter - output is sorted, and we want shorter before longer ones + ['name' => 'vendor/apackage', 'description' => 'generic description', 'version' => '1.0.0'], + ['name' => 'vendor/apackage', 'description' => 'generic description', 'version' => '1.1.0'], + ['name' => 'vendor/longpackagename', 'description' => 'generic description', 'version' => '1.0.0'], + ['name' => 'vendor/longpackagename', 'description' => 'generic description', 'version' => '1.1.0'], + ['name' => 'vendor/somepackage', 'description' => 'generic description', 'version' => '1.0.0'], + ], + ], + ], + ]); + + $this->createInstalledJson([ + self::getPackage('vendor/apackage', '1.0.0'), + self::getPackage('vendor/longpackagename', '1.0.0'), + self::getPackage('vendor/somepackage', '1.0.0'), + ]); + + $appTester = $this->getApplicationTester(); + $appTester->run(['command' => 'show', '-N' => true]); + self::assertSame( +'vendor/apackage +vendor/longpackagename +vendor/somepackage', trim($appTester->getDisplay(true))); // trim() is fine here, but see CAUTION above + + $appTester = $this->getApplicationTester(); + $appTester->run(['command' => 'show', '--outdated' => true, '-N' => true]); + self::assertSame( +'Legend: +! patch or minor release available - update recommended +~ major release available - update possible +vendor/apackage +vendor/longpackagename', trim($appTester->getDisplay(true))); // trim() is fine here, but see CAUTION above + } } From a6c7b0b32753b34abb246d00ed0985b6b88ce064 Mon Sep 17 00:00:00 2001 From: Julian Liebig Date: Fri, 21 Jul 2023 14:29:56 +0200 Subject: [PATCH 10/13] Retry download if curl error 55 is encountered (#11543) --- src/Composer/Util/Http/CurlDownloader.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index edc668a60..7bc9034d6 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -357,6 +357,13 @@ class CurlDownloader continue; } + // TODO: Remove this as soon as https://github.com/curl/curl/issues/10591 is resolved + if ($errno === 55 /* CURLE_SEND_ERROR */) { + $this->io->writeError('Retrying ('.($job['attributes']['retries'] + 1).') ' . Url::sanitize($job['url']) . ' due to curl error '. $errno, true, IOInterface::DEBUG); + $this->restartJobWithDelay($job, $job['url'], ['retries' => $job['attributes']['retries'] + 1]); + continue; + } + if ($errno === 28 /* CURLE_OPERATION_TIMEDOUT */ && PHP_VERSION_ID >= 70300 && $progress['namelookup_time'] === 0.0 && !$timeoutWarning) { $timeoutWarning = true; $this->io->writeError('A connection timeout was encountered. If you intend to run Composer without connecting to the internet, run the command again prefixed with COMPOSER_DISABLE_NETWORK=1 to make Composer run in offline mode.'); From 7f78decad7202c1942090bd6f896c98478fbd984 Mon Sep 17 00:00:00 2001 From: Dan Wallis Date: Fri, 21 Jul 2023 13:34:59 +0100 Subject: [PATCH 11/13] Fix PHPStan after running autoloader tests (#11558) --- phpstan/config.neon | 1 + tests/Composer/Test/Autoload/MinimumVersionSupport/.gitignore | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 tests/Composer/Test/Autoload/MinimumVersionSupport/.gitignore diff --git a/phpstan/config.neon b/phpstan/config.neon index f37049ab4..6ffdbe68e 100644 --- a/phpstan/config.neon +++ b/phpstan/config.neon @@ -15,6 +15,7 @@ parameters: excludePaths: - '../tests/Composer/Test/Fixtures/*' - '../tests/Composer/Test/Autoload/Fixtures/*' + - '../tests/Composer/Test/Autoload/MinimumVersionSupport/vendor/' - '../tests/Composer/Test/Plugin/Fixtures/*' - '../tests/Composer/Test/PolyfillTestCase.php' diff --git a/tests/Composer/Test/Autoload/MinimumVersionSupport/.gitignore b/tests/Composer/Test/Autoload/MinimumVersionSupport/.gitignore new file mode 100644 index 000000000..c8153b578 --- /dev/null +++ b/tests/Composer/Test/Autoload/MinimumVersionSupport/.gitignore @@ -0,0 +1,2 @@ +/composer.lock +/vendor/ From 0cdabcc4ee3a4daf43db746acf9b041ab9214c0b Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 21 Jul 2023 14:36:38 +0200 Subject: [PATCH 12/13] Add audit.ignored config setting to ignore security advisories by id or CVE id, fixes #11298 (#11556) --- doc/06-config.md | 18 +++++++++ res/composer-schema.json | 13 ++++++ src/Composer/Advisory/Auditor.php | 42 +++++++++++++++++++- src/Composer/Command/AuditCommand.php | 2 +- src/Composer/Command/ConfigCommand.php | 19 +++++++++ src/Composer/Config.php | 6 +++ src/Composer/Installer.php | 2 +- tests/Composer/Test/Advisory/AuditorTest.php | 38 ++++++++++++++++-- 8 files changed, 133 insertions(+), 7 deletions(-) diff --git a/doc/06-config.md b/doc/06-config.md index 70caf4432..615fd0d28 100644 --- a/doc/06-config.md +++ b/doc/06-config.md @@ -101,6 +101,24 @@ optionally be an object with package name patterns for keys for more granular in > configuration in global and package configurations the string notation > is translated to a `*` package pattern. +## audit + +Security audit configuration options + +### ignored + +A set of advisory ids, remote ids or CVE ids that should be ignored and not reported as part of an audit. + +```json +{ + "config": { + "audit": { + "ignored": ["CVE-1234", "GHSA-xx", "PKSA-yy"] + } + } +} +``` + ## use-parent-dir When running Composer in a directory where there is no composer.json, if there diff --git a/res/composer-schema.json b/res/composer-schema.json index 8e8c65691..9757c8950 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -325,6 +325,19 @@ "type": ["string"] } }, + "audit": { + "type": "object", + "description": "Security audit configuration options", + "properties": { + "ignored": { + "type": "array", + "description": "A set of advisory ids, remote ids or CVE ids that should be ignored and not reported as part of an audit.", + "items": { + "type": "string" + } + } + } + }, "notify-on-install": { "type": "boolean", "description": "Composer allows repositories to define a notification URL, so that they get notified whenever a package from that repository is installed. This option allows you to disable that behaviour, defaults to true." diff --git a/src/Composer/Advisory/Auditor.php b/src/Composer/Advisory/Auditor.php index 9ddb5b04b..c48f54d47 100644 --- a/src/Composer/Advisory/Auditor.php +++ b/src/Composer/Advisory/Auditor.php @@ -44,12 +44,18 @@ class Auditor * @param PackageInterface[] $packages * @param self::FORMAT_* $format The format that will be used to output audit results. * @param bool $warningOnly If true, outputs a warning. If false, outputs an error. + * @param string[] $ignoredIds Ignored advisory IDs, remote IDs or CVE IDs * @return int Amount of packages with vulnerabilities found * @throws InvalidArgumentException If no packages are passed in */ - public function audit(IOInterface $io, RepositorySet $repoSet, array $packages, string $format, bool $warningOnly = true): int + public function audit(IOInterface $io, RepositorySet $repoSet, array $packages, string $format, bool $warningOnly = true, array $ignoredIds = []): int { $advisories = $repoSet->getMatchingSecurityAdvisories($packages, $format === self::FORMAT_SUMMARY); + + if (\count($ignoredIds) > 0) { + $advisories = $this->filterIgnoredAdvisories($advisories, $ignoredIds); + } + if (self::FORMAT_JSON === $format) { $io->write(JsonFile::encode(['advisories' => $advisories])); @@ -73,6 +79,40 @@ class Auditor return 0; } + /** + * @phpstan-param array> $advisories + * @param array $ignoredIds + * @phpstan-return array> + */ + private function filterIgnoredAdvisories(array $advisories, array $ignoredIds): array + { + foreach ($advisories as $package => $pkgAdvisories) { + $advisories[$package] = array_filter($pkgAdvisories, static function (PartialSecurityAdvisory $advisory) use ($ignoredIds) { + if (in_array($advisory->advisoryId, $ignoredIds, true)) { + return false; + } + if ($advisory instanceof SecurityAdvisory) { + if (in_array($advisory->cve, $ignoredIds, true)) { + return false; + } + + foreach ($advisory->sources as $source) { + if (in_array($source['remoteId'], $ignoredIds, true)) { + return false; + } + } + } + + return true; + }); + if (\count($advisories[$package]) === 0) { + unset($advisories[$package]); + } + } + + return $advisories; + } + /** * @param array> $advisories * @return array{int, int} Count of affected packages and total count of advisories diff --git a/src/Composer/Command/AuditCommand.php b/src/Composer/Command/AuditCommand.php index 5a91a1743..ed97bbc94 100644 --- a/src/Composer/Command/AuditCommand.php +++ b/src/Composer/Command/AuditCommand.php @@ -63,7 +63,7 @@ EOT $repoSet->addRepository($repo); } - return min(255, $auditor->audit($this->getIO(), $repoSet, $packages, $this->getAuditFormat($input, 'format'), false)); + return min(255, $auditor->audit($this->getIO(), $repoSet, $packages, $this->getAuditFormat($input, 'format'), false, $composer->getConfig()->get('audit')['ignored'] ?? [])); } /** diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index bef163b3d..1a1e0bb48 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -556,8 +556,27 @@ EOT return $vals; }, ], + 'audit.ignore' => [ + static function ($vals) { + if (!is_array($vals)) { + return 'array expected'; + } + + return true; + }, + static function ($vals) { + return $vals; + }, + ], ]; + // allow unsetting audit config entirely + if ($input->getOption('unset') && $settingKey === 'audit') { + $this->configSource->removeConfigSetting($settingKey); + + return 0; + } + if ($input->getOption('unset') && (isset($uniqueConfigValues[$settingKey]) || isset($multiConfigValues[$settingKey]))) { if ($settingKey === 'disable-tls' && $this->config->get('disable-tls')) { $this->getIO()->writeError('You are now running Composer with SSL/TLS protection enabled.'); diff --git a/src/Composer/Config.php b/src/Composer/Config.php index 4555203f1..c5e1f355d 100644 --- a/src/Composer/Config.php +++ b/src/Composer/Config.php @@ -37,6 +37,7 @@ class Config 'allow-plugins' => [], 'use-parent-dir' => 'prompt', 'preferred-install' => 'dist', + 'audit' => ['ignored' => []], 'notify-on-install' => true, 'github-protocols' => ['https', 'ssh', 'git'], 'gitlab-protocol' => null, @@ -207,6 +208,11 @@ class Config $this->config[$key] = $val; $this->setSourceOfConfigValue($val, $key, $source); } + } elseif ('audit' === $key) { + $currentIgnores = $this->config['audit']['ignored']; + $this->config[$key] = $val; + $this->setSourceOfConfigValue($val, $key, $source); + $this->config['audit']['ignored'] = array_merge($currentIgnores, $val['ignored']); } else { $this->config[$key] = $val; $this->setSourceOfConfigValue($val, $key, $source); diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 205648f03..310507a53 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -402,7 +402,7 @@ class Installer $repoSet->addRepository($repo); } - return $auditor->audit($this->io, $repoSet, $packages, $this->auditFormat) > 0 ? self::ERROR_AUDIT_FAILED : 0; + return $auditor->audit($this->io, $repoSet, $packages, $this->auditFormat, true, $this->config->get('audit')['ignored'] ?? []) > 0 ? self::ERROR_AUDIT_FAILED : 0; } catch (TransportException $e) { $this->io->error('Failed to audit '.$target.' packages.'); if ($this->io->isVerbose()) { diff --git a/tests/Composer/Test/Advisory/AuditorTest.php b/tests/Composer/Test/Advisory/AuditorTest.php index b4a229e9a..ca87e8e9a 100644 --- a/tests/Composer/Test/Advisory/AuditorTest.php +++ b/tests/Composer/Test/Advisory/AuditorTest.php @@ -14,6 +14,7 @@ namespace Composer\Test\Advisory; use Composer\Advisory\PartialSecurityAdvisory; use Composer\Advisory\SecurityAdvisory; +use Composer\IO\BufferIO; use Composer\IO\NullIO; use Composer\Package\Package; use Composer\Package\Version\VersionParser; @@ -71,6 +72,35 @@ class AuditorTest extends TestCase $this->assertSame($expected, $result, $message); } + public function testAuditIgnoredIDs(): void + { + $packages = [ + new Package('vendor1/package1', '3.0.0.0', '3.0.0'), + new Package('vendor1/package2', '3.0.0.0', '3.0.0'), + new Package('vendorx/packagex', '3.0.0.0', '3.0.0'), + new Package('vendor3/package1', '3.0.0.0', '3.0.0'), + ]; + + $ignoredIds = ['CVE1', 'ID2', 'RemoteIDx']; + + $auditor = new Auditor(); + $result = $auditor->audit($io = $this->getIOMock(), $this->getRepoSet(), $packages, Auditor::FORMAT_PLAIN, false, $ignoredIds); + $io->expects([ + ['text' => 'Found 1 security vulnerability advisory affecting 1 package:'], + ['text' => 'Package: vendor3/package1'], + ['text' => 'CVE: CVE5'], + ['text' => 'Title: advisory7'], + ['text' => 'URL: https://advisory.example.com/advisory7'], + ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'], + ['text' => 'Reported at: 2015-05-25T13:21:00+00:00'], + ], true); + $this->assertSame(1, $result); + + // without ignored IDs, we should get all 4 + $result = $auditor->audit($io, $this->getRepoSet(), $packages, Auditor::FORMAT_PLAIN, false); + $this->assertSame(4, $result); + } + private function getRepoSet(): RepositorySet { $repo = $this @@ -160,7 +190,7 @@ class AuditorTest extends TestCase 'sources' => [ [ 'name' => 'source2', - 'remoteId' => 'RemoteID2', + 'remoteId' => 'RemoteID4', ], ], 'reportedAt' => '2022-05-25 13:21:00', @@ -205,14 +235,14 @@ class AuditorTest extends TestCase [ 'advisoryId' => 'IDx', 'packageName' => 'vendorx/packagex', - 'title' => 'advisory7', - 'link' => 'https://advisory.example.com/advisory7', + 'title' => 'advisory17', + 'link' => 'https://advisory.example.com/advisory17', 'cve' => 'CVE5', 'affectedVersions' => '>=3,<3.4.3|>=1,<2.5.6', 'sources' => [ [ 'name' => 'source2', - 'remoteId' => 'RemoteID4', + 'remoteId' => 'RemoteIDx', ], ], 'reportedAt' => '2015-05-25 13:21:00', From f752a9e3584de3f559d2fc41c6bd9a6d2daf5c44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Fri, 21 Jul 2023 14:47:54 +0200 Subject: [PATCH 13/13] Unify wording in docs (#11545) --- doc/03-cli.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/03-cli.md b/doc/03-cli.md index d11265782..f56a376d9 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -968,7 +968,7 @@ performance. * **--ignore-platform-req:** ignore a specific platform requirement (`php`, `hhvm`, `lib-*` and `ext-*`) and skip the [platform check](07-runtime.md#platform-check) for it. Multiple requirements can be ignored via wildcard. -* **--strict-psr:** Return a failed status code (1) if PSR-4 or PSR-0 mapping errors +* **--strict-psr:** Return a failed exit code (1) if PSR-4 or PSR-0 mapping errors are present. Requires --optimize to work. ## clear-cache / clearcache / cc