diff --git a/composer.lock b/composer.lock index 82527b30a..bd2f57c9c 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "composer/ca-bundle", - "version": "1.3.7", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "76e46335014860eec1aa5a724799a00a2e47cc85" + "reference": "b66d11b7479109ab547f9405b97205640b17d385" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/76e46335014860eec1aa5a724799a00a2e47cc85", - "reference": "76e46335014860eec1aa5a724799a00a2e47cc85", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/b66d11b7479109ab547f9405b97205640b17d385", + "reference": "b66d11b7479109ab547f9405b97205640b17d385", "shasum": "" }, "require": { @@ -29,7 +29,7 @@ "phpstan/phpstan": "^0.12.55", "psr/log": "^1.0", "symfony/phpunit-bridge": "^4.2 || ^5", - "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0" + "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" }, "type": "library", "extra": { @@ -64,7 +64,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.3.7" + "source": "https://github.com/composer/ca-bundle/tree/1.4.0" }, "funding": [ { @@ -80,7 +80,7 @@ "type": "tidelift" } ], - "time": "2023-08-30T09:31:38+00:00" + "time": "2023-12-18T12:05:55+00:00" }, { "name": "composer/class-map-generator", @@ -765,16 +765,16 @@ }, { "name": "seld/jsonlint", - "version": "1.10.0", + "version": "1.10.1", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "594fd6462aad8ecee0b45ca5045acea4776667f1" + "reference": "76d449a358ece77d6f1d6331c68453e657172202" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/594fd6462aad8ecee0b45ca5045acea4776667f1", - "reference": "594fd6462aad8ecee0b45ca5045acea4776667f1", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/76d449a358ece77d6f1d6331c68453e657172202", + "reference": "76d449a358ece77d6f1d6331c68453e657172202", "shasum": "" }, "require": { @@ -801,7 +801,7 @@ { "name": "Jordi Boggiano", "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" + "homepage": "https://seld.be" } ], "description": "JSON Linter", @@ -813,7 +813,7 @@ ], "support": { "issues": "https://github.com/Seldaek/jsonlint/issues", - "source": "https://github.com/Seldaek/jsonlint/tree/1.10.0" + "source": "https://github.com/Seldaek/jsonlint/tree/1.10.1" }, "funding": [ { @@ -825,7 +825,7 @@ "type": "tidelift" } ], - "time": "2023-05-11T13:16:46+00:00" + "time": "2023-12-18T13:03:25+00:00" }, { "name": "seld/phar-utils", @@ -938,16 +938,16 @@ }, { "name": "symfony/console", - "version": "v5.4.32", + "version": "v5.4.34", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "c70df1ffaf23a8d340bded3cfab1b86752ad6ed7" + "reference": "4b4d8cd118484aa604ec519062113dd87abde18c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c70df1ffaf23a8d340bded3cfab1b86752ad6ed7", - "reference": "c70df1ffaf23a8d340bded3cfab1b86752ad6ed7", + "url": "https://api.github.com/repos/symfony/console/zipball/4b4d8cd118484aa604ec519062113dd87abde18c", + "reference": "4b4d8cd118484aa604ec519062113dd87abde18c", "shasum": "" }, "require": { @@ -1017,7 +1017,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.32" + "source": "https://github.com/symfony/console/tree/v5.4.34" }, "funding": [ { @@ -1033,7 +1033,7 @@ "type": "tidelift" } ], - "time": "2023-11-18T18:23:04+00:00" + "time": "2023-12-08T13:33:03+00:00" }, { "name": "symfony/deprecation-contracts", @@ -1802,16 +1802,16 @@ }, { "name": "symfony/process", - "version": "v5.4.28", + "version": "v5.4.34", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b" + "reference": "8fa22178dfc368911dbd513b431cd9b06f9afe7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b", - "reference": "45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b", + "url": "https://api.github.com/repos/symfony/process/zipball/8fa22178dfc368911dbd513b431cd9b06f9afe7a", + "reference": "8fa22178dfc368911dbd513b431cd9b06f9afe7a", "shasum": "" }, "require": { @@ -1844,7 +1844,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.28" + "source": "https://github.com/symfony/process/tree/v5.4.34" }, "funding": [ { @@ -1860,7 +1860,7 @@ "type": "tidelift" } ], - "time": "2023-08-07T10:36:04+00:00" + "time": "2023-12-02T08:41:43+00:00" }, { "name": "symfony/service-contracts", @@ -1947,16 +1947,16 @@ }, { "name": "symfony/string", - "version": "v5.4.32", + "version": "v5.4.34", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "91bf4453d65d8231688a04376c3a40efe0770f04" + "reference": "e3f98bfc7885c957488f443df82d97814a3ce061" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/91bf4453d65d8231688a04376c3a40efe0770f04", - "reference": "91bf4453d65d8231688a04376c3a40efe0770f04", + "url": "https://api.github.com/repos/symfony/string/zipball/e3f98bfc7885c957488f443df82d97814a3ce061", + "reference": "e3f98bfc7885c957488f443df82d97814a3ce061", "shasum": "" }, "require": { @@ -2013,7 +2013,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.32" + "source": "https://github.com/symfony/string/tree/v5.4.34" }, "funding": [ { @@ -2029,22 +2029,22 @@ "type": "tidelift" } ], - "time": "2023-11-26T13:43:46+00:00" + "time": "2023-12-09T13:20:28+00:00" } ], "packages-dev": [ { "name": "phpstan/phpstan", - "version": "1.10.50", + "version": "1.10.55", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "06a98513ac72c03e8366b5a0cb00750b487032e4" + "reference": "9a88f9d18ddf4cf54c922fbeac16c4cb164c5949" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/06a98513ac72c03e8366b5a0cb00750b487032e4", - "reference": "06a98513ac72c03e8366b5a0cb00750b487032e4", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9a88f9d18ddf4cf54c922fbeac16c4cb164c5949", + "reference": "9a88f9d18ddf4cf54c922fbeac16c4cb164c5949", "shasum": "" }, "require": { @@ -2093,7 +2093,7 @@ "type": "tidelift" } ], - "time": "2023-12-13T10:59:42+00:00" + "time": "2024-01-08T12:32:40+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -2246,16 +2246,16 @@ }, { "name": "phpstan/phpstan-symfony", - "version": "1.3.5", + "version": "1.3.6", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-symfony.git", - "reference": "27ff6339f83796a7e0dd963cf445cd3c456fc620" + "reference": "34b3c43684834f6a20aa51af8d455480d9de8b88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/27ff6339f83796a7e0dd963cf445cd3c456fc620", - "reference": "27ff6339f83796a7e0dd963cf445cd3c456fc620", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/34b3c43684834f6a20aa51af8d455480d9de8b88", + "reference": "34b3c43684834f6a20aa51af8d455480d9de8b88", "shasum": "" }, "require": { @@ -2312,22 +2312,22 @@ "description": "Symfony Framework extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-symfony/issues", - "source": "https://github.com/phpstan/phpstan-symfony/tree/1.3.5" + "source": "https://github.com/phpstan/phpstan-symfony/tree/1.3.6" }, - "time": "2023-10-30T14:52:15+00:00" + "time": "2023-12-22T11:22:34+00:00" }, { "name": "symfony/phpunit-bridge", - "version": "v7.0.1", + "version": "v7.0.2", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "c2d059b25e31274157dd7727131cd1cf33650207" + "reference": "92df075808c9437beca9540e25ae0c40eea1c061" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/c2d059b25e31274157dd7727131cd1cf33650207", - "reference": "c2d059b25e31274157dd7727131cd1cf33650207", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/92df075808c9437beca9540e25ae0c40eea1c061", + "reference": "92df075808c9437beca9540e25ae0c40eea1c061", "shasum": "" }, "require": { @@ -2379,7 +2379,7 @@ "description": "Provides utilities for PHPUnit, especially user deprecation notices management", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/phpunit-bridge/tree/v7.0.1" + "source": "https://github.com/symfony/phpunit-bridge/tree/v7.0.2" }, "funding": [ { @@ -2395,7 +2395,7 @@ "type": "tidelift" } ], - "time": "2023-12-01T09:26:31+00:00" + "time": "2023-12-19T11:23:03+00:00" } ], "aliases": [], diff --git a/doc/01-basic-usage.md b/doc/01-basic-usage.md index 12df96468..3d2571536 100644 --- a/doc/01-basic-usage.md +++ b/doc/01-basic-usage.md @@ -122,8 +122,8 @@ versions of the dependencies that you are using. Your CI server, production machines, other developers in your team, everything and everyone runs on the same dependencies, which mitigates the potential for bugs affecting only some parts of the deployments. Even if you develop alone, in six months when -reinstalling the project you can feel confident the dependencies installed are -still working even if your dependencies released many new versions since then. +reinstalling the project you can feel confident that the dependencies installed are +still working, even if the dependencies have released many new versions since then. (See note below about using the `update` command.) > **Note:** For libraries it is not necessary to commit the lock @@ -141,7 +141,7 @@ in `composer.lock` to ensure that the package versions are consistent for everyo working on your project. As a result you will have all dependencies requested by your `composer.json` file, but they may not all be at the very latest available versions (some of the dependencies listed in the `composer.lock` file may have released newer versions since -the file was created). This is by design, it ensures that your project does not break because of +the file was created). This is by design, ensuring that your project does not break because of unexpected changes in dependencies. So after fetching new changes from your VCS repository it is recommended to run diff --git a/doc/03-cli.md b/doc/03-cli.md index 166fed193..6e6809a49 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -198,8 +198,9 @@ php composer.phar update vendor/package:2.0.1 vendor/package2:3.0.* * **--no-install:** Does not run the install step after updating the composer.lock file. * **--no-audit:** Does not run the audit steps after updating the composer.lock file. Also see [COMPOSER_NO_AUDIT](#composer-no-audit). * **--audit-format:** Audit output format. Must be "table", "plain", "json", or "summary" (default). -* **--lock:** Only updates the lock file hash to suppress warning about the - lock file being out of date. +* **--lock:** Overwrites the lock file hash to suppress warning about the lock file being out of + date without updating package versions. Package metadata like mirrors and URLs are updated if + they changed. * **--with:** Temporary version constraint to add, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 * **--no-autoloader:** Skips autoloader generation. * **--no-progress:** Removes the progress display that can mess with some @@ -556,6 +557,7 @@ php composer.phar show monolog/monolog 1.0.2 * **--major-only (-M):** Use with --latest or --outdated. Only shows packages that have major SemVer-compatible updates. * **--minor-only (-m):** Use with --latest or --outdated. Only shows packages that have minor SemVer-compatible updates. * **--patch-only:** Use with --latest or --outdated. Only shows packages that have patch-level SemVer-compatible updates. +* **--sort-by-age (-A):** Displays the installed version's age, and sorts packages oldest first. Use with the --latest or --outdated option. * **--direct (-D):** Restricts the list of packages to your direct dependencies. * **--strict:** Return a non-zero exit code when there are outdated packages. * **--format (-f):** Lets you pick between text (default) or json output format. @@ -589,6 +591,7 @@ The color coding is as such: * **--major-only (-M):** Only shows packages that have major SemVer-compatible updates. * **--minor-only (-m):** Only shows packages that have minor SemVer-compatible updates. * **--patch-only (-p):** Only shows packages that have patch-level SemVer-compatible updates. +* **--sort-by-age (-A):** Displays the installed version's age, and sorts packages oldest first. * **--format (-f):** Lets you pick between text (default) or json output format. * **--no-dev:** Do not show outdated dev dependencies. * **--locked:** Shows updates for packages from the lock file, regardless of what is currently in vendor dir. @@ -1143,6 +1146,10 @@ If set to 1, this env suppresses a warning when Composer is running with the Xde This env var controls the [`discard-changes`](06-config.md#discard-changes) config option. +### COMPOSER_FUND + +If set to 0, this env suppresses funding notices when installing. + ### COMPOSER_HOME The `COMPOSER_HOME` var allows you to change the Composer home directory. This @@ -1237,6 +1244,11 @@ defaults to 12 and must be between 1 and 50. If your proxy has issues with concurrency maybe you want to lower this. Increasing it should generally not result in performance gains. +### COMPOSER_IPRESOLVE + +Set to `4` or `6` to force IPv4 or IPv6 DNS resolution. This only works when the +curl extension is used for downloads. + ### HTTP_PROXY_REQUEST_FULLURI If you use a proxy, but it does not support the request_fulluri flag, then you diff --git a/doc/05-repositories.md b/doc/05-repositories.md index b6d5beb02..de6e323b0 100644 --- a/doc/05-repositories.md +++ b/doc/05-repositories.md @@ -738,7 +738,7 @@ monolithic repository. "repositories": [ { "type": "path", - "url": "../../packages/my-package", + "url": "../../packages/*", "options": { "symlink": false } @@ -772,7 +772,7 @@ The following modes exist: "repositories": [ { "type": "path", - "url": "../../packages/my-package", + "url": "../../packages/*", "options": { "reference": "config" } diff --git a/doc/articles/composer-platform-dependencies.md b/doc/articles/composer-platform-dependencies.md index e166d6f4a..4e6dedafe 100644 --- a/doc/articles/composer-platform-dependencies.md +++ b/doc/articles/composer-platform-dependencies.md @@ -61,7 +61,7 @@ autoloader are considered the application "runtime". Starting with version 2.0, Composer makes [additional features](../07-runtime.md) (besides registering the class autoloader) available to the application runtime environment. Similar to `composer-plugin-api`, not every Composer release adds new runtime features, -thus the version of `composer-runtimeapi` is also increased independently from Composer's version. +thus the version of `composer-runtime-api` is also increased independently from Composer's version. ## Composer package `composer` diff --git a/doc/articles/troubleshooting.md b/doc/articles/troubleshooting.md index 7d5afdea9..0607535d7 100644 --- a/doc/articles/troubleshooting.md +++ b/doc/articles/troubleshooting.md @@ -304,6 +304,11 @@ open stream: Operation timed out We recommend you fix your IPv6 setup. If that is not possible, you can try the following workarounds: +**Generic Workaround:** + +Set the [`COMPOSER_IPRESOLVE=4`](../03-cli.md#composer-ipresolve) environment variable which will force curl to resolve +domains using IPv4. This only works when the curl extension is used for downloads. + **Workaround Linux:** On linux, it seems that running this command helps to make ipv4 traffic have a diff --git a/phpstan/baseline-8.1.neon b/phpstan/baseline-8.1.neon index 9c13d8216..5a0aabf76 100644 --- a/phpstan/baseline-8.1.neon +++ b/phpstan/baseline-8.1.neon @@ -55,6 +55,11 @@ parameters: count: 1 path: ../src/Composer/Config/JsonConfigSource.php + - + message: "#^Call to function method_exists\\(\\) with \\$this\\(Composer\\\\Console\\\\Application\\) and 'setCatchErrors' will always evaluate to true\\.$#" + count: 2 + path: ../src/Composer/Console/Application.php + - message: "#^Parameter \\#2 \\$callback of function uksort expects callable\\(string, string\\)\\: int, 'version_compare' given\\.$#" count: 2 @@ -85,6 +90,11 @@ parameters: count: 1 path: ../src/Composer/Downloader/GzipDownloader.php + - + message: "#^Call to function method_exists\\(\\) with Symfony\\\\Component\\\\Console\\\\Application and 'setCatchErrors' will always evaluate to true\\.$#" + count: 1 + path: ../src/Composer/EventDispatcher/EventDispatcher.php + - message: "#^Parameter \\#3 \\$length of function substr expects int\\|null, int\\<0, max\\>\\|false given\\.$#" count: 1 @@ -270,11 +280,21 @@ parameters: count: 2 path: ../tests/Composer/Test/ConfigTest.php + - + message: "#^Call to function method_exists\\(\\) with Composer\\\\Console\\\\Application and 'setCatchErrors' will always evaluate to true\\.$#" + count: 1 + path: ../tests/Composer/Test/DocumentationTest.php + - message: "#^Parameter \\#1 \\$callback of function call_user_func_array expects callable\\(\\)\\: mixed, array\\{Composer\\\\Repository\\\\CompositeRepository, string\\} given\\.$#" count: 1 path: ../tests/Composer/Test/Repository/CompositeRepositoryTest.php + - + message: "#^Call to function method_exists\\(\\) with Composer\\\\Console\\\\Application and 'setCatchErrors' will always evaluate to true\\.$#" + count: 1 + path: ../tests/Composer/Test/TestCase.php + - message: "#^Parameter \\#1 \\$object of method ReflectionProperty\\:\\:getValue\\(\\) expects object\\|null, object\\|string given\\.$#" count: 1 diff --git a/phpstan/baseline.neon b/phpstan/baseline.neon index 50f63ff25..b17166a49 100644 --- a/phpstan/baseline.neon +++ b/phpstan/baseline.neon @@ -765,16 +765,6 @@ parameters: count: 1 path: ../src/Composer/Command/ShowCommand.php - - - message: "#^Only booleans are allowed in \\|\\|, array\\ given on the left side\\.$#" - count: 1 - path: ../src/Composer/Command/ShowCommand.php - - - - message: "#^Only booleans are allowed in \\|\\|, array\\ given on the right side\\.$#" - count: 1 - path: ../src/Composer/Command/ShowCommand.php - - message: "#^Parameter \\#1 \\$arrayTree of method Composer\\\\Command\\\\ShowCommand\\:\\:displayPackageTree\\(\\) expects array\\\\>, array\\\\>\\|string\\|null\\>\\> given\\.$#" count: 2 @@ -4314,7 +4304,7 @@ parameters: path: ../src/Composer/Util/Http/CurlDownloader.php - - message: "#^Property Composer\\\\Util\\\\Http\\\\CurlDownloader\\:\\:\\$jobs \\(array\\, retries\\: int\\<0, max\\>, storeAuth\\: 'prompt'\\|bool\\}, options\\: array, progress\\: array, curlHandle\\: CurlHandle, filename\\: string\\|null, headerHandle\\: resource, \\.\\.\\.\\}\\>\\) does not accept non\\-empty\\-array\\, retries\\: int\\<0, max\\>, storeAuth\\: 'prompt'\\|bool\\}, options\\: array, progress\\: array, curlHandle\\: CurlHandle\\|resource, filename\\: string\\|null, headerHandle\\: resource, \\.\\.\\.\\}\\>\\.$#" + message: "#^Property Composer\\\\Util\\\\Http\\\\CurlDownloader\\:\\:\\$jobs \\(array\\, retries\\: int\\<0, max\\>, storeAuth\\: 'prompt'\\|bool, ipResolve\\: 4\\|6\\|null\\}, options\\: array, progress\\: array, curlHandle\\: CurlHandle, filename\\: string\\|null, headerHandle\\: resource, \\.\\.\\.\\}\\>\\) does not accept non\\-empty\\-array\\, retries\\: int\\<0, max\\>, storeAuth\\: 'prompt'\\|bool, ipResolve\\: 4\\|6\\|null\\}, options\\: array, progress\\: array, curlHandle\\: CurlHandle\\|resource, filename\\: string\\|null, headerHandle\\: resource, \\.\\.\\.\\}\\>\\.$#" count: 1 path: ../src/Composer/Util/Http/CurlDownloader.php diff --git a/src/Composer/Advisory/Auditor.php b/src/Composer/Advisory/Auditor.php index 6ca3d8102..bc6520d55 100644 --- a/src/Composer/Advisory/Auditor.php +++ b/src/Composer/Advisory/Auditor.php @@ -247,6 +247,7 @@ class Auditor foreach ($packageAdvisories as $advisory) { $headers = [ 'Package', + 'Severity', 'CVE', 'Title', 'URL', @@ -255,6 +256,7 @@ class Auditor ]; $row = [ $advisory->packageName, + $this->getSeverity($advisory), $this->getCVE($advisory), $advisory->title, $this->getURL($advisory), @@ -289,6 +291,7 @@ class Auditor $error[] = '--------'; } $error[] = "Package: ".$advisory->packageName; + $error[] = "Severity: ".$this->getSeverity($advisory); $error[] = "CVE: ".$this->getCVE($advisory); $error[] = "Title: ".OutputFormatter::escape($advisory->title); $error[] = "URL: ".$this->getURL($advisory); @@ -350,6 +353,15 @@ class Auditor return $packageUrl !== null ? '' . $package->getPrettyName() . '' : $package->getPrettyName(); } + private function getSeverity(SecurityAdvisory $advisory): string + { + if ($advisory->severity === null) { + return ''; + } + + return $advisory->severity; + } + private function getCVE(SecurityAdvisory $advisory): string { if ($advisory->cve === null) { diff --git a/src/Composer/Advisory/IgnoredSecurityAdvisory.php b/src/Composer/Advisory/IgnoredSecurityAdvisory.php index ba9079287..3d8b56a1c 100644 --- a/src/Composer/Advisory/IgnoredSecurityAdvisory.php +++ b/src/Composer/Advisory/IgnoredSecurityAdvisory.php @@ -26,9 +26,9 @@ class IgnoredSecurityAdvisory extends SecurityAdvisory /** * @param non-empty-array $sources */ - public function __construct(string $packageName, string $advisoryId, ConstraintInterface $affectedVersions, string $title, array $sources, DateTimeImmutable $reportedAt, ?string $cve = null, ?string $link = null, ?string $ignoreReason = null) + public function __construct(string $packageName, string $advisoryId, ConstraintInterface $affectedVersions, string $title, array $sources, DateTimeImmutable $reportedAt, ?string $cve = null, ?string $link = null, ?string $ignoreReason = null, ?string $severity = null) { - parent::__construct($packageName, $advisoryId, $affectedVersions, $title, $sources, $reportedAt, $cve, $link); + parent::__construct($packageName, $advisoryId, $affectedVersions, $title, $sources, $reportedAt, $cve, $link, $severity); $this->ignoreReason = $ignoreReason; } diff --git a/src/Composer/Advisory/PartialSecurityAdvisory.php b/src/Composer/Advisory/PartialSecurityAdvisory.php index 28dbdc4f4..2867e9b60 100644 --- a/src/Composer/Advisory/PartialSecurityAdvisory.php +++ b/src/Composer/Advisory/PartialSecurityAdvisory.php @@ -44,7 +44,7 @@ class PartialSecurityAdvisory implements JsonSerializable { $constraint = $parser->parseConstraints($data['affectedVersions']); if (isset($data['title'], $data['sources'], $data['reportedAt'])) { - return new SecurityAdvisory($packageName, $data['advisoryId'], $constraint, $data['title'], $data['sources'], new \DateTimeImmutable($data['reportedAt'], new \DateTimeZone('UTC')), $data['cve'] ?? null, $data['link'] ?? null); + return new SecurityAdvisory($packageName, $data['advisoryId'], $constraint, $data['title'], $data['sources'], new \DateTimeImmutable($data['reportedAt'], new \DateTimeZone('UTC')), $data['cve'] ?? null, $data['link'] ?? null, $data['severity'] ?? null); } return new self($packageName, $data['advisoryId'], $constraint); diff --git a/src/Composer/Advisory/SecurityAdvisory.php b/src/Composer/Advisory/SecurityAdvisory.php index e88228d60..a3d58b462 100644 --- a/src/Composer/Advisory/SecurityAdvisory.php +++ b/src/Composer/Advisory/SecurityAdvisory.php @@ -47,10 +47,16 @@ class SecurityAdvisory extends PartialSecurityAdvisory */ public $sources; + /** + * @var string|null + * @readonly + */ + public $severity; + /** * @param non-empty-array $sources */ - public function __construct(string $packageName, string $advisoryId, ConstraintInterface $affectedVersions, string $title, array $sources, DateTimeImmutable $reportedAt, ?string $cve = null, ?string $link = null) + public function __construct(string $packageName, string $advisoryId, ConstraintInterface $affectedVersions, string $title, array $sources, DateTimeImmutable $reportedAt, ?string $cve = null, ?string $link = null, ?string $severity = null) { parent::__construct($packageName, $advisoryId, $affectedVersions); @@ -59,6 +65,7 @@ class SecurityAdvisory extends PartialSecurityAdvisory $this->reportedAt = $reportedAt; $this->cve = $cve; $this->link = $link; + $this->severity = $severity; } /** @@ -75,7 +82,8 @@ class SecurityAdvisory extends PartialSecurityAdvisory $this->reportedAt, $this->cve, $this->link, - $ignoreReason + $ignoreReason, + $this->severity ); } diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index d1b4fbee5..18b1911d7 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -12,14 +12,23 @@ namespace Composer\Command; +use Composer\Advisory\Auditor; use Composer\Composer; use Composer\Factory; use Composer\Config; use Composer\Downloader\TransportException; +use Composer\IO\BufferIO; +use Composer\Json\JsonFile; +use Composer\Package\RootPackage; +use Composer\Package\Version\VersionParser; use Composer\Pcre\Preg; +use Composer\Repository\ComposerRepository; +use Composer\Repository\FilesystemRepository; use Composer\Repository\PlatformRepository; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; +use Composer\Repository\RepositorySet; +use Composer\Repository\RootPackageRepository; use Composer\Util\ConfigValidator; use Composer\Util\Git; use Composer\Util\IniHelper; @@ -153,10 +162,13 @@ EOT $io->write('Checking pubkeys: ', false); $this->outputResult($this->checkPubKeys($config)); - $io->write('Checking composer version: ', false); + $io->write('Checking Composer version: ', false); $this->outputResult($this->checkVersion($config)); } + $io->write('Checking Composer and its dependencies for vulnerabilities: ', false); + $this->outputResult($this->checkComposerAudit($config)); + $io->write(sprintf('Composer version: %s', Composer::getVersion())); $platformOverrides = $config->get('platform') ?: []; @@ -438,6 +450,48 @@ EOT return true; } + /** + * @return string|true + */ + private function checkComposerAudit(Config $config) + { + $result = $this->checkConnectivityAndComposerNetworkHttpEnablement(); + if ($result !== true) { + return $result; + } + + $auditor = new Auditor(); + $repoSet = new RepositorySet(); + $installedJson = new JsonFile(__DIR__ . '/../../../vendor/composer/installed.json'); + if (!$installedJson->exists()) { + return 'Could not find Composer\'s installed.json, this must be a non-standard Composer installation.'; + } + + $localRepo = new FilesystemRepository($installedJson); + $version = Composer::getVersion(); + $packages = $localRepo->getCanonicalPackages(); + if ($version !== '@package_version@') { + $versionParser = new VersionParser(); + $normalizedVersion = $versionParser->normalize($version); + $rootPkg = new RootPackage('composer/composer', $normalizedVersion, $version); + $packages[] = $rootPkg; + } + $repoSet->addRepository(new ComposerRepository(['type' => 'composer', 'url' => 'https://packagist.org'], new NullIO(), $config, $this->httpDownloader)); + + try { + $io = new BufferIO(); + $result = $auditor->audit($io, $repoSet, $packages, Auditor::FORMAT_TABLE, true, [], Auditor::ABANDONED_IGNORE); + } catch (\Throwable $e) { + return 'Failed performing audit: '.$e->getMessage().''; + } + + if ($result > 0) { + return 'Audit found some issues:' . PHP_EOL . $io->getOutput(); + } + + return true; + } + private function getCurlVersion(): string { if (extension_loaded('curl')) { @@ -499,7 +553,7 @@ EOT if ($result) { foreach ($result as $message) { - $io->write($message); + $io->write(trim($message)); } } } @@ -722,6 +776,11 @@ EOT $out($iniMessage, 'comment'); } + if (in_array(Platform::getEnv('COMPOSER_IPRESOLVE'), ['4', '6'], true)) { + $warnings['ipresolve'] = true; + $out('The COMPOSER_IPRESOLVE env var is set to ' . Platform::getEnv('COMPOSER_IPRESOLVE') .' which may result in network failures below.', 'comment'); + } + return count($warnings) === 0 && count($errors) === 0 ? true : $output; } diff --git a/src/Composer/Command/OutdatedCommand.php b/src/Composer/Command/OutdatedCommand.php index 05f22ebbb..b6c71c175 100644 --- a/src/Composer/Command/OutdatedCommand.php +++ b/src/Composer/Command/OutdatedCommand.php @@ -40,6 +40,7 @@ class OutdatedCommand extends BaseCommand new InputOption('major-only', 'M', InputOption::VALUE_NONE, 'Show only packages that have major SemVer-compatible updates.'), new InputOption('minor-only', 'm', InputOption::VALUE_NONE, 'Show only packages that have minor SemVer-compatible updates.'), new InputOption('patch-only', 'p', InputOption::VALUE_NONE, 'Show only packages that have patch SemVer-compatible updates.'), + new InputOption('sort-by-age', 'A', InputOption::VALUE_NONE, 'Displays the installed version\'s age, and sorts packages oldest first.'), new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text', ['json', 'text']), new InputOption('ignore', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore specified package(s). Use it if you don\'t want to be informed about new versions of some packages.', null, $this->suggestInstalledPackage(false)), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables search in require-dev packages.'), @@ -97,6 +98,9 @@ EOT if ($input->getOption('no-dev')) { $args['--no-dev'] = true; } + if ($input->getOption('sort-by-age')) { + $args['--sort-by-age'] = true; + } $args['--ignore-platform-req'] = $input->getOption('ignore-platform-req'); if ($input->getOption('ignore-platform-reqs')) { $args['--ignore-platform-reqs'] = true; diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 658d58de3..4a8a47559 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -349,6 +349,10 @@ EOT } throw $e; } finally { + if ($input->getOption('dry-run') && $this->newlyCreated) { + @unlink($this->json->getPath()); + } + $signalHandler->unregister(); } } diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index 45db77e52..26d9f7546 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -156,6 +156,10 @@ EOT return $this->rollback($output, $rollbackDir, $localFilename); } + if ($input->getArgument('command') === 'self' && $input->getArgument('version') === 'update') { + $input->setArgument('version', null); + } + $latest = $versionsUtil->getLatest(); $latestStable = $versionsUtil->getLatest('stable'); try { diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 083a49ec3..fc5940b91 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -27,6 +27,7 @@ use Composer\Package\Version\VersionSelector; use Composer\Pcre\Preg; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; +use Composer\Repository\ArrayRepository; use Composer\Repository\InstalledArrayRepository; use Composer\Repository\ComposerRepository; use Composer\Repository\CompositeRepository; @@ -98,6 +99,7 @@ class ShowCommand extends BaseCommand new InputOption('major-only', 'M', InputOption::VALUE_NONE, 'Show only packages that have major SemVer-compatible updates. Use with the --latest or --outdated option.'), new InputOption('minor-only', 'm', InputOption::VALUE_NONE, 'Show only packages that have minor SemVer-compatible updates. Use with the --latest or --outdated option.'), new InputOption('patch-only', null, InputOption::VALUE_NONE, 'Show only packages that have patch SemVer-compatible updates. Use with the --latest or --outdated option.'), + new InputOption('sort-by-age', 'A', InputOption::VALUE_NONE, 'Displays the installed version\'s age, and sorts packages oldest first. Use with the --latest or --outdated option.'), new InputOption('direct', 'D', InputOption::VALUE_NONE, 'Shows only packages that are directly required by the root package'), new InputOption('strict', null, InputOption::VALUE_NONE, 'Return a non-zero exit code when there are outdated packages'), new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text', ['json', 'text']), @@ -141,7 +143,7 @@ EOT $composer = $this->tryComposer(); $io = $this->getIO(); - if ($input->getOption('installed')) { + if ($input->getOption('installed') && !$input->getOption('self')) { $io->writeError('You are using the deprecated option "installed". Only installed packages are shown by default now. The --all option can be used to show all packages.'); } @@ -198,7 +200,7 @@ EOT $platformRepo = new PlatformRepository([], $platformOverrides); $lockedRepo = null; - if ($input->getOption('self')) { + if ($input->getOption('self') && !$input->getOption('installed') && !$input->getOption('locked')) { $package = clone $this->requireComposer()->getPackage(); if ($input->getOption('name-only')) { $io->write($package->getName()); @@ -242,6 +244,9 @@ EOT } $locker = $composer->getLocker(); $lockedRepo = $locker->getLockedRepository(!$input->getOption('no-dev')); + if ($input->getOption('self')) { + $lockedRepo->addPackage(clone $composer->getPackage()); + } $repos = $installedRepo = new InstalledRepository([$lockedRepo]); } else { // --installed / default case @@ -249,17 +254,30 @@ EOT $composer = $this->requireComposer(); } $rootPkg = $composer->getPackage(); - $repos = $installedRepo = new InstalledRepository([$composer->getRepositoryManager()->getLocalRepository()]); + $rootRepo = new InstalledArrayRepository(); + if ($input->getOption('self')) { + $rootRepo = new RootPackageRepository(clone $rootPkg); + } if ($input->getOption('no-dev')) { - $packages = RepositoryUtils::filterRequiredPackages($installedRepo->getPackages(), $rootPkg); - $repos = $installedRepo = new InstalledRepository([new InstalledArrayRepository(array_map(static function ($pkg): PackageInterface { + $packages = RepositoryUtils::filterRequiredPackages($composer->getRepositoryManager()->getLocalRepository()->getPackages(), $rootPkg); + $repos = $installedRepo = new InstalledRepository([$rootRepo, new InstalledArrayRepository(array_map(static function ($pkg): PackageInterface { return clone $pkg; }, $packages))]); + } else { + $repos = $installedRepo = new InstalledRepository([$rootRepo, $composer->getRepositoryManager()->getLocalRepository()]); } - if (!$installedRepo->getPackages() && ($rootPkg->getRequires() || $rootPkg->getDevRequires())) { - $io->writeError('No dependencies installed. Try running composer install or update.'); + if (!$installedRepo->getPackages()) { + $hasNonPlatformReqs = static function (array $reqs): bool { + return (bool) array_filter(array_keys($reqs), function (string $name) { + return !PlatformRepository::isPlatformPackage($name); + }); + }; + + if ($hasNonPlatformReqs($rootPkg->getRequires()) || $hasNonPlatformReqs($rootPkg->getDevRequires())) { + $io->writeError('No dependencies installed. Try running composer install or update.'); + } } } @@ -281,6 +299,12 @@ EOT } elseif (null !== $packageFilter && !str_contains($packageFilter, '*')) { [$package, $versions] = $this->getPackage($installedRepo, $repos, $packageFilter, $input->getArgument('version')); + if (isset($package) && $input->getOption('direct')) { + if (!in_array($package->getName(), $this->getRootRequires(), true)) { + throw new \InvalidArgumentException('Package "' . $package->getName() . '" is installed but not a direct dependent of the root package.'); + } + } + if (!isset($package)) { $options = $input->getOptions(); $hint = ''; @@ -293,7 +317,7 @@ EOT if (PlatformRepository::isPlatformPackage($packageFilter) && !$input->getOption('platform')) { $hint .= ', try using --platform (-p) to show platform packages'; } - if (!$input->getOption('all')) { + if (!$input->getOption('all') && !$input->getOption('available')) { $hint .= ', try using --available (-a) to show all available packages'; } @@ -450,7 +474,7 @@ EOT if (isset($packages[$type])) { ksort($packages[$type]); - $nameLength = $versionLength = $latestLength = 0; + $nameLength = $versionLength = $latestLength = $releaseDateLength = 0; if ($showLatest && $showVersion) { foreach ($packages[$type] as $package) { @@ -469,9 +493,20 @@ EOT $writeVersion = !$input->getOption('name-only') && !$input->getOption('path') && $showVersion; $writeLatest = $writeVersion && $showLatest; $writeDescription = !$input->getOption('name-only') && !$input->getOption('path'); + $writeReleaseDate = $writeLatest && $input->getOption('sort-by-age'); $hasOutdatedPackages = false; + if ($input->getOption('sort-by-age')) { + usort($packages[$type], function ($a, $b) { + if (is_object($a) && is_object($b)) { + return $a->getReleaseDate() <=> $b->getReleaseDate(); + } + + return 0; + }); + } + $viewData[$type] = []; foreach ($packages[$type] as $package) { $packageViewData = []; @@ -505,6 +540,17 @@ EOT $packageViewData['version'] = $package->getFullPrettyVersion(); $versionLength = max($versionLength, strlen($package->getFullPrettyVersion())); } + if ($writeReleaseDate) { + if ($package->getReleaseDate() !== null) { + $packageViewData['release-age'] = str_replace(' ago', ' old', $this->getRelativeTime($package->getReleaseDate())); + if (!str_contains($packageViewData['release-age'], ' old')) { + $packageViewData['release-age'] = 'from '.$packageViewData['release-age']; + } + $releaseDateLength = max($releaseDateLength, strlen($packageViewData['release-age'])); + } else { + $packageViewData['release-age'] = ''; + } + } if ($writeLatest && $latestPackage) { $packageViewData['latest'] = $latestPackage->getFullPrettyVersion(); $packageViewData['latest-status'] = $this->getUpdateStatus($latestPackage, $package); @@ -552,7 +598,9 @@ EOT 'nameLength' => $nameLength, 'versionLength' => $versionLength, 'latestLength' => $latestLength, + 'releaseDateLength' => $releaseDateLength, 'writeLatest' => $writeLatest, + 'writeReleaseDate' => $writeReleaseDate, ]; if ($input->getOption('strict') && $hasOutdatedPackages) { $exitCode = 1; @@ -588,11 +636,14 @@ EOT $nameLength = $viewMetaData[$type]['nameLength']; $versionLength = $viewMetaData[$type]['versionLength']; $latestLength = $viewMetaData[$type]['latestLength']; + $releaseDateLength = $viewMetaData[$type]['releaseDateLength']; $writeLatest = $viewMetaData[$type]['writeLatest']; + $writeReleaseDate = $viewMetaData[$type]['writeReleaseDate']; $versionFits = $nameLength + $versionLength + 3 <= $width; $latestFits = $nameLength + $versionLength + $latestLength + 3 <= $width; - $descriptionFits = $nameLength + $versionLength + $latestLength + 24 <= $width; + $releaseDateFits = $nameLength + $versionLength + $latestLength + $releaseDateLength + 3 <= $width; + $descriptionFits = $nameLength + $versionLength + $latestLength + $releaseDateLength + 24 <= $width; if ($latestFits && !$io->isDecorated()) { $latestLength += 2; @@ -620,14 +671,14 @@ EOT $io->writeError(''); $io->writeError('Direct dependencies required in composer.json:'); if (\count($directDeps) > 0) { - $this->printPackages($io, $directDeps, $indent, $writeVersion && $versionFits, $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength); + $this->printPackages($io, $directDeps, $indent, $writeVersion && $versionFits, $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength, $writeReleaseDate && $releaseDateFits, $releaseDateLength); } 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, $writeVersion && $versionFits, $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength); + $this->printPackages($io, $transitiveDeps, $indent, $writeVersion && $versionFits, $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength, $writeReleaseDate && $releaseDateFits, $releaseDateLength); } else { $io->writeError('Everything up to date'); } @@ -635,7 +686,7 @@ EOT if ($writeLatest && \count($packages) === 0) { $io->writeError('All your direct dependencies are up to date'); } else { - $this->printPackages($io, $packages, $indent, $writeVersion && $versionFits, $writeLatest && $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength); + $this->printPackages($io, $packages, $indent, $writeVersion && $versionFits, $writeLatest && $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength, $writeReleaseDate && $releaseDateFits, $releaseDateLength); } } @@ -651,11 +702,12 @@ EOT /** * @param array $packages */ - private function printPackages(IOInterface $io, array $packages, string $indent, bool $writeVersion, bool $writeLatest, bool $writeDescription, int $width, int $versionLength, int $nameLength, int $latestLength): void + private function printPackages(IOInterface $io, array $packages, string $indent, bool $writeVersion, bool $writeLatest, bool $writeDescription, int $width, int $versionLength, int $nameLength, int $latestLength, bool $writeReleaseDate, int $releaseDateLength): void { - $padName = $writeVersion || $writeLatest || $writeDescription; - $padVersion = $writeLatest || $writeDescription; - $padLatest = $writeDescription; + $padName = $writeVersion || $writeLatest || $writeReleaseDate || $writeDescription; + $padVersion = $writeLatest || $writeReleaseDate || $writeDescription; + $padLatest = $writeDescription || $writeReleaseDate; + $padReleaseDate = $writeDescription; foreach ($packages as $package) { $link = $package['source'] ?? $package['homepage'] ?? ''; if ($link !== '') { @@ -674,10 +726,13 @@ EOT $latestVersion = str_replace(['up-to-date', 'semver-safe-update', 'update-possible'], ['=', '!', '~'], $updateStatus) . ' ' . $latestVersion; } $io->write(' <' . $style . '>' . str_pad($latestVersion, ($padLatest ? $latestLength : 0), ' ') . '', false); + if ($writeReleaseDate && isset($package['release-age'])) { + $io->write(' '.str_pad($package['release-age'], ($padReleaseDate ? $releaseDateLength : 0), ' '), false); + } } if (isset($package['description']) && $writeDescription) { $description = strtok($package['description'], "\r\n"); - $remaining = $width - $nameLength - $versionLength - 4; + $remaining = $width - $nameLength - $versionLength - $releaseDateLength - 4; if ($writeLatest) { $remaining -= $latestLength; } @@ -806,14 +861,20 @@ EOT */ protected function printMeta(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo, ?PackageInterface $latestPackage = null): void { + $isInstalledPackage = !PlatformRepository::isPlatformPackage($package->getName()) && $installedRepo->hasPackage($package); + $io = $this->getIO(); $io->write('name : ' . $package->getPrettyName()); $io->write('descrip. : ' . $package->getDescription()); $io->write('keywords : ' . implode(', ', $package->getKeywords() ?: [])); $this->printVersions($package, $versions, $installedRepo); + if ($isInstalledPackage && $package->getReleaseDate() !== null) { + $io->write('released : ' . $package->getReleaseDate()->format('Y-m-d') . ', ' . $this->getRelativeTime($package->getReleaseDate())); + } if ($latestPackage) { $style = $this->getVersionStyle($latestPackage, $package); - $io->write('latest : <'.$style.'>' . $latestPackage->getPrettyVersion() . ''); + $releasedTime = $latestPackage->getReleaseDate() === null ? '' : ' released ' . $latestPackage->getReleaseDate()->format('Y-m-d') . ', ' . $this->getRelativeTime($latestPackage->getReleaseDate()); + $io->write('latest : <'.$style.'>' . $latestPackage->getPrettyVersion() . '' . $releasedTime); } else { $latestPackage = $package; } @@ -822,7 +883,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 (!PlatformRepository::isPlatformPackage($package->getName()) && $installedRepo->hasPackage($package)) { + if ($isInstalledPackage) { $path = $this->requireComposer()->getInstallationManager()->getInstallPath($package); if (is_string($path)) { $io->write('path : ' . realpath($path)); @@ -993,6 +1054,10 @@ EOT } else { $json['path'] = null; } + + if ($package->getReleaseDate() !== null) { + $json['released'] = $package->getReleaseDate()->format(DATE_ATOM); + } } if ($latestPackage instanceof CompletePackageInterface && $latestPackage->isAbandoned()) { @@ -1447,4 +1512,30 @@ EOT return $this->repositorySet; } + + private function getRelativeTime(\DateTimeInterface $releaseDate): string + { + if ($releaseDate->format('Ymd') === date('Ymd')) { + return 'today'; + } + + $diff = $releaseDate->diff(new \DateTimeImmutable()); + if ($diff->days < 7) { + return 'this week'; + } + + if ($diff->days < 14) { + return 'last week'; + } + + if ($diff->m < 1 && $diff->days < 31) { + return floor($diff->days / 7) . ' weeks ago'; + } + + if ($diff->y < 1) { + return $diff->m . ' month' . ($diff->m > 1 ? 's' : '') . ' ago'; + } + + return $diff->y . ' year' . ($diff->y > 1 ? 's' : '') . ' ago'; + } } diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index 92eb147f5..70603b867 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -15,6 +15,7 @@ namespace Composer\DependencyResolver; use Composer\Package\AliasPackage; use Composer\Package\BasePackage; use Composer\Package\Package; +use Composer\Pcre\Preg; /** * @author Nils Adermann @@ -111,6 +112,14 @@ class LockTransaction extends Transaction if ($package->getName() === $presentPackage->getName() && $package->getVersion() === $presentPackage->getVersion()) { if ($presentPackage->getSourceReference() && $presentPackage->getSourceType() === $package->getSourceType()) { $package->setSourceDistReferences($presentPackage->getSourceReference()); + // if the dist url is not one of those handled gracefully by setSourceDistReferences then we should overwrite it with the old one + if ($package->getDistUrl() !== null && !Preg::isMatch('{^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/}i', $package->getDistUrl())) { + $package->setDistUrl($presentPackage->getDistUrl()); + } + $package->setDistType($presentPackage->getDistType()); + if ($package instanceof Package) { + $package->setDistSha1Checksum($presentPackage->getDistSha1Checksum()); + } } if ($presentPackage->getReleaseDate() !== null && $package instanceof Package) { $package->setReleaseDate($presentPackage->getReleaseDate()); diff --git a/src/Composer/Downloader/DvcsDownloaderInterface.php b/src/Composer/Downloader/DvcsDownloaderInterface.php index 879aab2ed..6e5b67c0a 100644 --- a/src/Composer/Downloader/DvcsDownloaderInterface.php +++ b/src/Composer/Downloader/DvcsDownloaderInterface.php @@ -24,7 +24,7 @@ interface DvcsDownloaderInterface /** * Checks for unpushed changes to a current branch * - * @param PackageInterface $package package directory + * @param PackageInterface $package package instance * @param string $path package directory * @return string|null changes or null */ diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 0c989a82d..2a2847f33 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -458,7 +458,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface /** * Process the download url * - * @param PackageInterface $package package the url is coming from + * @param PackageInterface $package package instance * @param non-empty-string $url download url * @throws \RuntimeException If any problem with the url * @return non-empty-string url diff --git a/src/Composer/Downloader/VcsCapableDownloaderInterface.php b/src/Composer/Downloader/VcsCapableDownloaderInterface.php index 879170276..c99005aa4 100644 --- a/src/Composer/Downloader/VcsCapableDownloaderInterface.php +++ b/src/Composer/Downloader/VcsCapableDownloaderInterface.php @@ -24,7 +24,7 @@ interface VcsCapableDownloaderInterface /** * Gets the VCS Reference for the package at path * - * @param PackageInterface $package package directory + * @param PackageInterface $package package instance * @param string $path package directory * @return string|null reference or null */ diff --git a/src/Composer/Filter/PlatformRequirementFilter/IgnoreAllPlatformRequirementFilter.php b/src/Composer/Filter/PlatformRequirementFilter/IgnoreAllPlatformRequirementFilter.php index 491ad01b9..8167fdd3e 100644 --- a/src/Composer/Filter/PlatformRequirementFilter/IgnoreAllPlatformRequirementFilter.php +++ b/src/Composer/Filter/PlatformRequirementFilter/IgnoreAllPlatformRequirementFilter.php @@ -20,4 +20,9 @@ final class IgnoreAllPlatformRequirementFilter implements PlatformRequirementFil { return PlatformRepository::isPlatformPackage($req); } + + public function isUpperBoundIgnored(string $req): bool + { + return $this->isIgnored($req); + } } diff --git a/src/Composer/Filter/PlatformRequirementFilter/IgnoreListPlatformRequirementFilter.php b/src/Composer/Filter/PlatformRequirementFilter/IgnoreListPlatformRequirementFilter.php index 43cc33469..73d536376 100644 --- a/src/Composer/Filter/PlatformRequirementFilter/IgnoreListPlatformRequirementFilter.php +++ b/src/Composer/Filter/PlatformRequirementFilter/IgnoreListPlatformRequirementFilter.php @@ -60,6 +60,15 @@ final class IgnoreListPlatformRequirementFilter implements PlatformRequirementFi return Preg::isMatch($this->ignoreRegex, $req); } + public function isUpperBoundIgnored(string $req): bool + { + if (!PlatformRepository::isPlatformPackage($req)) { + return false; + } + + return $this->isIgnored($req) || Preg::isMatch($this->ignoreUpperBoundRegex, $req); + } + /** * @param bool $allowUpperBoundOverride For conflicts we do not want the upper bound to be skipped */ diff --git a/src/Composer/Filter/PlatformRequirementFilter/IgnoreNothingPlatformRequirementFilter.php b/src/Composer/Filter/PlatformRequirementFilter/IgnoreNothingPlatformRequirementFilter.php index 265701824..ab225d6c9 100644 --- a/src/Composer/Filter/PlatformRequirementFilter/IgnoreNothingPlatformRequirementFilter.php +++ b/src/Composer/Filter/PlatformRequirementFilter/IgnoreNothingPlatformRequirementFilter.php @@ -21,4 +21,12 @@ final class IgnoreNothingPlatformRequirementFilter implements PlatformRequiremen { return false; } + + /** + * @return false + */ + public function isUpperBoundIgnored(string $req): bool + { + return false; + } } diff --git a/src/Composer/Filter/PlatformRequirementFilter/PlatformRequirementFilterInterface.php b/src/Composer/Filter/PlatformRequirementFilter/PlatformRequirementFilterInterface.php index af1f651c8..59e824591 100644 --- a/src/Composer/Filter/PlatformRequirementFilter/PlatformRequirementFilterInterface.php +++ b/src/Composer/Filter/PlatformRequirementFilter/PlatformRequirementFilterInterface.php @@ -15,4 +15,6 @@ namespace Composer\Filter\PlatformRequirementFilter; interface PlatformRequirementFilterInterface { public function isIgnored(string $req): bool; + + public function isUpperBoundIgnored(string $req): bool; } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index d974f8c29..fe50c8fde 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -372,22 +372,30 @@ class Installer } } - $fundingCount = 0; - foreach ($localRepo->getPackages() as $package) { - if ($package instanceof CompletePackageInterface && !$package instanceof AliasPackage && $package->getFunding()) { - $fundingCount++; - } + $fundEnv = Platform::getEnv('COMPOSER_FUND'); + $showFunding = true; + if (is_numeric($fundEnv)) { + $showFunding = intval($fundEnv) !== 0; } - if ($fundingCount > 0) { - $this->io->writeError([ - sprintf( - "%d package%s you are using %s looking for funding.", - $fundingCount, - 1 === $fundingCount ? '' : 's', - 1 === $fundingCount ? 'is' : 'are' - ), - 'Use the `composer fund` command to find out more!', - ]); + + if ($showFunding) { + $fundingCount = 0; + foreach ($localRepo->getPackages() as $package) { + if ($package instanceof CompletePackageInterface && !$package instanceof AliasPackage && $package->getFunding()) { + $fundingCount++; + } + } + if ($fundingCount > 0) { + $this->io->writeError([ + sprintf( + "%d package%s you are using %s looking for funding.", + $fundingCount, + 1 === $fundingCount ? '' : 's', + 1 === $fundingCount ? 'is' : 'are' + ), + 'Use the `composer fund` command to find out more!', + ]); + } } if ($this->runScripts) { @@ -605,7 +613,14 @@ class Installer // output op if lock file is enabled, but alias op only in debug verbosity if ($this->config->get('lock') && (false === strpos($operation->getOperationType(), 'Alias') || $this->io->isDebug())) { - $this->io->writeError(' - ' . $operation->show(true)); + $sourceRepo = ''; + if ($this->io->isVeryVerbose() && false === strpos($operation->getOperationType(), 'Alias')) { + $operationPkg = ($operation instanceof UpdateOperation ? $operation->getTargetPackage() : $operation->getPackage()); + if ($operationPkg->getRepository() !== null) { + $sourceRepo = ' from ' . $operationPkg->getRepository()->getRepoName(); + } + } + $this->io->writeError(' - ' . $operation->show(true) . $sourceRepo); } } diff --git a/src/Composer/Package/Version/VersionBumper.php b/src/Composer/Package/Version/VersionBumper.php index aa86d66de..e6dbdebe6 100644 --- a/src/Composer/Package/Version/VersionBumper.php +++ b/src/Composer/Package/Version/VersionBumper.php @@ -83,10 +83,10 @@ class VersionBumper $pattern = '{ (?<=,|\ |\||^) # leading separator (?P - \^'.$major.'(?:\.\d+)* # e.g. ^2.anything - | ~'.$major.'(?:\.\d+){0,2} # e.g. ~2 or ~2.2 or ~2.2.2 but no more - | '.$major.'(?:\.[*x])+ # e.g. 2.* or 2.*.* or 2.x.x.x etc - | >=\d(?:\.\d+)* # e.g. >=2 or >=1.2 etc + \^v?'.$major.'(?:\.\d+)* # e.g. ^2.anything + | ~v?'.$major.'(?:\.\d+){0,2} # e.g. ~2 or ~2.2 or ~2.2.2 but no more + | v?'.$major.'(?:\.[*x])+ # e.g. 2.* or 2.*.* or 2.x.x.x etc + | >=v?\d(?:\.\d+)* # e.g. >=2 or >=1.2 etc | \* # full wildcard ) (?=,|$|\ |\||@) # trailing separator diff --git a/src/Composer/Package/Version/VersionSelector.php b/src/Composer/Package/Version/VersionSelector.php index 138b8fabf..9ab322084 100644 --- a/src/Composer/Package/Version/VersionSelector.php +++ b/src/Composer/Package/Version/VersionSelector.php @@ -13,6 +13,7 @@ namespace Composer\Package\Version; use Composer\Filter\PlatformRequirementFilter\IgnoreAllPlatformRequirementFilter; +use Composer\Filter\PlatformRequirementFilter\IgnoreListPlatformRequirementFilter; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterInterface; use Composer\IO\IOInterface; @@ -130,6 +131,13 @@ class VersionSelector // constraint satisfied, go to next require continue 2; } + if ($platformRequirementFilter instanceof IgnoreListPlatformRequirementFilter && $platformRequirementFilter->isUpperBoundIgnored($name)) { + $filteredConstraint = $platformRequirementFilter->filterConstraint($name, $link->getConstraint()); + if ($filteredConstraint->matches($providedConstraint)) { + // constraint satisfied with the upper bound ignored, go to next require + continue 2; + } + } } // constraint not satisfied @@ -182,6 +190,7 @@ class VersionSelector * * For example: * * 1.2.1 -> ^1.2 + * * 1.2.1.2 -> ^1.2 * * 1.2 -> ^1.2 * * v3.2.1 -> ^3.2 * * 2.0-beta.1 -> ^2.0@beta @@ -227,7 +236,7 @@ class VersionSelector $semanticVersionParts = explode('.', $version); // check to see if we have a semver-looking version - if (count($semanticVersionParts) === 4 && Preg::isMatch('{^0\D?}', $semanticVersionParts[3])) { + if (count($semanticVersionParts) === 4 && Preg::isMatch('{^\d+\D?}', $semanticVersionParts[3])) { // remove the last parts (i.e. the patch version number and any extra) if ($semanticVersionParts[0] === '0') { unset($semanticVersionParts[3]); diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 1dcf991aa..ec244b129 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -109,7 +109,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito private $partialPackagesByName = null; /** @var bool */ private $displayedWarningAboutNonMatchingPackageIndex = false; - /** @var array{metadata: bool, query-all: bool, api-url: string|null}|null */ + /** @var array{metadata: bool, api-url: string|null}|null */ private $securityAdvisoryConfig = null; /** @@ -637,6 +637,15 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $apiUrl = $this->securityAdvisoryConfig['api-url']; + // respect available-package-patterns / available-packages directives from the repo + if ($this->hasAvailablePackageList) { + foreach ($packageConstraintMap as $name => $constraint) { + if (!$this->lazyProvidersRepoContains(strtolower($name))) { + unset($packageConstraintMap[$name]); + } + } + } + $parser = new VersionParser(); /** * @param array $data @@ -700,8 +709,16 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $options['http']['content'] = http_build_query(['packages' => array_keys($packageConstraintMap)]); $response = $this->httpDownloader->get($apiUrl, $options); + $warned = false; /** @var string $name */ foreach ($response->decodeJson()['advisories'] as $name => $list) { + if (!isset($packageConstraintMap[$name])) { + if (!$warned) { + $this->io->writeError(''.$this->getRepoName().' returned names which were not requested in response to the security-advisories API. '.$name.' was not requested but is present in the response. Requested names were: '.implode(', ', array_keys($packageConstraintMap)).''); + $warned = true; + } + continue; + } if (count($list) > 0) { $advisories[$name] = array_filter(array_map( static function ($data) use ($name, $create) { @@ -1248,9 +1265,11 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito if (isset($data['security-advisories']) && is_array($data['security-advisories'])) { $this->securityAdvisoryConfig = [ 'metadata' => $data['security-advisories']['metadata'] ?? false, - 'api-url' => $data['security-advisories']['api-url'] ?? null, - 'query-all' => $data['security-advisories']['query-all'] ?? false, + 'api-url' => isset($data['security-advisories']['api-url']) && is_string($data['security-advisories']['api-url']) ? $this->canonicalizeUrl($data['security-advisories']['api-url']) : null, ]; + if ($this->securityAdvisoryConfig['api-url'] === null && !$this->hasAvailablePackageList) { + throw new \UnexpectedValueException('Invalid security advisory configuration on '.$this->getRepoName().': If the repository does not provide a security-advisories.api-url then available-packages or available-package-patterns are required to be provided for performance reason.'); + } } } @@ -1280,12 +1299,16 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } /** - * @param non-empty-string $url + * @param string $url * @return non-empty-string */ private function canonicalizeUrl(string $url): string { - if ('/' === $url[0]) { + if (strlen($url) === 0) { + throw new \InvalidArgumentException('Expected a string with a value and not an empty string'); + } + + if (str_starts_with($url, '/')) { if (Preg::isMatch('{^[^:]++://[^/]*+}', $this->url, $matches)) { return $matches[0] . $url; } diff --git a/src/Composer/Repository/Vcs/GitLabDriver.php b/src/Composer/Repository/Vcs/GitLabDriver.php index 1e13154c5..3721419b1 100644 --- a/src/Composer/Repository/Vcs/GitLabDriver.php +++ b/src/Composer/Repository/Vcs/GitLabDriver.php @@ -43,7 +43,7 @@ class GitLabDriver extends VcsDriver /** * @var mixed[] Project data returned by GitLab API */ - private $project; + private $project = null; /** * @var array Keeps commits returned by GitLab API as commit id => info @@ -381,6 +381,10 @@ class GitLabDriver extends VcsDriver protected function fetchProject(): void { + if (!is_null($this->project)) { + return; + } + // we need to fetch the default branch from the api $resource = $this->getApiUrl(); $this->project = $this->getContents($resource, true)->decodeJson(); @@ -581,6 +585,18 @@ class GitLabDriver extends VcsDriver return true; } + /** + * Gives back the loaded /projects// result + * + * @return mixed[]|null + */ + public function getRepoData(): ?array + { + $this->fetchProject(); + + return $this->project; + } + protected function getNextPage(Response $response): ?string { $header = $response->getHeader('link'); diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index 967aed2e7..f5bbe24aa 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -28,7 +28,7 @@ use React\Promise\Promise; * @internal * @author Jordi Boggiano * @author Nicolas Grekas - * @phpstan-type Attributes array{retryAuthFailure: bool, redirects: int<0, max>, retries: int<0, max>, storeAuth: 'prompt'|bool} + * @phpstan-type Attributes array{retryAuthFailure: bool, redirects: int<0, max>, retries: int<0, max>, storeAuth: 'prompt'|bool, ipResolve: 4|6|null} * @phpstan-type Job array{url: non-empty-string, origin: string, attributes: Attributes, options: mixed[], progress: mixed[], curlHandle: \CurlHandle, filename: string|null, headerHandle: resource, bodyHandle: resource, resolve: callable, reject: callable} */ class CurlDownloader @@ -143,7 +143,7 @@ class CurlDownloader /** * @param mixed[] $options * - * @param array{retryAuthFailure?: bool, redirects?: int<0, max>, retries?: int<0, max>, storeAuth?: 'prompt'|bool} $attributes + * @param array{retryAuthFailure?: bool, redirects?: int<0, max>, retries?: int<0, max>, storeAuth?: 'prompt'|bool, ipResolve?: 4|6|null} $attributes * @param non-empty-string $url * * @return int internal job id @@ -155,8 +155,15 @@ class CurlDownloader 'redirects' => 0, 'retries' => 0, 'storeAuth' => false, + 'ipResolve' => null, ], $attributes); + if ($attributes['ipResolve'] === null && Platform::getEnv('COMPOSER_IPRESOLVE') === '4') { + $attributes['ipResolve'] = 4; + } elseif ($attributes['ipResolve'] === null && Platform::getEnv('COMPOSER_IPRESOLVE') === '6') { + $attributes['ipResolve'] = 6; + } + $originalOptions = $options; // check URL can be accessed (i.e. is not insecure), but allow insecure Packagist calls to $hashed providers as file integrity is verified with sha256 @@ -199,6 +206,12 @@ class CurlDownloader curl_setopt($curlHandle, CURLOPT_ENCODING, ""); // let cURL set the Accept-Encoding header to what it supports curl_setopt($curlHandle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); + if ($attributes['ipResolve'] === 4) { + curl_setopt($curlHandle, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); + } elseif ($attributes['ipResolve'] === 6) { + curl_setopt($curlHandle, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6); + } + if (function_exists('curl_share_init')) { curl_setopt($curlHandle, CURLOPT_SHARE, $this->shareHandle); } @@ -352,8 +365,12 @@ class CurlDownloader || (in_array($errno, [56 /* CURLE_RECV_ERROR */, 35 /* CURLE_SSL_CONNECT_ERROR */], true) && str_contains((string) $error, 'Connection reset by peer')) ) && $job['attributes']['retries'] < $this->maxRetries ) { + $attributes = ['retries' => $job['attributes']['retries'] + 1]; + if ($errno === 7 && !isset($job['attributes']['ipResolve'])) { // CURLE_COULDNT_CONNECT, retry forcing IPv4 if no IP stack was selected + $attributes['ipResolve'] = 4; + } $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]); + $this->restartJobWithDelay($job, $job['url'], $attributes); continue; } @@ -582,7 +599,7 @@ class CurlDownloader * @param Job $job * @param non-empty-string $url * - * @param array{retryAuthFailure?: bool, redirects?: int<0, max>, storeAuth?: 'prompt'|bool, retries?: int<1, max>} $attributes + * @param array{retryAuthFailure?: bool, redirects?: int<0, max>, storeAuth?: 'prompt'|bool, retries?: int<1, max>, ipResolve?: 4|6} $attributes */ private function restartJob(array $job, string $url, array $attributes = []): void { @@ -600,7 +617,7 @@ class CurlDownloader * @param Job $job * @param non-empty-string $url * - * @param array{retryAuthFailure?: bool, redirects?: int<0, max>, storeAuth?: 'prompt'|bool, retries: int<1, max>} $attributes + * @param array{retryAuthFailure?: bool, redirects?: int<0, max>, storeAuth?: 'prompt'|bool, retries: int<1, max>, ipResolve?: 4|6} $attributes */ private function restartJobWithDelay(array $job, string $url, array $attributes): void { diff --git a/tests/Composer/Test/Advisory/AuditorTest.php b/tests/Composer/Test/Advisory/AuditorTest.php index 656505844..2253169f4 100644 --- a/tests/Composer/Test/Advisory/AuditorTest.php +++ b/tests/Composer/Test/Advisory/AuditorTest.php @@ -54,6 +54,7 @@ class AuditorTest extends TestCase 'expected' => 1, 'output' => 'Found 2 security vulnerability advisories affecting 1 package: Package: vendor1/package1 +Severity: high CVE: CVE3 Title: advisory4 URL: https://advisory.example.com/advisory4 @@ -61,6 +62,7 @@ Affected versions: >=8,<8.2.2|>=1,<2.5.6 Reported at: 2022-05-25T13:21:00+00:00 -------- Package: vendor1/package1 +Severity: medium CVE: '.' Title: advisory5 URL: https://advisory.example.com/advisory5 @@ -169,6 +171,7 @@ Found 2 abandoned packages: [ ['text' => 'Found 1 ignored security vulnerability advisory affecting 1 package:'], ['text' => 'Package: vendor1/package1'], + ['text' => 'Severity: medium'], ['text' => 'CVE: CVE1'], ['text' => 'Title: advisory1'], ['text' => 'URL: https://advisory.example.com/advisory1'], @@ -185,6 +188,7 @@ Found 2 abandoned packages: [ ['text' => 'Found 1 ignored security vulnerability advisory affecting 1 package:'], ['text' => 'Package: vendor1/package1'], + ['text' => 'Severity: medium'], ['text' => 'CVE: CVE1'], ['text' => 'Title: advisory1'], ['text' => 'URL: https://advisory.example.com/advisory1'], @@ -202,6 +206,7 @@ Found 2 abandoned packages: [ ['text' => 'Found 1 ignored security vulnerability advisory affecting 1 package:'], ['text' => 'Package: vendor1/package2'], + ['text' => 'Severity: medium'], ['text' => 'CVE: '], ['text' => 'Title: advisory2'], ['text' => 'URL: https://advisory.example.com/advisory2'], @@ -218,6 +223,7 @@ Found 2 abandoned packages: [ ['text' => 'Found 1 ignored security vulnerability advisory affecting 1 package:'], ['text' => 'Package: vendorx/packagex'], + ['text' => 'Severity: medium'], ['text' => 'CVE: CVE5'], ['text' => 'Title: advisory17'], ['text' => 'URL: https://advisory.example.com/advisory17'], @@ -234,6 +240,7 @@ Found 2 abandoned packages: [ ['text' => 'Found 1 security vulnerability advisory affecting 1 package:'], ['text' => 'Package: vendor1/package1'], + ['text' => 'Severity: medium'], ['text' => 'CVE: CVE1'], ['text' => 'Title: advisory1'], ['text' => 'URL: https://advisory.example.com/advisory1'], @@ -254,6 +261,7 @@ Found 2 abandoned packages: [ ['text' => 'Found 3 ignored security vulnerability advisories affecting 2 packages:'], ['text' => 'Package: vendor2/package1'], + ['text' => 'Severity: medium'], ['text' => 'CVE: CVE2'], ['text' => 'Title: advisory3'], ['text' => 'URL: https://advisory.example.com/advisory3'], @@ -262,6 +270,7 @@ Found 2 abandoned packages: ['text' => 'Ignore reason: None specified'], ['text' => '--------'], ['text' => 'Package: vendor2/package1'], + ['text' => 'Severity: medium'], ['text' => 'CVE: CVE4'], ['text' => 'Title: advisory6'], ['text' => 'URL: https://advisory.example.com/advisory6'], @@ -270,6 +279,7 @@ Found 2 abandoned packages: ['text' => 'Ignore reason: None specified'], ['text' => '--------'], ['text' => 'Package: vendorx/packagex'], + ['text' => 'Severity: medium'], ['text' => 'CVE: CVE5'], ['text' => 'Title: advisory17'], ['text' => 'URL: https://advisory.example.com/advisory17'], @@ -278,6 +288,7 @@ Found 2 abandoned packages: ['text' => 'Ignore reason: None specified'], ['text' => 'Found 1 security vulnerability advisory affecting 1 package:'], ['text' => 'Package: vendor3/package1'], + ['text' => 'Severity: medium'], ['text' => 'CVE: CVE5'], ['text' => 'Title: advisory7'], ['text' => 'URL: https://advisory.example.com/advisory7'], @@ -380,6 +391,7 @@ Found 2 abandoned packages: ], 'reportedAt' => '2022-05-25 13:21:00', 'composerRepository' => 'https://packagist.org', + 'severity' => 'medium', ], [ 'advisoryId' => 'ID4', @@ -396,6 +408,7 @@ Found 2 abandoned packages: ], 'reportedAt' => '2022-05-25 13:21:00', 'composerRepository' => 'https://packagist.org', + 'severity' => 'high', ], [ 'advisoryId' => 'ID5', @@ -412,6 +425,7 @@ Found 2 abandoned packages: ], 'reportedAt' => '2022-05-25 13:21:00', 'composerRepository' => 'https://packagist.org', + 'severity' => 'medium', ], ], 'vendor1/package2' => [ @@ -430,6 +444,7 @@ Found 2 abandoned packages: ], 'reportedAt' => '2022-05-25 13:21:00', 'composerRepository' => 'https://packagist.org', + 'severity' => 'medium', ], ], 'vendorx/packagex' => [ @@ -448,6 +463,7 @@ Found 2 abandoned packages: ], 'reportedAt' => '2015-05-25 13:21:00', 'composerRepository' => 'https://packagist.org', + 'severity' => 'medium', ], ], 'vendor2/package1' => [ @@ -466,6 +482,7 @@ Found 2 abandoned packages: ], 'reportedAt' => '2022-05-25 13:21:00', 'composerRepository' => 'https://packagist.org', + 'severity' => 'medium', ], [ 'advisoryId' => 'ID6', @@ -482,6 +499,7 @@ Found 2 abandoned packages: ], 'reportedAt' => '2015-05-25 13:21:00', 'composerRepository' => 'https://packagist.org', + 'severity' => 'medium', ], ], 'vendory/packagey' => [ @@ -500,6 +518,7 @@ Found 2 abandoned packages: ], 'reportedAt' => '2015-05-25 13:21:00', 'composerRepository' => 'https://packagist.org', + 'severity' => 'medium', ], ], 'vendor3/package1' => [ @@ -518,6 +537,7 @@ Found 2 abandoned packages: ], 'reportedAt' => '2015-05-25 13:21:00', 'composerRepository' => 'https://packagist.org', + 'severity' => 'medium', ], ], ]; diff --git a/tests/Composer/Test/Command/BumpCommandTest.php b/tests/Composer/Test/Command/BumpCommandTest.php index 8489ef017..383aaf90c 100644 --- a/tests/Composer/Test/Command/BumpCommandTest.php +++ b/tests/Composer/Test/Command/BumpCommandTest.php @@ -76,7 +76,7 @@ class BumpCommandTest extends TestCase yield 'bump all by default' => [ [ 'require' => [ - 'first/pkg' => '^2.0', + 'first/pkg' => '^v2.0', 'second/pkg' => '3.*', ], 'require-dev' => [ diff --git a/tests/Composer/Test/Command/ConfigCommandTest.php b/tests/Composer/Test/Command/ConfigCommandTest.php index 10dd170f2..68653c622 100644 --- a/tests/Composer/Test/Command/ConfigCommandTest.php +++ b/tests/Composer/Test/Command/ConfigCommandTest.php @@ -13,6 +13,7 @@ namespace Composer\Test\Command; use Composer\Test\TestCase; +use RuntimeException; class ConfigCommandTest extends TestCase { @@ -139,4 +140,13 @@ class ConfigCommandTest extends TestCase '{"foo":{"type":"vcs","url":"https://example.org"}}', ]; } + + public function testConfigThrowsForInvalidArgCombination(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('--file and --global can not be combined'); + + $appTester = $this->getApplicationTester(); + $appTester->run(['command' => 'config', '--file' => 'alt.composer.json', '--global' => true]); + } } diff --git a/tests/Composer/Test/Command/ShowCommandTest.php b/tests/Composer/Test/Command/ShowCommandTest.php index 9601205d9..acab3ccc9 100644 --- a/tests/Composer/Test/Command/ShowCommandTest.php +++ b/tests/Composer/Test/Command/ShowCommandTest.php @@ -17,6 +17,8 @@ use Composer\Pcre\Preg; use Composer\Pcre\Regex; use Composer\Repository\PlatformRepository; use Composer\Test\TestCase; +use DateTimeImmutable; +use InvalidArgumentException; class ShowCommandTest extends TestCase { @@ -28,6 +30,8 @@ class ShowCommandTest extends TestCase public function testShow(array $command, string $expected, array $requires = []): void { $this->initTempComposer([ + 'name' => 'root/pkg', + 'version' => '1.2.3', 'repositories' => [ 'packages' => [ 'type' => 'package', @@ -55,12 +59,19 @@ class ShowCommandTest extends TestCase $pkg = self::getPackage('vendor/package', '1.0.0'); $pkg->setDescription('description of installed package'); + $major = self::getPackage('outdated/major', '1.0.0'); + $major->setReleaseDate(new DateTimeImmutable()); + $minor = self::getPackage('outdated/minor', '1.0.0'); + $minor->setReleaseDate(new DateTimeImmutable('-2 years')); + $patch = self::getPackage('outdated/patch', '1.0.0'); + $patch->setReleaseDate(new DateTimeImmutable('-2 weeks')); - $this->createInstalledJson([ + $this->createInstalledJson([$pkg, $major, $minor, $patch]); + + $pkg = self::getPackage('vendor/locked', '3.0.0'); + $pkg->setDescription('description of locked package'); + $this->createComposerLock([ $pkg, - self::getPackage('outdated/major', '1.0.0'), - self::getPackage('outdated/minor', '1.0.0'), - self::getPackage('outdated/patch', '1.0.0'), ]); $appTester = $this->getApplicationTester(); @@ -78,6 +89,21 @@ outdated/patch 1.0.0 vendor/package 1.0.0 description of installed package', ]; + yield 'with -s and --installed shows list of installed + self package' => [ + ['--installed' => true, '--self' => true], +'outdated/major 1.0.0 +outdated/minor 1.0.0 +outdated/patch 1.0.0 +root/pkg 1.2.3 +vendor/package 1.0.0 description of installed package', + ]; + + yield 'with -s and --locked shows list of installed + self package' => [ + ['--locked' => true, '--self' => true], +'root/pkg 1.2.3 +vendor/locked 3.0.0 description of locked package', + ]; + yield 'with -a show available packages with description but no version' => [ ['-a' => true], 'outdated/major outdated/major v2.0.0 description @@ -112,6 +138,21 @@ outdated/minor 1.0.0 ! 1.1.1 outdated/patch 1.0.0 ! 1.0.1', ]; + yield 'outdated deps sorting by age' => [ + ['command' => 'outdated', '--sort-by-age' => true], +'Legend: +! patch or minor release available - update recommended +~ major release available - update possible + +Direct dependencies required in composer.json: +Everything up to date + +Transitive dependencies not required in composer.json: +outdated/minor 1.0.0 ! 1.1.1 2 years old +outdated/patch 1.0.0 ! 1.0.1 2 weeks old +outdated/major 1.0.0 ~ 2.0.0 from today', + ]; + yield 'outdated deps with --direct only show direct deps with updated' => [ ['command' => 'outdated', '--direct' => true], 'Legend: @@ -258,6 +299,23 @@ Transitive dependencies not required in composer.json: vendor/package 1.1.0 ! 1.2.0", trim($appTester->getDisplay(true))); } + public function testShowDirectWithNameOnlyShowsDirectDependents(): void + { + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage('Package "vendor/package" is installed but not a direct dependent of the root package.'); + + $this->initTempComposer([ + 'repositories' => [], + ]); + + $this->createInstalledJson([ + self::getPackage('vendor/package', '1.0.0'), + ]); + + $appTester = $this->getApplicationTester(); + $appTester->run(['command' => 'show', '--direct' => true, 'package' => 'vendor/package']); + } + public function testShowPlatformOnlyShowsPlatformPackages(): void { $this->initTempComposer([ @@ -533,7 +591,7 @@ OUTPUT; public function testSelf(): void { - $this->initTempComposer(['name' => 'vendor/package']); + $this->initTempComposer(['name' => 'vendor/package', 'time' => date('Y-m-d')]); $appTester = $this->getApplicationTester(); $appTester->run(['command' => 'show', '--self' => true]); @@ -542,6 +600,7 @@ OUTPUT; 'descrip.' => '', 'keywords' => '', 'versions' => '* 1.0.0+no-version-set', + 'released' => date('Y-m-d'). ', today', 'type' => 'library', 'homepage' => '', 'source' => '[] ', diff --git a/tests/Composer/Test/Command/UpdateCommandTest.php b/tests/Composer/Test/Command/UpdateCommandTest.php index cf84015e1..fce735087 100644 --- a/tests/Composer/Test/Command/UpdateCommandTest.php +++ b/tests/Composer/Test/Command/UpdateCommandTest.php @@ -29,7 +29,7 @@ class UpdateCommandTest extends TestCase $appTester = $this->getApplicationTester(); $appTester->run(array_merge(['command' => 'update', '--dry-run' => true, '--no-audit' => true], $command)); - $this->assertSame(trim($expected), trim($appTester->getDisplay(true))); + $this->assertStringMatchesFormat(trim($expected), trim($appTester->getDisplay(true))); } public static function provideUpdates(): \Generator @@ -67,6 +67,29 @@ Package operations: 2 installs, 0 updates, 0 removals OUTPUT ]; + yield 'simple update with very verbose output' => [ + $rootDepAndTransitiveDep, + ['-vv' => true], + << [ $rootDepAndTransitiveDep, ['--with' => ['dep/pkg:1.0.0'], '--no-install' => true], diff --git a/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreAllPlatformRequirementFilterTest.php b/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreAllPlatformRequirementFilterTest.php index 8df56d81d..8d148ec7c 100644 --- a/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreAllPlatformRequirementFilterTest.php +++ b/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreAllPlatformRequirementFilterTest.php @@ -25,6 +25,7 @@ final class IgnoreAllPlatformRequirementFilterTest extends TestCase $platformRequirementFilter = new IgnoreAllPlatformRequirementFilter(); $this->assertSame($expectIgnored, $platformRequirementFilter->isIgnored($req)); + $this->assertSame($expectIgnored, $platformRequirementFilter->isUpperBoundIgnored($req)); } /** diff --git a/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreListPlatformRequirementFilterTest.php b/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreListPlatformRequirementFilterTest.php index a3a588cef..5f3cc1f51 100644 --- a/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreListPlatformRequirementFilterTest.php +++ b/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreListPlatformRequirementFilterTest.php @@ -48,4 +48,37 @@ final class IgnoreListPlatformRequirementFilterTest extends TestCase 'list entries are not completing each other' => [['ext-', 'foo'], 'ext-foo', false], ]; } + + /** + * @dataProvider dataIsUpperBoundIgnored + * + * @param string[] $reqList + */ + public function testIsUpperBoundIgnored(array $reqList, string $req, bool $expectIgnored): void + { + $platformRequirementFilter = new IgnoreListPlatformRequirementFilter($reqList); + + $this->assertSame($expectIgnored, $platformRequirementFilter->isUpperBoundIgnored($req)); + } + + /** + * @return array + */ + public static function dataIsUpperBoundIgnored(): array + { + return [ + 'ext-json is ignored if listed and fully ignored' => [['ext-json', 'monolog/monolog'], 'ext-json', true], + 'ext-json is ignored if listed and upper bound ignored' => [['ext-json+', 'monolog/monolog'], 'ext-json', true], + 'php is not ignored if not listed' => [['ext-json+', 'monolog/monolog'], 'php', false], + 'monolog/monolog is not ignored even if listed' => [['monolog/monolog'], 'monolog/monolog', false], + 'ext-json is ignored if ext-* is listed' => [['ext-*+'], 'ext-json', true], + 'php is ignored if php* is listed' => [['ext-*+', 'php*+'], 'php', true], + 'ext-json is ignored if * is listed' => [['foo', '*+'], 'ext-json', true], + 'php is ignored if * is listed' => [['*+', 'foo'], 'php', true], + 'monolog/monolog is not ignored even if * or monolog/* are listed' => [['*+', 'monolog/*+'], 'monolog/monolog', false], + 'empty list entry does not ignore' => [[''], 'ext-foo', false], + 'empty array does not ignore' => [[], 'ext-foo', false], + 'list entries are not completing each other' => [['ext-', 'foo'], 'ext-foo', false], + ]; + } } diff --git a/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreNothingPlatformRequirementFilterTest.php b/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreNothingPlatformRequirementFilterTest.php index a1baac58b..70c3fabd2 100644 --- a/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreNothingPlatformRequirementFilterTest.php +++ b/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreNothingPlatformRequirementFilterTest.php @@ -25,6 +25,7 @@ final class IgnoreNothingPlatformRequirementFilterTest extends TestCase $platformRequirementFilter = new IgnoreNothingPlatformRequirementFilter(); $this->assertFalse($platformRequirementFilter->isIgnored($req)); // @phpstan-ignore-line + $this->assertFalse($platformRequirementFilter->isUpperBoundIgnored($req)); // @phpstan-ignore-line } /** diff --git a/tests/Composer/Test/Fixtures/installer/install-funding-notice-env.test b/tests/Composer/Test/Fixtures/installer/install-funding-notice-env.test new file mode 100644 index 000000000..c6f737f60 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/install-funding-notice-env.test @@ -0,0 +1,62 @@ +--TEST-- +Installs a simple package with exact match requirement +--CONDITION-- +putenv('COMPOSER_FUND=1') +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "a/a", + "version": "1.0.0", + "funding": [{ "type": "example", "url": "http://example.org/fund" }], + "require": { + "d/d": "^1.0" + } + }, + { + "name": "b/b", + "version": "1.0.0", + "funding": [{ "type": "example", "url": "http://example.org/fund" }] + }, + { + "name": "c/c", + "version": "1.0.0", + "funding": [{ "type": "example", "url": "http://example.org/fund" }] + }, + { + "name": "d/d", + "version": "1.0.0", + "require": { + "b/b": "^1.0" + } + } + ] + } + ], + "require": { + "a/a": "1.0.0" + } +} +--RUN-- +install +--EXPECT-OUTPUT-- +No composer.lock file present. Updating dependencies to latest instead of installing from lock file. See https://getcomposer.org/install for more information. +Loading composer repositories with package information +Updating dependencies +Lock file operations: 3 installs, 0 updates, 0 removals + - Locking a/a (1.0.0) + - Locking b/b (1.0.0) + - Locking d/d (1.0.0) +Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 3 installs, 0 updates, 0 removals +Generating autoload files +2 packages you are using are looking for funding. +Use the `composer fund` command to find out more! +--EXPECT-- +Installing b/b (1.0.0) +Installing d/d (1.0.0) +Installing a/a (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/install-funding-notice-not-displayed-env.test b/tests/Composer/Test/Fixtures/installer/install-funding-notice-not-displayed-env.test new file mode 100644 index 000000000..1f4ef8035 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/install-funding-notice-not-displayed-env.test @@ -0,0 +1,60 @@ +--TEST-- +Installs a simple package with exact match requirement +--CONDITION-- +putenv('COMPOSER_FUND=0') +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "a/a", + "version": "1.0.0", + "funding": [{ "type": "example", "url": "http://example.org/fund" }], + "require": { + "d/d": "^1.0" + } + }, + { + "name": "b/b", + "version": "1.0.0", + "funding": [{ "type": "example", "url": "http://example.org/fund" }] + }, + { + "name": "c/c", + "version": "1.0.0", + "funding": [{ "type": "example", "url": "http://example.org/fund" }] + }, + { + "name": "d/d", + "version": "1.0.0", + "require": { + "b/b": "^1.0" + } + } + ] + } + ], + "require": { + "a/a": "1.0.0" + } +} +--RUN-- +install +--EXPECT-OUTPUT-- +No composer.lock file present. Updating dependencies to latest instead of installing from lock file. See https://getcomposer.org/install for more information. +Loading composer repositories with package information +Updating dependencies +Lock file operations: 3 installs, 0 updates, 0 removals + - Locking a/a (1.0.0) + - Locking b/b (1.0.0) + - Locking d/d (1.0.0) +Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 3 installs, 0 updates, 0 removals +Generating autoload files +--EXPECT-- +Installing b/b (1.0.0) +Installing d/d (1.0.0) +Installing a/a (1.0.0) diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 511d649a8..858d88e7c 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -59,6 +59,7 @@ class InstallerTest extends TestCase { parent::tearDown(); Platform::clearEnv('COMPOSER_POOL_OPTIMIZER'); + Platform::clearEnv('COMPOSER_FUND'); chdir($this->prevCwd); if (isset($this->tempComposerHome) && is_dir($this->tempComposerHome)) { diff --git a/tests/Composer/Test/Package/Version/VersionBumperTest.php b/tests/Composer/Test/Package/Version/VersionBumperTest.php index 4d0b610c0..a146c2884 100644 --- a/tests/Composer/Test/Package/Version/VersionBumperTest.php +++ b/tests/Composer/Test/Package/Version/VersionBumperTest.php @@ -44,6 +44,7 @@ class VersionBumperTest extends TestCase { // constraint, version, expected recommendation, [branch-alias] yield 'upgrade caret' => ['^1.0', '1.2.1', '^1.2.1']; + yield 'upgrade caret with v' => ['^v1.0', '1.2.1', '^1.2.1']; yield 'skip trailing .0s' => ['^1.0', '1.0.0', '^1.0']; yield 'skip trailing .0s/2' => ['^1.2', '1.2.0', '^1.2']; yield 'preserve major.minor.patch format when installed minor is 0' => ['^1.0.0', '1.2.0', '^1.2.0']; @@ -58,6 +59,7 @@ class VersionBumperTest extends TestCase yield 'dev version does not upgrade' => ['^3.2', 'dev-main', '^3.2']; yield 'upgrade dev version if aliased' => ['^3.2', 'dev-main', '^3.3', '3.3.x-dev']; yield 'upgrade major wildcard to caret' => ['2.*', '2.4.0', '^2.4']; + yield 'upgrade major wildcard to caret with v' => ['v2.*', '2.4.0', '^2.4']; yield 'upgrade major wildcard as x to caret' => ['2.x', '2.4.0', '^2.4']; yield 'upgrade major wildcard as x to caret/2' => ['2.x.x', '2.4.0', '^2.4.0']; yield 'leave minor wildcard alone' => ['2.4.*', '2.4.3', '2.4.*']; @@ -66,6 +68,7 @@ class VersionBumperTest extends TestCase yield 'update patch-only-tilde alone' => ['~2.2.3', '2.2.6', '~2.2.6']; yield 'leave extra-only-tilde alone' => ['~2.2.3.1', '2.2.4.5', '~2.2.3.1']; yield 'upgrade bigger-or-eq to latest' => ['>=3.0', '3.4.5', '>=3.4.5']; + yield 'upgrade bigger-or-eq to latest with v' => ['>=v3.0', '3.4.5', '>=3.4.5']; yield 'leave bigger-than untouched' => ['>2.2.3', '2.2.6', '>2.2.3']; yield 'upgrade full wildcard to bigger-or-eq' => ['*', '1.2.3', '>=1.2.3']; } diff --git a/tests/Composer/Test/Package/Version/VersionSelectorTest.php b/tests/Composer/Test/Package/Version/VersionSelectorTest.php index e5d21d7a2..aec5cf86c 100644 --- a/tests/Composer/Test/Package/Version/VersionSelectorTest.php +++ b/tests/Composer/Test/Package/Version/VersionSelectorTest.php @@ -346,6 +346,9 @@ class VersionSelectorTest extends TestCase ['0.1.3', '^0.1.3'], ['0.0.3', '^0.0.3'], ['0.0.3-alpha', '^0.0.3@alpha'], + ['0.0.3.4-alpha', '^0.0.3@alpha'], + ['3.0.0.2-RC2', '^3.0@RC'], + ['1.2.1.1020402', '^1.2'], // date-based versions are not touched at all ['v20121020', 'v20121020'], ['v20121020.2', 'v20121020.2'],