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" 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/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 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/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/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/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/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 603a26e9c..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"); @@ -815,7 +822,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 +983,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/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/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.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/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; } 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/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/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/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index e0f008d14..19e0efa8a 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) { @@ -784,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/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.'); 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/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', diff --git a/tests/Composer/Test/Autoload/MinimumVersionSupport/.gitignore b/tests/Composer/Test/Autoload/MinimumVersionSupport/.gitignore index 4fbb073c4..c8153b578 100644 --- a/tests/Composer/Test/Autoload/MinimumVersionSupport/.gitignore +++ b/tests/Composer/Test/Autoload/MinimumVersionSupport/.gitignore @@ -1,2 +1,2 @@ -/vendor/ /composer.lock +/vendor/ 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/ShowCommandTest.php b/tests/Composer/Test/Command/ShowCommandTest.php index 0e1bf0ada..c3b719618 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 @@ -397,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 + } } 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/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/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 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', ' 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 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); 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();