diff --git a/.gitattributes b/.gitattributes index bd516c568f47..bf7e4b39e1cb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,7 +11,7 @@ # Exclude non-essential files from dist /.github/ export-ignore /doc export-ignore -/phpstan/ export-ignore +/phpstan/* export-ignore /tests/ export-ignore /.editorconfig export-ignore /.gitattributes export-ignore diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 78b4b798be5a..bfe1dc9d5511 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -44,10 +44,6 @@ To achieve this, you need to acquire the Composer source code: You can run the test suite by executing `vendor/bin/simple-phpunit` when inside the composer directory, and run Composer by executing the `bin/composer`. -For running the tests against the most recent PHP versions (PHP 8.0/8.1), you will -need to run `composer update --ignore-platform-reqs && git checkout composer.lock` before running -the `vendor/bin/simple-phpunit` command. - To test your modified Composer code against another project, run `php /path/to/composer/bin/composer` inside that project's directory. diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 1e3f99b46a89..5412f0af7937 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -30,6 +30,8 @@ jobs: - "7.4" - "8.0" - "8.1" + - "8.2" + - "8.3" dependencies: [locked] os: [ubuntu-latest] experimental: [false] @@ -54,14 +56,14 @@ jobs: os: macos-latest dependencies: locked experimental: false - - php-version: "8.2" + - php-version: "8.3" dependencies: lowest-ignore os: ubuntu-latest - experimental: true - - php-version: "8.2" + experimental: false + - php-version: "8.3" dependencies: highest-ignore os: ubuntu-latest - experimental: true + experimental: false steps: - name: "Checkout" diff --git a/CHANGELOG.md b/CHANGELOG.md index 79ac978a6afa..821a32b6e09a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,44 @@ +### [2.6.2] 2023-09-03 + + * Reverted "Fixed binary proxies causing scripts inspecting `$_SERVER['SCRIPT_NAME']` to detect them, they are now more transparent (#11562)" which caused a regression (#11617) + * Fixed non-zero exit code on failed audits to only apply to `install --audit` runs and not implicit audits with `require`, `create-project` or `update` commands (#11616) + * Fixed `create-project` infinite post-install loop in some circumstances (#11613) + +### [2.6.1] 2023-09-01 + + * Reverted "Fixed executability of non-php binaries which are not marked executable (#11557)" which caused a regression (#11612) + +### [2.6.0] 2023-09-01 + + * Added audit.ignore config setting to ignore security advisories by id or CVE id (#11556, #11605) + * Added `rm` alias to the `remove` command (#11367) + * Added runtime platform check to verify the php-64bit requirement is met (#11334) + * Added platform package detection for lib-pq-libpq and lib-rdkafka-librdkafka (#11418) + * Added `--dry-run` to `dump-autoload` command to allow running --strict-psr checks without modifying the filesystem (#11608) + * Added support for `bump`ing patch level in `~1.2.3` constraints (#11590) + * Added prompt in `require` if the package name is not found but similar ones exist (#11284) + * Added support for env vars and `~` in repository paths for vcs and artifact repositories (#11453) + * Added support for local directory paths for repositories of type `composer` (#11526) + * Added links to package homepages in `why`/`why-not` command output (#11308) + * Added a `security` key to the `support` key of composer.json to set the URL to the vulnerability disclosure policy (#11271) + * Added support for gathering security advisories from multiple repositories for a single package (#11436) + * Fixed `install` exit code to be non-zero (5) if a requested security audit failed (#11362) + * ~~Fixed binary proxies causing scripts inspecting `$_SERVER['SCRIPT_NAME']` to detect them, they are now more transparent (#11562)~~ (Reverted in 2.6.2) + * ~~Fixed executability of non-php binaries which are not marked executable (#11557)~~ (Reverted in 2.6.1) + * Fixed `mtime` modification of the vendor dir to only happen when packages are modified, and not require lock file modification to happen (#11593) + * Fixed `create-project` using the wrong composer.json file if one was set via the `COMPOSER` env var (#11493) + * Fixed json editing to preserve indentation when updating json files (#11390) + * Fixed handling of broken junctions on windows (#11550) + * Fixed parsing of lib-curl-openssl version with OSX SecureTransport (#11534) + * Fixed svn repo parsing in some edge cases (#11350) + * Fixed handling of archive URLs without file extension (#11520) + * Performance improvement in pool optimization step (#11449, #11450) + ### [2.5.8] 2023-06-09 * Fixed regression in edge cases where root package gets added to a repository already during the install process (#11495) * Fixed EventDispatcher on windows picking bat files when using "@php binary" (#11490) - * Fixed ICU CDLR version parsing failing the whole process when ICU cannot initialize the resource bundle (#11492) + * Fixed ICU CLDR version parsing failing the whole process when ICU cannot initialize the resource bundle (#11492) * Fixed type declarations on ClassLoader (#11500) ### [2.5.7] 2023-05-24 @@ -254,7 +290,7 @@ * Added abandoned flag to `show`/`outdated` commands JSON-formatted output (#10485) * Added config.reference option to `path` repositories to configure the way the reference is generated, and possibly reduce composer.lock conflicts (#10488) * Added automatic removal of allow-plugins rules when removing a plugin via the `remove` command (#10615) - * Added COMPOSER_IGNORE_PLATFOR_REQ & COMPOSER_IGNORE_PLATFOR_REQS env vars to configure the equivalent flags (#10616) + * Added `COMPOSER_IGNORE_PLATFORM_REQ` & `COMPOSER_IGNORE_PLATFORM_REQS` env vars to configure the equivalent flags (#10616) * Added support for Symfony 6.0 components * Added support for psr/log 3.x (#10454) * Fixed symlink creation in linux VM guest filesystems to be recognized by Windows (#10592) @@ -1731,6 +1767,9 @@ * Initial release +[2.6.2]: https://github.com/composer/composer/compare/2.6.1...2.6.2 +[2.6.1]: https://github.com/composer/composer/compare/2.6.0...2.6.1 +[2.6.0]: https://github.com/composer/composer/compare/2.5.8...2.6.0 [2.5.8]: https://github.com/composer/composer/compare/2.5.7...2.5.8 [2.5.7]: https://github.com/composer/composer/compare/2.5.6...2.5.7 [2.5.6]: https://github.com/composer/composer/compare/2.5.5...2.5.6 diff --git a/composer.json b/composer.json index 36dbfe22db11..70f48850667d 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 e43a5d2cbe69..4b5494312b6d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "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", - "version": "1.3.6", + "version": "1.3.7", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "90d087e988ff194065333d16bc5cf649872d9cdb" + "reference": "76e46335014860eec1aa5a724799a00a2e47cc85" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/90d087e988ff194065333d16bc5cf649872d9cdb", - "reference": "90d087e988ff194065333d16bc5cf649872d9cdb", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/76e46335014860eec1aa5a724799a00a2e47cc85", + "reference": "76e46335014860eec1aa5a724799a00a2e47cc85", "shasum": "" }, "require": { @@ -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.6" + "source": "https://github.com/composer/ca-bundle/tree/1.3.7" }, "funding": [ { @@ -80,26 +80,26 @@ "type": "tidelift" } ], - "time": "2023-06-06T12:02:59+00:00" + "time": "2023-08-30T09:31:38+00:00" }, { "name": "composer/class-map-generator", - "version": "1.0.0", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/composer/class-map-generator.git", - "reference": "1e1cb2b791facb2dfe32932a7718cf2571187513" + "reference": "953cc4ea32e0c31f2185549c7d216d7921f03da9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/class-map-generator/zipball/1e1cb2b791facb2dfe32932a7718cf2571187513", - "reference": "1e1cb2b791facb2dfe32932a7718cf2571187513", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/953cc4ea32e0c31f2185549c7d216d7921f03da9", + "reference": "953cc4ea32e0c31f2185549c7d216d7921f03da9", "shasum": "" }, "require": { - "composer/pcre": "^2 || ^3", + "composer/pcre": "^2.1 || ^3.1", "php": "^7.2 || ^8.0", - "symfony/finder": "^4.4 || ^5.3 || ^6" + "symfony/finder": "^4.4 || ^5.3 || ^6 || ^7" }, "require-dev": { "phpstan/phpstan": "^1.6", @@ -137,7 +137,7 @@ ], "support": { "issues": "https://github.com/composer/class-map-generator/issues", - "source": "https://github.com/composer/class-map-generator/tree/1.0.0" + "source": "https://github.com/composer/class-map-generator/tree/1.1.0" }, "funding": [ { @@ -153,7 +153,7 @@ "type": "tidelift" } ], - "time": "2022-06-19T11:31:27+00:00" + "time": "2023-06-30T13:58:57+00:00" }, { "name": "composer/metadata-minifier", @@ -297,16 +297,16 @@ }, { "name": "composer/semver", - "version": "3.3.2", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9" + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9", - "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9", + "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", "shasum": "" }, "require": { @@ -356,9 +356,9 @@ "versioning" ], "support": { - "irc": "irc://irc.freenode.org/composer", + "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.3.2" + "source": "https://github.com/composer/semver/tree/3.4.0" }, "funding": [ { @@ -374,7 +374,7 @@ "type": "tidelift" } ], - "time": "2022-04-01T19:23:25+00:00" + "time": "2023-08-31T09:50:34+00:00" }, { "name": "composer/spdx-licenses", @@ -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", @@ -937,16 +938,16 @@ }, { "name": "symfony/console", - "version": "v5.4.24", + "version": "v5.4.28", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "560fc3ed7a43e6d30ea94a07d77f9a60b8ed0fb8" + "reference": "f4f71842f24c2023b91237c72a365306f3c58827" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/560fc3ed7a43e6d30ea94a07d77f9a60b8ed0fb8", - "reference": "560fc3ed7a43e6d30ea94a07d77f9a60b8ed0fb8", + "url": "https://api.github.com/repos/symfony/console/zipball/f4f71842f24c2023b91237c72a365306f3c58827", + "reference": "f4f71842f24c2023b91237c72a365306f3c58827", "shasum": "" }, "require": { @@ -1016,7 +1017,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.24" + "source": "https://github.com/symfony/console/tree/v5.4.28" }, "funding": [ { @@ -1032,7 +1033,7 @@ "type": "tidelift" } ], - "time": "2023-05-26T05:13:16+00:00" + "time": "2023-08-07T06:12:30+00:00" }, { "name": "symfony/deprecation-contracts", @@ -1103,16 +1104,16 @@ }, { "name": "symfony/filesystem", - "version": "v5.4.23", + "version": "v5.4.25", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "b2f79d86cd9e7de0fff6d03baa80eaed7a5f38b5" + "reference": "0ce3a62c9579a53358d3a7eb6b3dfb79789a6364" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/b2f79d86cd9e7de0fff6d03baa80eaed7a5f38b5", - "reference": "b2f79d86cd9e7de0fff6d03baa80eaed7a5f38b5", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/0ce3a62c9579a53358d3a7eb6b3dfb79789a6364", + "reference": "0ce3a62c9579a53358d3a7eb6b3dfb79789a6364", "shasum": "" }, "require": { @@ -1147,7 +1148,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.23" + "source": "https://github.com/symfony/filesystem/tree/v5.4.25" }, "funding": [ { @@ -1163,20 +1164,20 @@ "type": "tidelift" } ], - "time": "2023-03-02T11:38:35+00:00" + "time": "2023-05-31T13:04:02+00:00" }, { "name": "symfony/finder", - "version": "v5.4.21", + "version": "v5.4.27", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "078e9a5e1871fcfe6a5ce421b539344c21afef19" + "reference": "ff4bce3c33451e7ec778070e45bd23f74214cd5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/078e9a5e1871fcfe6a5ce421b539344c21afef19", - "reference": "078e9a5e1871fcfe6a5ce421b539344c21afef19", + "url": "https://api.github.com/repos/symfony/finder/zipball/ff4bce3c33451e7ec778070e45bd23f74214cd5d", + "reference": "ff4bce3c33451e7ec778070e45bd23f74214cd5d", "shasum": "" }, "require": { @@ -1210,7 +1211,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.21" + "source": "https://github.com/symfony/finder/tree/v5.4.27" }, "funding": [ { @@ -1226,20 +1227,20 @@ "type": "tidelift" } ], - "time": "2023-02-16T09:33:00+00:00" + "time": "2023-07-31T08:02:31+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", "shasum": "" }, "require": { @@ -1254,7 +1255,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1292,7 +1293,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" }, "funding": [ { @@ -1308,20 +1309,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354" + "reference": "875e90aeea2777b6f135677f618529449334a612" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", + "reference": "875e90aeea2777b6f135677f618529449334a612", "shasum": "" }, "require": { @@ -1333,7 +1334,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1373,7 +1374,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" }, "funding": [ { @@ -1389,20 +1390,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", "shasum": "" }, "require": { @@ -1414,7 +1415,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1457,7 +1458,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" }, "funding": [ { @@ -1473,20 +1474,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + "reference": "42292d99c55abe617799667f454222c54c60e229" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", "shasum": "" }, "require": { @@ -1501,7 +1502,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1540,7 +1541,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" }, "funding": [ { @@ -1556,20 +1557,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-07-28T09:04:16+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9" + "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/9e8ecb5f92152187c4799efd3c96b78ccab18ff9", - "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fe2f306d1d9d346a7fee353d0d5012e401e984b5", + "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5", "shasum": "" }, "require": { @@ -1578,7 +1579,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1619,7 +1620,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.28.0" }, "funding": [ { @@ -1635,20 +1636,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", "shasum": "" }, "require": { @@ -1657,7 +1658,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1702,7 +1703,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" }, "funding": [ { @@ -1718,20 +1719,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a" + "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/707403074c8ea6e2edaf8794b0157a0bfa52157a", - "reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/7581cd600fa9fd681b797d00b02f068e2f13263b", + "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b", "shasum": "" }, "require": { @@ -1740,7 +1741,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1781,7 +1782,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.28.0" }, "funding": [ { @@ -1797,20 +1798,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/process", - "version": "v5.4.24", + "version": "v5.4.28", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "e3c46cc5689c8782944274bb30702106ecbe3b64" + "reference": "45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/e3c46cc5689c8782944274bb30702106ecbe3b64", - "reference": "e3c46cc5689c8782944274bb30702106ecbe3b64", + "url": "https://api.github.com/repos/symfony/process/zipball/45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b", + "reference": "45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b", "shasum": "" }, "require": { @@ -1843,7 +1844,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.24" + "source": "https://github.com/symfony/process/tree/v5.4.28" }, "funding": [ { @@ -1859,7 +1860,7 @@ "type": "tidelift" } ], - "time": "2023-05-17T11:26:05+00:00" + "time": "2023-08-07T10:36:04+00:00" }, { "name": "symfony/service-contracts", @@ -1946,16 +1947,16 @@ }, { "name": "symfony/string", - "version": "v5.4.22", + "version": "v5.4.26", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "8036a4c76c0dd29e60b6a7cafcacc50cf088ea62" + "reference": "1181fe9270e373537475e826873b5867b863883c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/8036a4c76c0dd29e60b6a7cafcacc50cf088ea62", - "reference": "8036a4c76c0dd29e60b6a7cafcacc50cf088ea62", + "url": "https://api.github.com/repos/symfony/string/zipball/1181fe9270e373537475e826873b5867b863883c", + "reference": "1181fe9270e373537475e826873b5867b863883c", "shasum": "" }, "require": { @@ -2012,7 +2013,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.22" + "source": "https://github.com/symfony/string/tree/v5.4.26" }, "funding": [ { @@ -2028,22 +2029,22 @@ "type": "tidelift" } ], - "time": "2023-03-14T06:11:53+00:00" + "time": "2023-06-28T12:46:07+00:00" } ], "packages-dev": [ { "name": "phpstan/phpstan", - "version": "1.10.16", + "version": "1.10.32", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "352bdbb960bb523e3d71b834862589f910921c23" + "reference": "c47e47d3ab03137c0e121e77c4d2cb58672f6d44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/352bdbb960bb523e3d71b834862589f910921c23", - "reference": "352bdbb960bb523e3d71b834862589f910921c23", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c47e47d3ab03137c0e121e77c4d2cb58672f6d44", + "reference": "c47e47d3ab03137c0e121e77c4d2cb58672f6d44", "shasum": "" }, "require": { @@ -2092,25 +2093,25 @@ "type": "tidelift" } ], - "time": "2023-06-05T08:21:46+00:00" + "time": "2023-08-24T21:54:50+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", - "version": "1.1.3", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", - "reference": "a22b36b955a2e9a3d39fe533b6c1bb5359f9c319" + "reference": "089d8a8258ed0aeefdc7b68b6c3d25572ebfdbaa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/a22b36b955a2e9a3d39fe533b6c1bb5359f9c319", - "reference": "a22b36b955a2e9a3d39fe533b6c1bb5359f9c319", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/089d8a8258ed0aeefdc7b68b6c3d25572ebfdbaa", + "reference": "089d8a8258ed0aeefdc7b68b6c3d25572ebfdbaa", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.10" + "phpstan/phpstan": "^1.10.3" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", @@ -2138,22 +2139,22 @@ "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", "support": { "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", - "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/1.1.3" + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/1.1.4" }, - "time": "2023-03-17T07:50:08+00:00" + "time": "2023-08-05T09:02:04+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "1.3.13", + "version": "1.3.14", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "d8bdab0218c5eb0964338d24a8511b65e9c94fa5" + "reference": "614acc10c522e319639bf38b0698a4a566665f04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/d8bdab0218c5eb0964338d24a8511b65e9c94fa5", - "reference": "d8bdab0218c5eb0964338d24a8511b65e9c94fa5", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/614acc10c522e319639bf38b0698a4a566665f04", + "reference": "614acc10c522e319639bf38b0698a4a566665f04", "shasum": "" }, "require": { @@ -2166,7 +2167,7 @@ "require-dev": { "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-strict-rules": "^1.0", + "phpstan/phpstan-strict-rules": "^1.5.1", "phpunit/phpunit": "^9.5" }, "type": "phpstan-extension", @@ -2190,9 +2191,9 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.13" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.14" }, - "time": "2023-05-26T11:05:59+00:00" + "time": "2023-08-25T09:46:39+00:00" }, { "name": "phpstan/phpstan-strict-rules", @@ -2316,16 +2317,16 @@ }, { "name": "symfony/phpunit-bridge", - "version": "v6.3.0", + "version": "v6.3.2", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "f8d75b4d9bf7243979b2c2e5e6cd73f03e10579f" + "reference": "e020e1efbd1b42cb670fcd7d19a25abbddba035d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/f8d75b4d9bf7243979b2c2e5e6cd73f03e10579f", - "reference": "f8d75b4d9bf7243979b2c2e5e6cd73f03e10579f", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/e020e1efbd1b42cb670fcd7d19a25abbddba035d", + "reference": "e020e1efbd1b42cb670fcd7d19a25abbddba035d", "shasum": "" }, "require": { @@ -2377,7 +2378,7 @@ "description": "Provides utilities for PHPUnit, especially user deprecation notices management", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/phpunit-bridge/tree/v6.3.0" + "source": "https://github.com/symfony/phpunit-bridge/tree/v6.3.2" }, "funding": [ { @@ -2393,7 +2394,7 @@ "type": "tidelift" } ], - "time": "2023-05-30T09:01:24+00:00" + "time": "2023-07-12T16:00:22+00:00" } ], "aliases": [], @@ -2408,5 +2409,5 @@ "platform-overrides": { "php": "7.2.5" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/doc/03-cli.md b/doc/03-cli.md index d11265782ab5..dd6e7001e869 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -74,7 +74,7 @@ php composer.phar init * **--repository:** Provide one (or more) custom repositories. They will be stored in the generated composer.json, and used for auto-completion when prompting for the list of requires. Every repository can be either an HTTP URL pointing - to a `composer` repository or a JSON string which similar to what the + to a `composer` repository or a JSON string which is similar to what the [repositories](04-schema.md#repositories) key accepts. * **--autoload (-a):** Add a PSR-4 autoload mapping to the composer.json. Automatically maps your package's namespace to the provided directory. (Expects a relative path, e.g. src/) See also [PSR-4 autoload](04-schema.md#psr-4). @@ -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/04-schema.md b/doc/04-schema.md index de22c551a2a8..95625053924c 100644 --- a/doc/04-schema.md +++ b/doc/04-schema.md @@ -91,7 +91,7 @@ Out of the box, Composer supports four types: - **library:** This is the default. It will copy the files to `vendor`. - **project:** This denotes a project rather than a library. For example application shells like the [Symfony standard edition](https://github.com/symfony/symfony-standard), - CMSs like the [SilverStripe installer](https://github.com/silverstripe/silverstripe-installer) + CMSs like the [Silverstripe installer](https://github.com/silverstripe/silverstripe-installer) or full fledged applications distributed as packages. This can for example be used by IDEs to provide listings of projects to initialize when creating a new workspace. diff --git a/doc/05-repositories.md b/doc/05-repositories.md index d88635b114c1..b6d5beb021a0 100644 --- a/doc/05-repositories.md +++ b/doc/05-repositories.md @@ -225,7 +225,7 @@ This field is optional. #### list The `list` field allows you to return the names of packages which match a -given field (or all names if no filter is present). It should accept an +given filter (or all names if no filter is present). It should accept an optional `?filter=xx` query param, which can contain `*` as wildcards matching any substring. diff --git a/doc/06-config.md b/doc/06-config.md index 70caf4432c7f..61391e2763a0 100644 --- a/doc/06-config.md +++ b/doc/06-config.md @@ -101,6 +101,40 @@ 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 + +### ignore + +A list of advisory ids, remote ids or CVE ids that are reported but let the audit command pass. + +```json +{ + "config": { + "audit": { + "ignore": { + "CVE-1234": "The affected component is not in use.", + "GHSA-xx": "The security fix was applied as a patch.", + "PKSA-yy": "Due to mitigations in place the update can be delayed." + } + } + } +} +``` + +or + +```json +{ + "config": { + "audit": { + "ignore": ["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-8.1.neon b/phpstan/baseline-8.1.neon index 29e52ed7ce9d..26137b6ca80c 100644 --- a/phpstan/baseline-8.1.neon +++ b/phpstan/baseline-8.1.neon @@ -85,11 +85,6 @@ parameters: count: 1 path: ../src/Composer/Downloader/GzipDownloader.php - - - message: "#^Parameter \\#1 \\$string of function rawurldecode expects string, string\\|false given\\.$#" - count: 1 - path: ../src/Composer/Downloader/VcsDownloader.php - - message: "#^Parameter \\#3 \\$length of function substr expects int\\|null, int\\<0, max\\>\\|false given\\.$#" count: 1 diff --git a/phpstan/baseline.neon b/phpstan/baseline.neon index 3597f9fee288..73f87c38878a 100644 --- a/phpstan/baseline.neon +++ b/phpstan/baseline.neon @@ -5,6 +5,11 @@ parameters: count: 1 path: ../src/Composer/Advisory/Auditor.php + - + message: "#^Variable \\$affectedPackagesCount might not be defined\\.$#" + count: 1 + path: ../src/Composer/Advisory/Auditor.php + - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" count: 10 @@ -645,26 +650,11 @@ parameters: count: 1 path: ../src/Composer/Command/ShowCommand.php - - - message: "#^Cannot call method getId\\(\\) on Composer\\\\Package\\\\BasePackage\\|int\\.$#" - count: 1 - path: ../src/Composer/Command/ShowCommand.php - - message: "#^Cannot call method getInstallationManager\\(\\) on Composer\\\\Composer\\|null\\.$#" count: 2 path: ../src/Composer/Command/ShowCommand.php - - - message: "#^Cannot call method getPrettyVersion\\(\\) on Composer\\\\Package\\\\BasePackage\\|int\\.$#" - count: 1 - path: ../src/Composer/Command/ShowCommand.php - - - - message: "#^Cannot call method getVersion\\(\\) on Composer\\\\Package\\\\BasePackage\\|int\\.$#" - count: 1 - path: ../src/Composer/Command/ShowCommand.php - - message: "#^Foreach overwrites \\$packages with its value variable\\.$#" count: 1 @@ -675,11 +665,6 @@ parameters: count: 1 path: ../src/Composer/Command/ShowCommand.php - - - message: "#^Method Composer\\\\Command\\\\ShowCommand\\:\\:getPackage\\(\\) should return array\\{Composer\\\\Package\\\\CompletePackageInterface\\|null, array\\\\} but returns array\\{Composer\\\\Package\\\\BasePackage\\|int\\|null, array\\\\}\\.$#" - count: 1 - path: ../src/Composer/Command/ShowCommand.php - - message: "#^Only booleans are allowed in &&, Composer\\\\Composer\\|null given on the right side\\.$#" count: 1 @@ -720,11 +705,6 @@ parameters: count: 2 path: ../src/Composer/Command/ShowCommand.php - - - message: "#^Only booleans are allowed in a negated boolean, Composer\\\\Package\\\\BasePackage\\|int\\|null given\\.$#" - count: 1 - path: ../src/Composer/Command/ShowCommand.php - - message: "#^Only booleans are allowed in a negated boolean, Composer\\\\Repository\\\\RepositorySet\\|null given\\.$#" count: 1 @@ -805,11 +785,6 @@ parameters: count: 2 path: ../src/Composer/Command/ShowCommand.php - - - message: "#^Parameter \\#1 \\$package of method Composer\\\\Repository\\\\CompositeRepository\\:\\:hasPackage\\(\\) expects Composer\\\\Package\\\\PackageInterface, Composer\\\\Package\\\\BasePackage\\|int given\\.$#" - count: 1 - path: ../src/Composer/Command/ShowCommand.php - - message: "#^Parameter \\#1 \\$str of function strtok expects string, array\\|string given\\.$#" count: 1 @@ -1695,6 +1670,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 @@ -1930,36 +1910,11 @@ parameters: count: 1 path: ../src/Composer/Downloader/VcsDownloader.php - - - message: "#^Parameter \\#1 \\$haystack of function strpos expects string, string\\|false given\\.$#" - count: 2 - path: ../src/Composer/Downloader/VcsDownloader.php - - message: "#^Parameter \\#1 \\$package of method Composer\\\\Downloader\\\\VcsDownloader\\:\\:cleanChanges\\(\\) expects Composer\\\\Package\\\\PackageInterface, Composer\\\\Package\\\\PackageInterface\\|null given\\.$#" count: 1 path: ../src/Composer/Downloader/VcsDownloader.php - - - message: "#^Parameter \\#1 \\$path of function realpath expects string, string\\|false given\\.$#" - count: 1 - path: ../src/Composer/Downloader/VcsDownloader.php - - - - message: "#^Parameter \\#1 \\$path of static method Composer\\\\Util\\\\Filesystem\\:\\:isLocalPath\\(\\) expects string, string\\|false given\\.$#" - count: 1 - path: ../src/Composer/Downloader/VcsDownloader.php - - - - message: "#^Parameter \\#1 \\$str of function rawurldecode expects string, string\\|false given\\.$#" - count: 1 - path: ../src/Composer/Downloader/VcsDownloader.php - - - - message: "#^Parameter \\#1 \\$string of function substr expects string, string\\|false given\\.$#" - count: 1 - path: ../src/Composer/Downloader/VcsDownloader.php - - message: "#^Parameter \\#2 \\$toReference of method Composer\\\\Downloader\\\\VcsDownloader\\:\\:getCommitLogs\\(\\) expects string, string\\|null given\\.$#" count: 1 @@ -2360,11 +2315,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 @@ -2500,11 +2450,6 @@ parameters: count: 1 path: ../src/Composer/Installer/SuggestedPackagesReporter.php - - - message: "#^Method Composer\\\\Json\\\\JsonFile\\:\\:encode\\(\\) should return string but returns string\\|false\\.$#" - count: 1 - path: ../src/Composer/Json/JsonFile.php - - message: "#^Only booleans are allowed in &&, Composer\\\\IO\\\\IOInterface\\|null given on the left side\\.$#" count: 1 @@ -3635,7 +3580,7 @@ parameters: - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" - count: 6 + count: 5 path: ../src/Composer/Repository/Vcs/GitHubDriver.php - @@ -3670,7 +3615,7 @@ parameters: - message: "#^Only booleans are allowed in a negated boolean, string given\\.$#" - count: 2 + count: 1 path: ../src/Composer/Repository/Vcs/GitHubDriver.php - diff --git a/phpstan/config.neon b/phpstan/config.neon index f37049ab41ec..6ffdbe68e3bd 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 8e8c656917c4..fd469d51c7cb 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -325,6 +325,30 @@ "type": ["string"] } }, + "audit": { + "type": "object", + "description": "Security audit configuration options", + "properties": { + "ignore": { + "anyOf": [ + { + "type": "object", + "description": "A list of advisory ids, remote ids or CVE ids (keys) and the explanations (values) for why they're being ignored. The listed items are reported but let the audit command pass.", + "additionalProperties": { + "type": ["string", "string"] + } + }, + { + "type": "array", + "description": "A set of advisory ids, remote ids or CVE ids that are reported but let the audit command pass.", + "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 9ddb5b04b396..5a8397c64066 100644 --- a/src/Composer/Advisory/Auditor.php +++ b/src/Composer/Advisory/Auditor.php @@ -44,28 +44,55 @@ 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[] $ignoreList List of advisory IDs, remote IDs or CVE IDs that reported but not listed as vulnerabilities. + * * @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 $ignoreList = []): int { - $advisories = $repoSet->getMatchingSecurityAdvisories($packages, $format === self::FORMAT_SUMMARY); + $allAdvisories = $repoSet->getMatchingSecurityAdvisories($packages, $format === self::FORMAT_SUMMARY); + // we need the CVE & remote IDs set to filter ignores correctly so if we have any matches using the optimized codepath above + // and ignores are set then we need to query again the full data to make sure it can be filtered + if (count($allAdvisories) > 0 && $ignoreList !== [] && $format === self::FORMAT_SUMMARY) { + $allAdvisories = $repoSet->getMatchingSecurityAdvisories($packages, false); + } + ['advisories' => $advisories, 'ignoredAdvisories' => $ignoredAdvisories] = $this->processAdvisories($allAdvisories, $ignoreList); + if (self::FORMAT_JSON === $format) { - $io->write(JsonFile::encode(['advisories' => $advisories])); + $json = ['advisories' => $advisories]; + if ($ignoredAdvisories !== []) { + $json['ignored-advisories'] = $ignoredAdvisories; + } + + $io->write(JsonFile::encode($json)); return count($advisories); } $errorOrWarn = $warningOnly ? 'warning' : 'error'; - if (count($advisories) > 0) { - [$affectedPackages, $totalAdvisories] = $this->countAdvisories($advisories); - $plurality = $totalAdvisories === 1 ? 'y' : 'ies'; - $pkgPlurality = $affectedPackages === 1 ? '' : 's'; - $punctuation = $format === 'summary' ? '.' : ':'; - $io->writeError("<$errorOrWarn>Found $totalAdvisories security vulnerability advisor{$plurality} affecting $affectedPackages package{$pkgPlurality}{$punctuation}"); - $this->outputAdvisories($io, $advisories, $format); - - return $affectedPackages; + if (count($advisories) > 0 || count($ignoredAdvisories) > 0) { + $passes = [ + [$ignoredAdvisories, "Found %d ignored security vulnerability advisor%s affecting %d package%s%s"], + // this has to run last to allow $affectedPackagesCount in the return statement to be correct + [$advisories, "<$errorOrWarn>Found %d security vulnerability advisor%s affecting %d package%s%s"], + ]; + foreach ($passes as [$advisoriesToOutput, $message]) { + [$affectedPackagesCount, $totalAdvisoryCount] = $this->countAdvisories($advisoriesToOutput); + if ($affectedPackagesCount > 0) { + $plurality = $totalAdvisoryCount === 1 ? 'y' : 'ies'; + $pkgPlurality = $affectedPackagesCount === 1 ? '' : 's'; + $punctuation = $format === 'summary' ? '.' : ':'; + $io->writeError(sprintf($message, $totalAdvisoryCount, $plurality, $affectedPackagesCount, $pkgPlurality, $punctuation)); + $this->outputAdvisories($io, $advisoriesToOutput, $format); + } + } + + if ($format === self::FORMAT_SUMMARY) { + $io->writeError('Run "composer audit" for a full list of advisories.'); + } + + return $affectedPackagesCount; } $io->writeError('No security vulnerability advisories found'); @@ -73,6 +100,69 @@ public function audit(IOInterface $io, RepositorySet $repoSet, array $packages, return 0; } + /** + * @phpstan-param array> $allAdvisories + * @param array|array $ignoreList List of advisory IDs, remote IDs or CVE IDs that reported but not listed as vulnerabilities. + * @phpstan-return array{advisories: array>, ignoredAdvisories: array>} + */ + private function processAdvisories(array $allAdvisories, array $ignoreList): array + { + if ($ignoreList === []) { + return ['advisories' => $allAdvisories, 'ignoredAdvisories' => []]; + } + + if (\count($ignoreList) > 0 && !\array_is_list($ignoreList)) { + $ignoredIds = array_keys($ignoreList); + } else { + $ignoredIds = $ignoreList; + } + + $advisories = []; + $ignored = []; + $ignoreReason = null; + + foreach ($allAdvisories as $package => $pkgAdvisories) { + foreach ($pkgAdvisories as $advisory) { + $isActive = true; + + if (in_array($advisory->advisoryId, $ignoredIds, true)) { + $isActive = false; + $ignoreReason = $ignoreList[$advisory->advisoryId] ?? null; + } + + if ($advisory instanceof SecurityAdvisory) { + if (in_array($advisory->cve, $ignoredIds, true)) { + $isActive = false; + $ignoreReason = $ignoreList[$advisory->cve] ?? null; + } + + foreach ($advisory->sources as $source) { + if (in_array($source['remoteId'], $ignoredIds, true)) { + $isActive = false; + $ignoreReason = $ignoreList[$source['remoteId']] ?? null; + break; + } + } + } + + if ($isActive) { + $advisories[$package][] = $advisory; + continue; + } + + // Partial security advisories only used in summary mode + // and in that case we do not need to cast the object. + if ($advisory instanceof SecurityAdvisory) { + $advisory = $advisory->toIgnoredAdvisory($ignoreReason); + } + + $ignored[$package][] = $advisory; + } + } + + return ['advisories' => $advisories, 'ignoredAdvisories' => $ignored]; + } + /** * @param array> $advisories * @return array{int, int} Count of affected packages and total count of advisories @@ -106,8 +196,6 @@ private function outputAdvisories(IOInterface $io, array $advisories, string $fo return; case self::FORMAT_SUMMARY: - // We've already output the number of advisories in audit() - $io->writeError('Run composer audit for a full list of advisories.'); return; default: @@ -122,24 +210,30 @@ private function outputAdvisoriesTable(ConsoleIO $io, array $advisories): void { foreach ($advisories as $packageAdvisories) { foreach ($packageAdvisories as $advisory) { + $headers = [ + 'Package', + 'CVE', + 'Title', + 'URL', + 'Affected versions', + 'Reported at', + ]; + $row = [ + $advisory->packageName, + $this->getCVE($advisory), + $advisory->title, + $this->getURL($advisory), + $advisory->affectedVersions->getPrettyString(), + $advisory->reportedAt->format(DATE_ATOM), + ]; + if ($advisory instanceof IgnoredSecurityAdvisory) { + $headers[] = 'Ignore reason'; + $row[] = $advisory->ignoreReason ?? 'None specified'; + } $io->getTable() ->setHorizontal() - ->setHeaders([ - 'Package', - 'CVE', - 'Title', - 'URL', - 'Affected versions', - 'Reported at', - ]) - ->addRow([ - $advisory->packageName, - $this->getCVE($advisory), - $advisory->title, - $this->getURL($advisory), - $advisory->affectedVersions->getPrettyString(), - $advisory->reportedAt->format(DATE_ATOM), - ]) + ->setHeaders($headers) + ->addRow($row) ->setColumnWidth(1, 80) ->setColumnMaxWidth(1, 80) ->render(); @@ -165,6 +259,9 @@ private function outputAdvisoriesPlain(IOInterface $io, array $advisories): void $error[] = "URL: ".$this->getURL($advisory); $error[] = "Affected versions: ".OutputFormatter::escape($advisory->affectedVersions->getPrettyString()); $error[] = "Reported at: ".$advisory->reportedAt->format(DATE_ATOM); + if ($advisory instanceof IgnoredSecurityAdvisory) { + $error[] = "Ignore reason: ".($advisory->ignoreReason ?? 'None specified'); + } $firstAdvisory = false; } } @@ -188,4 +285,5 @@ private function getURL(SecurityAdvisory $advisory): string return 'link).'>'.OutputFormatter::escape($advisory->link).''; } + } diff --git a/src/Composer/Advisory/IgnoredSecurityAdvisory.php b/src/Composer/Advisory/IgnoredSecurityAdvisory.php new file mode 100644 index 000000000000..ba9079287b84 --- /dev/null +++ b/src/Composer/Advisory/IgnoredSecurityAdvisory.php @@ -0,0 +1,50 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Advisory; + +use Composer\Semver\Constraint\ConstraintInterface; +use DateTimeImmutable; + +class IgnoredSecurityAdvisory extends SecurityAdvisory +{ + /** + * @var string|null + * @readonly + */ + public $ignoreReason; + + /** + * @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) + { + parent::__construct($packageName, $advisoryId, $affectedVersions, $title, $sources, $reportedAt, $cve, $link); + + $this->ignoreReason = $ignoreReason; + } + + /** + * @return mixed + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $data = parent::jsonSerialize(); + if ($this->ignoreReason === NULL) { + unset($data['ignoreReason']); + } + + return $data; + } + +} diff --git a/src/Composer/Advisory/SecurityAdvisory.php b/src/Composer/Advisory/SecurityAdvisory.php index 8fdf4dd55ad0..e88228d60567 100644 --- a/src/Composer/Advisory/SecurityAdvisory.php +++ b/src/Composer/Advisory/SecurityAdvisory.php @@ -42,14 +42,13 @@ class SecurityAdvisory extends PartialSecurityAdvisory public $reportedAt; /** - * @var array + * @var non-empty-array * @readonly */ public $sources; /** * @param non-empty-array $sources - * @readonly */ public function __construct(string $packageName, string $advisoryId, ConstraintInterface $affectedVersions, string $title, array $sources, DateTimeImmutable $reportedAt, ?string $cve = null, ?string $link = null) { @@ -62,6 +61,24 @@ public function __construct(string $packageName, string $advisoryId, ConstraintI $this->link = $link; } + /** + * @internal + */ + public function toIgnoredAdvisory(?string $ignoreReason): IgnoredSecurityAdvisory + { + return new IgnoredSecurityAdvisory( + $this->packageName, + $this->advisoryId, + $this->affectedVersions, + $this->title, + $this->sources, + $this->reportedAt, + $this->cve, + $this->link, + $ignoreReason + ); + } + /** * @return mixed */ diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index f9238bec4778..be776f1d0969 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -70,6 +70,11 @@ class AutoloadGenerator */ private $apcuPrefix; + /** + * @var bool + */ + private $dryRun = false; + /** * @var bool */ @@ -127,6 +132,14 @@ public function setRunScripts(bool $runScripts = true) $this->runScripts = $runScripts; } + /** + * Whether to run in drymode or not + */ + public function setDryRun(bool $dryRun = true): void + { + $this->dryRun = $dryRun; + } + /** * Whether platform requirements should be ignored. * @@ -398,6 +411,10 @@ public static function autoload(\$class) } } + if ($this->dryRun) { + return $classMap; + } + $filesystem->filePutContentsIfModified($targetDir.'/autoload_namespaces.php', $namespacesFile); $filesystem->filePutContentsIfModified($targetDir.'/autoload_psr4.php', $psr4File); $filesystem->filePutContentsIfModified($targetDir.'/autoload_classmap.php', $classmapFile); diff --git a/src/Composer/Command/AuditCommand.php b/src/Composer/Command/AuditCommand.php index 5a91a174326c..3c58d7feb8c0 100644 --- a/src/Composer/Command/AuditCommand.php +++ b/src/Composer/Command/AuditCommand.php @@ -63,7 +63,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $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')['ignore'] ?? [])); } /** diff --git a/src/Composer/Command/BaseDependencyCommand.php b/src/Composer/Command/BaseDependencyCommand.php index 82662093a52e..d3b690eca6b1 100644 --- a/src/Composer/Command/BaseDependencyCommand.php +++ b/src/Composer/Command/BaseDependencyCommand.php @@ -24,10 +24,12 @@ use Composer\Repository\RepositoryFactory; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; +use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Composer\Package\Version\VersionParser; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Composer\Util\PackageInfo; /** * Base implementation for commands mapping dependency relationships. @@ -180,7 +182,9 @@ protected function printTable(OutputInterface $output, $results): void } $doubles[$unique] = true; $version = $package->getPrettyVersion() === RootPackage::DEFAULT_PRETTY_VERSION ? '-' : $package->getPrettyVersion(); - $rows[] = [$package->getPrettyName(), $version, $link->getDescription(), sprintf('%s (%s)', $link->getTarget(), $link->getPrettyConstraint())]; + $packageUrl = PackageInfo::getViewSourceOrHomepageUrl($package); + $nameWithLink = $packageUrl !== null ? '' . $package->getPrettyName() . '' : $package->getPrettyName(); + $rows[] = [$nameWithLink, $version, $link->getDescription(), sprintf('%s (%s)', $link->getTarget(), $link->getPrettyConstraint())]; if ($children) { $queue = array_merge($queue, $children); } @@ -229,7 +233,9 @@ protected function printTree(array $results, string $prefix = '', int $level = 1 $prevColor = $this->colors[($level - 1) % count($this->colors)]; $isLast = (++$idx === $count); $versionText = $package->getPrettyVersion() === RootPackage::DEFAULT_PRETTY_VERSION ? '' : $package->getPrettyVersion(); - $packageText = rtrim(sprintf('<%s>%s %s', $color, $package->getPrettyName(), $versionText)); + $packageUrl = PackageInfo::getViewSourceOrHomepageUrl($package); + $nameWithLink = $packageUrl !== null ? '' . $package->getPrettyName() . '' : $package->getPrettyName(); + $packageText = rtrim(sprintf('<%s>%s %s', $color, $nameWithLink, $versionText)); $linkText = sprintf('%s <%s>%s %s', $link->getDescription(), $prevColor, $link->getTarget(), $link->getPrettyConstraint()); $circularWarn = $children === false ? '(circular dependency aborted here)' : ''; $this->writeTreeLine(rtrim(sprintf("%s%s%s (%s) %s", $prefix, $isLast ? '└──' : '├──', $packageText, $linkText, $circularWarn))); diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index bef163b3d7ef..1a1e0bb48626 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -556,8 +556,27 @@ static function ($vals) { 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/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 250f558f4f1b..1a9cbf4bdf2e 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -472,8 +472,8 @@ protected function installRootPackage(IOInterface $io, Config $config, string $p // ensure that the env var being set does not interfere with create-project // as it is probably not meant to be used here, so we do not use it if a composer.json can be found // in the project - if (file_exists($directory.'/composer.json')) { - Platform::putEnv('COMPOSER', $directory.'/composer.json'); + if (file_exists($directory.'/composer.json') && Platform::getEnv('COMPOSER') !== false) { + Platform::clearEnv('COMPOSER'); } Platform::putEnv('COMPOSER_ROOT_VERSION', $package->getPrettyVersion()); diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index 6f306d340dc8..b1af4d6be886 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -251,7 +251,7 @@ private function checkGit(): string */ private function checkHttp(string $proto, Config $config) { - $result = $this->checkConnectivity(); + $result = $this->checkConnectivityAndComposerNetworkHttpEnablement(); if ($result !== true) { return $result; } @@ -290,7 +290,7 @@ private function checkHttp(string $proto, Config $config) */ private function checkHttpProxy() { - $result = $this->checkConnectivity(); + $result = $this->checkConnectivityAndComposerNetworkHttpEnablement(); if ($result !== true) { return $result; } @@ -318,7 +318,7 @@ private function checkHttpProxy() */ private function checkGithubOauth(string $domain, string $token) { - $result = $this->checkConnectivity(); + $result = $this->checkConnectivityAndComposerNetworkHttpEnablement(); if ($result !== true) { return $result; } @@ -348,7 +348,7 @@ private function checkGithubOauth(string $domain, string $token) */ private function getGithubRateLimit(string $domain, ?string $token = null) { - $result = $this->checkConnectivity(); + $result = $this->checkConnectivityAndComposerNetworkHttpEnablement(); if ($result !== true) { return $result; } @@ -419,7 +419,7 @@ private function checkPubKeys(Config $config) */ private function checkVersion(Config $config) { - $result = $this->checkConnectivity(); + $result = $this->checkConnectivityAndComposerNetworkHttpEnablement(); if ($result !== true) { return $result; } @@ -733,7 +733,39 @@ private function checkPlatform() private function checkConnectivity() { if (!ini_get('allow_url_fopen')) { - return 'Skipped because allow_url_fopen is missing.'; + return 'SKIP Because allow_url_fopen is missing.'; + } + + return true; + } + + /** + * @return string|true + */ + private function checkConnectivityAndComposerNetworkHttpEnablement() + { + $result = $this->checkConnectivity(); + if ($result !== true) { + return $result; + } + + $result = $this->checkComposerNetworkHttpEnablement(); + if ($result !== true) { + return $result; + } + + return true; + } + + /** + * Check if Composer network is enabled for HTTP/S + * + * @return string|true + */ + private function checkComposerNetworkHttpEnablement() + { + if ((bool) Platform::getEnv('COMPOSER_DISABLE_NETWORK')) { + return 'SKIP Network is disabled by COMPOSER_DISABLE_NETWORK.'; } return true; diff --git a/src/Composer/Command/DumpAutoloadCommand.php b/src/Composer/Command/DumpAutoloadCommand.php index a6c6a5ae49b3..41a23cd3afe4 100644 --- a/src/Composer/Command/DumpAutoloadCommand.php +++ b/src/Composer/Command/DumpAutoloadCommand.php @@ -37,6 +37,7 @@ protected function configure() new InputOption('classmap-authoritative', 'a', InputOption::VALUE_NONE, 'Autoload classes from the classmap only. Implicitly enables `--optimize`.'), new InputOption('apcu', null, InputOption::VALUE_NONE, 'Use APCu to cache found/not-found classes.'), new InputOption('apcu-prefix', null, InputOption::VALUE_REQUIRED, 'Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu'), + new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything.'), new InputOption('dev', null, InputOption::VALUE_NONE, 'Enables autoload-dev rules. Composer will by default infer this automatically according to the last install or update --no-dev state.'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables autoload-dev rules. Composer will by default infer this automatically according to the last install or update --no-dev state.'), new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'), @@ -70,8 +71,8 @@ protected function execute(InputInterface $input, OutputInterface $output) $apcuPrefix = $input->getOption('apcu-prefix'); $apcu = $apcuPrefix !== null || $input->getOption('apcu') || $config->get('apcu-autoloader'); - if ($input->getOption('strict-psr') && !$optimize) { - throw new \InvalidArgumentException('--strict-psr mode only works with optimized autoloader, use --optimize if you want a strict return value.'); + if ($input->getOption('strict-psr') && !$optimize && !$authoritative) { + throw new \InvalidArgumentException('--strict-psr mode only works with optimized autoloader, use --optimize or --classmap-authoritative if you want a strict return value.'); } if ($authoritative) { @@ -83,6 +84,9 @@ protected function execute(InputInterface $input, OutputInterface $output) } $generator = $composer->getAutoloadGenerator(); + if ($input->getOption('dry-run')) { + $generator->setDryRun(true); + } if ($input->getOption('no-dev')) { $generator->setDevMode(false); } diff --git a/src/Composer/Command/InstallCommand.php b/src/Composer/Command/InstallCommand.php index a469837bb576..7ab60260cd66 100644 --- a/src/Composer/Command/InstallCommand.php +++ b/src/Composer/Command/InstallCommand.php @@ -136,6 +136,7 @@ protected function execute(InputInterface $input, OutputInterface $output) ->setApcuAutoloader($apcu, $apcuPrefix) ->setPlatformRequirementFilter($this->getPlatformRequirementFilter($input)) ->setAudit($input->getOption('audit')) + ->setErrorOnAudit($input->getOption('audit')) ->setAuditFormat($this->getAuditFormat($input)) ; diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 90ca115b0e18..760bfdb38fe5 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -488,7 +488,7 @@ private function doUpdate(InputInterface $input, OutputInterface $output, IOInte } $status = $install->run(); - if ($status !== 0) { + if ($status !== 0 && $status !== Installer::ERROR_AUDIT_FAILED) { if ($status === Installer::ERROR_DEPENDENCY_RESOLUTION_FAILED) { foreach ($this->normalizeRequirements($input->getArgument('packages')) as $req) { if (!isset($req['version'])) { diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index d7bd2c75a7cc..ea5a9db3ba92 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -442,6 +442,10 @@ protected function execute(InputInterface $input, OutputInterface $output) $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 @@ protected function execute(InputInterface $input, OutputInterface $output) $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 @@ protected function execute(InputInterface $input, OutputInterface $output) 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 @@ protected function execute(InputInterface $input, OutputInterface $output) */ 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 @@ private function printPackages(IOInterface $io, array $packages, string $indent, 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"); @@ -756,10 +763,14 @@ protected function getPackage(InstalledRepository $installedRepo, RepositoryInte } // select preferred package according to policy rules - if (!$matchedPackage && $matches && $preferred = $policy->selectPreferredPackages($pool, $matches)) { + if (null === $matchedPackage && $matches && $preferred = $policy->selectPreferredPackages($pool, $matches)) { $matchedPackage = $pool->literalToPackage($preferred[0]); } + if ($matchedPackage !== null && !$matchedPackage instanceof CompletePackageInterface) { + throw new \LogicException('ShowCommand::getPackage can only work with CompletePackageInterface, but got '.get_class($matchedPackage)); + } + return [$matchedPackage, $versions]; } @@ -811,7 +822,7 @@ protected function printMeta(CompletePackageInterface $package, array $versions, $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)); @@ -972,7 +983,7 @@ protected function printPackageInfoAsJson(CompletePackageInterface $package, arr ]; } - 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); @@ -1251,6 +1262,9 @@ protected function displayTree( $colorIdent = $level % count($this->colors); $color = $this->colors[$colorIdent]; + assert(is_string($require['name'])); + assert(is_string($require['version'])); + $circularWarn = in_array( $require['name'], $currentTree, diff --git a/src/Composer/Config.php b/src/Composer/Config.php index 4555203f1ced..0c11ab50575d 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' => ['ignore' => []], 'notify-on-install' => true, 'github-protocols' => ['https', 'ssh', 'git'], 'gitlab-protocol' => null, @@ -207,6 +208,11 @@ public function merge(array $config, string $source = self::SOURCE_UNKNOWN): voi $this->config[$key] = $val; $this->setSourceOfConfigValue($val, $key, $source); } + } elseif ('audit' === $key) { + $currentIgnores = $this->config['audit']['ignore']; + $this->config[$key] = $val; + $this->setSourceOfConfigValue($val, $key, $source); + $this->config['audit']['ignore'] = array_merge($currentIgnores, $val['ignore'] ?? []); } else { $this->config[$key] = $val; $this->setSourceOfConfigValue($val, $key, $source); diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index 01a2497b77b3..da77636d112d 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -87,6 +87,10 @@ class Application extends BaseApplication public function __construct(string $name = 'Composer', string $version = '') { + if (method_exists($this, 'setCatchErrors')) { + $this->setCatchErrors(true); + } + static $shutdownRegistered = false; if ($version === '') { $version = Composer::getVersion(); @@ -395,9 +399,10 @@ function_exists('php_uname') ? php_uname('s') . ' / ' . php_uname('r') : 'Unknow $this->hintCommonErrors($e, $output); - // symfony/console does not handle \Error subtypes so we have to renderThrowable ourselves + // symfony/console <6.4 does not handle \Error subtypes so we have to renderThrowable ourselves // instead of rethrowing those for consumption by the parent class - if (!$e instanceof \Exception) { + // can be removed when Composer supports PHP 8.1+ + if (!method_exists($this, 'setCatchErrors') && !$e instanceof \Exception) { if ($output instanceof ConsoleOutputInterface) { $this->renderThrowable($e, $output->getErrorOutput()); } else { diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index 21b76ec3a9c1..cf2cb381e327 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -453,7 +453,7 @@ private static function getPlatformPackageVersion(Pool $pool, string $packageNam } /** - * @param array $versions an array of pretty versions, with normalized versions as keys + * @param array $versions an array of pretty versions, with normalized versions as keys * @return list a list of pretty versions and '...' where versions were removed */ private static function condenseVersionList(array $versions, int $max, int $maxDev = 16): array @@ -465,10 +465,10 @@ private static function condenseVersionList(array $versions, int $max, int $maxD $filtered = []; $byMajor = []; foreach ($versions as $version => $pretty) { - if (0 === stripos($version, 'dev-')) { + if (0 === stripos((string) $version, 'dev-')) { $byMajor['dev'][] = $pretty; } else { - $byMajor[Preg::replace('{^(\d+)\..*}', '$1', $version)][] = $pretty; + $byMajor[Preg::replace('{^(\d+)\..*}', '$1', (string) $version)][] = $pretty; } } foreach ($byMajor as $majorVersion => $versionsForMajor) { diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index a6648bcdf1e1..6de51ee58ba2 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -216,6 +216,7 @@ protected function getInstallOperationAppendix(PackageInterface $package, string * * @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 7f7ad2c3a773..e4adfdf37ac0 100644 --- a/src/Composer/Downloader/DownloadManager.php +++ b/src/Composer/Downloader/DownloadManager.php @@ -174,6 +174,7 @@ public function getDownloaderType(DownloaderInterface $downloader): string * @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 @@ public function download(PackageInterface $package, string $targetDir, ?PackageI * @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 @@ public function prepare(string $type, PackageInterface $package, string $targetD * * @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 @@ public function install(PackageInterface $package, string $targetDir): PromiseIn * @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 @@ public function update(PackageInterface $initial, PackageInterface $target, stri * * @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 @@ public function remove(PackageInterface $package, string $targetDir): PromiseInt * @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 8e135d7256de..8cb86cdbb7f2 100644 --- a/src/Composer/Downloader/DownloaderInterface.php +++ b/src/Composer/Downloader/DownloaderInterface.php @@ -34,6 +34,7 @@ public function getInstallationSource(): string; * 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 @@ public function download(PackageInterface $package, string $path, ?PackageInterf * @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 @@ public function prepare(string $type, PackageInterface $package, string $path, ? * * @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 @@ public function install(PackageInterface $package, string $path): PromiseInterfa * @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 @@ public function update(PackageInterface $initial, PackageInterface $target, stri * * @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 @@ public function remove(PackageInterface $package, string $path): PromiseInterfac * @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 05901a9b21de..0840219d0fe6 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -536,6 +536,7 @@ protected function getCommitLogs(string $fromReference, string $toReference, str } /** + * @phpstan-return PromiseInterface * @throws \RuntimeException */ protected function discardChanges(string $path): PromiseInterface @@ -551,6 +552,7 @@ protected function discardChanges(string $path): PromiseInterface } /** + * @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 daa93dfe9af6..be180d63d7f6 100644 --- a/src/Composer/Downloader/SvnDownloader.php +++ b/src/Composer/Downloader/SvnDownloader.php @@ -230,6 +230,9 @@ protected function getCommitLogs(string $fromReference, string $toReference, str 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 15f3aeccdb7a..a1c5979d5731 100644 --- a/src/Composer/Downloader/VcsDownloader.php +++ b/src/Composer/Downloader/VcsDownloader.php @@ -259,6 +259,7 @@ public function getVcsReference(PackageInterface $package, string $path): ?strin * 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 @@ protected function reapplyChanges(string $path): void * @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 protected function doDownload(PackageInterface $package, string $path, * @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 protected function doInstall(PackageInterface $package, string $path, s * @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 851e70de10cc..9d0f35353e67 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -105,6 +105,7 @@ public function download(PackageInterface $package, string $path, ?PackageInterf * * @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 @@ private function extractWithSystemUnzip(PackageInterface $package, string $file, * * @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/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php index 2286a2b1c32e..c7e2fd5658cd 100644 --- a/src/Composer/EventDispatcher/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -277,6 +277,9 @@ protected function doDispatch(Event $event) $app = new Application(); $app->setCatchExceptions(false); + if (method_exists($app, 'setCatchErrors')) { + $app->setCatchErrors(false); + } $app->setAutoExit(false); $cmd = new $className($event->getName()); $app->add($cmd); diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 205648f030d9..3fc3bcf83294 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -170,6 +170,8 @@ class Installer protected $executeOperations = true; /** @var bool */ protected $audit = true; + /** @var bool */ + protected $errorOnAudit = false; /** @var Auditor::FORMAT_* */ protected $auditFormat = Auditor::FORMAT_SUMMARY; @@ -402,7 +404,7 @@ public function run(): int $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')['ignore'] ?? []) > 0 && $this->errorOnAudit ? self::ERROR_AUDIT_FAILED : 0; } catch (TransportException $e) { $this->io->error('Failed to audit '.$target.' packages.'); if ($this->io->isVerbose()) { @@ -609,16 +611,6 @@ protected function doUpdate(InstalledRepositoryInterface $localRepo, bool $doIns $this->io->writeError('Writing lock file'); } - // see https://github.com/composer/composer/issues/2764 - if ($this->executeOperations && count($lockTransaction->getOperations()) > 0) { - $vendorDir = $this->config->get('vendor-dir'); - if (is_dir($vendorDir)) { - // suppress errors as this fails sometimes on OSX for no apparent reason - // see https://github.com/composer/composer/issues/4070#issuecomment-129792748 - @touch($vendorDir); - } - } - if ($doInstall) { // TODO ensure lock is used from locker as-is, since it may not have been written to disk in case of executeOperations == false return $this->doInstall($localRepo, true); @@ -797,6 +789,16 @@ protected function doInstall(InstalledRepositoryInterface $localRepo, bool $alre if ($this->executeOperations) { $localRepo->setDevPackageNames($this->locker->getDevPackageNames()); $this->installationManager->execute($localRepo, $localRepoTransaction->getOperations(), $this->devMode, $this->runScripts, $this->downloadOnly); + + // see https://github.com/composer/composer/issues/2764 + if (count($localRepoTransaction->getOperations()) > 0) { + $vendorDir = $this->config->get('vendor-dir'); + if (is_dir($vendorDir)) { + // suppress errors as this fails sometimes on OSX for no apparent reason + // see https://github.com/composer/composer/issues/4070#issuecomment-129792748 + @touch($vendorDir); + } + } } else { foreach ($localRepoTransaction->getOperations() as $operation) { // output op, but alias op only in debug verbosity @@ -1422,6 +1424,19 @@ public function setAudit(bool $audit): self return $this; } + /** + * Should exit with status code 5 on audit error + * + * @param bool $errorOnAudit + * @return Installer + */ + public function setErrorOnAudit(bool $errorOnAudit): self + { + $this->errorOnAudit = $errorOnAudit; + + return $this; + } + /** * What format should be used for audit output? * diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index d171139e9d61..f92a117ba7b3 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -180,7 +180,7 @@ public function ensureBinariesPresence(PackageInterface $package): void */ 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 @@ public function execute(InstalledRepositoryInterface $repo, array $operations, b /** * @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 @@ private function downloadAndExecuteBatch(InstalledRepositoryInterface $repo, arr if ($opType !== 'uninstall') { $promise = $installer->download($package, $initialPackage); - if ($promise) { + if (null !== $promise) { $promises[] = $promise; } } @@ -322,8 +322,8 @@ private function downloadAndExecuteBatch(InstalledRepositoryInterface $repo, arr /** * @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 @@ private function executeBatch(InstalledRepositoryInterface $repo, array $operati } /** - * @param PromiseInterface[] $promises + * @param array> $promises */ private function waitOnPromises(array $promises): void { @@ -440,7 +440,7 @@ private function waitOnPromises(array $promises): void /** * Executes download operation. * - * $param PackageInterface $package + * @phpstan-return PromiseInterface|null */ public function download(PackageInterface $package): ?PromiseInterface { @@ -455,6 +455,7 @@ public function download(PackageInterface $package): ?PromiseInterface * * @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 @@ public function install(InstalledRepositoryInterface $repo, InstallOperation $op * * @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 @@ public function update(InstalledRepositoryInterface $repo, UpdateOperation $oper * * @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 @@ private function markForNotification(PackageInterface $package): void } /** - * @param array $cleanupPromises * @return void + * @phpstan-param array> $cleanupPromises */ private function runCleanup(array $cleanupPromises): void { @@ -648,7 +651,7 @@ private function runCleanup(array $cleanupPromises): void $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 bdf42ec42292..7c92e91d4fad 100644 --- a/src/Composer/Installer/InstallerInterface.php +++ b/src/Composer/Installer/InstallerInterface.php @@ -48,6 +48,7 @@ public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface * @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 @@ public function download(PackageInterface $package, ?PackageInterface $prevPacka * @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 @@ public function prepare(string $type, PackageInterface $package, ?PackageInterfa * @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 @@ public function install(InstalledRepositoryInterface $repo, PackageInterface $pa * @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 @@ public function update(InstalledRepositoryInterface $repo, PackageInterface $ini * @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 @@ public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $ * @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 d87c6e3dc152..0626fb189cde 100644 --- a/src/Composer/Installer/LibraryInstaller.php +++ b/src/Composer/Installer/LibraryInstaller.php @@ -272,6 +272,7 @@ protected function getPackageBasePath(PackageInterface $package) /** * @return PromiseInterface|null + * @phpstan-return PromiseInterface|null */ protected function installCode(PackageInterface $package) { @@ -282,6 +283,7 @@ protected function installCode(PackageInterface $package) /** * @return PromiseInterface|null + * @phpstan-return PromiseInterface|null */ protected function updateCode(PackageInterface $initial, PackageInterface $target) { @@ -316,6 +318,7 @@ protected function updateCode(PackageInterface $initial, PackageInterface $targe /** * @return PromiseInterface|null + * @phpstan-return PromiseInterface|null */ protected function removeCode(PackageInterface $package) { diff --git a/src/Composer/Json/JsonFile.php b/src/Composer/Json/JsonFile.php index 3ad34a619981..7f9814d61d3e 100644 --- a/src/Composer/Json/JsonFile.php +++ b/src/Composer/Json/JsonFile.php @@ -42,12 +42,16 @@ class JsonFile public const COMPOSER_SCHEMA_PATH = __DIR__ . '/../../../res/composer-schema.json'; + public const INDENT_DEFAULT = ' '; + /** @var string */ private $path; /** @var ?HttpDownloader */ private $httpDownloader; /** @var ?IOInterface */ private $io; + /** @var string */ + private $indent = self::INDENT_DEFAULT; /** * Initializes json file reader/parser. @@ -117,6 +121,8 @@ public function read() throw new \RuntimeException('Could not read '.$this->path); } + $this->indent = self::detectIndenting($json); + return static::parseJson($json, $this->path); } @@ -131,7 +137,7 @@ public function read() public function write(array $hash, int $options = JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) { if ($this->path === 'php://memory') { - file_put_contents($this->path, static::encode($hash, $options)); + file_put_contents($this->path, static::encode($hash, $options, $this->indent)); return; } @@ -153,7 +159,7 @@ public function write(array $hash, int $options = JSON_UNESCAPED_SLASHES | JSON_ $retries = 3; while ($retries--) { try { - $this->filePutContentsIfModified($this->path, static::encode($hash, $options). ($options & JSON_PRETTY_PRINT ? "\n" : '')); + $this->filePutContentsIfModified($this->path, static::encode($hash, $options, $this->indent). ($options & JSON_PRETTY_PRINT ? "\n" : '')); break; } catch (\Exception $e) { if ($retries > 0) { @@ -262,15 +268,28 @@ public static function validateJsonSchema(string $source, $data, int $schema, ?s * * @param mixed $data Data to encode into a formatted JSON string * @param int $options json_encode options (defaults to JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) + * @param string $indent Indentation string * @return string Encoded json */ - public static function encode($data, int $options = 448) + public static function encode($data, int $options = 448, string $indent = self::INDENT_DEFAULT): string { $json = json_encode($data, $options); + if (false === $json) { self::throwEncodeError(json_last_error()); } + if (($options & JSON_PRETTY_PRINT) > 0 && $indent !== self::INDENT_DEFAULT ) { + // Pretty printing and not using default indentation + return Preg::replaceCallback( + '#^ {4,}#m', + static function ($match) use ($indent): string { + return str_repeat($indent, (int)(strlen($match[0] ?? '') / 4)); + }, + $json + ); + } + return $json; } @@ -279,6 +298,7 @@ public static function encode($data, int $options = 448) * * @param int $code return code of json_last_error function * @throws \RuntimeException + * @return never */ private static function throwEncodeError(int $code): void { @@ -356,4 +376,12 @@ protected static function validateSyntax(string $json, ?string $file = null): bo $result->getDetails()); } } + + public static function detectIndenting(?string $json): string + { + if (Preg::isMatchStrictGroups('#^([ \t]+)"#m', $json ?? '', $match)) { + return $match[1]; + } + return self::INDENT_DEFAULT; + } } diff --git a/src/Composer/Json/JsonManipulator.php b/src/Composer/Json/JsonManipulator.php index d6caffebaa56..8d6759671080 100644 --- a/src/Composer/Json/JsonManipulator.php +++ b/src/Composer/Json/JsonManipulator.php @@ -561,10 +561,6 @@ public function format($data, int $depth = 0): string protected function detectIndenting(): void { - if (Preg::isMatchStrictGroups('{^([ \t]+)"}m', $this->contents, $match)) { - $this->indent = $match[1]; - } else { - $this->indent = ' '; - } + $this->indent = JsonFile::detectIndenting($this->contents); } } diff --git a/src/Composer/Package/Version/VersionBumper.php b/src/Composer/Package/Version/VersionBumper.php index 1129efaa0084..690dfbeeddac 100644 --- a/src/Composer/Package/Version/VersionBumper.php +++ b/src/Composer/Package/Version/VersionBumper.php @@ -83,7 +83,7 @@ public function bumpRequirement(ConstraintInterface $constraint, PackageInterfac (?<=,|\ |\||^) # leading separator (?P \^'.$major.'(?:\.\d+)* # e.g. ^2.anything - | ~'.$major.'(?:\.\d+)? # e.g. ~2 or ~2.2 but no more + | ~'.$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 ) @@ -97,7 +97,9 @@ public function bumpRequirement(ConstraintInterface $constraint, PackageInterfac if (substr_count($match[0], '.') === 2 && substr_count($versionWithoutSuffix, '.') === 1) { $suffix = '.0'; } - if (str_starts_with($match[0], '>=')) { + if (str_starts_with($match[0], '~') && substr_count($match[0], '.') === 2) { + $replacement = '~'.$versionWithoutSuffix.$suffix; + } elseif (str_starts_with($match[0], '>=')) { $replacement = '>='.$versionWithoutSuffix.$suffix; } else { $replacement = $newPrettyConstraint.$suffix; diff --git a/src/Composer/Package/Version/VersionGuesser.php b/src/Composer/Package/Version/VersionGuesser.php index 46116f0b7822..2b2b19706506 100644 --- a/src/Composer/Package/Version/VersionGuesser.php +++ b/src/Composer/Package/Version/VersionGuesser.php @@ -322,9 +322,8 @@ private function guessFeatureVersion(array $packageConfig, ?string $version, arr $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 ea87e5c4bfa5..1dcf991aa0b6 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -139,8 +139,13 @@ public function __construct(array $repoConfig, IOInterface $io, Config $config, { parent::__construct(); if (!Preg::isMatch('{^[\w.]+\??://}', $repoConfig['url'])) { - // assume http as the default protocol - $repoConfig['url'] = 'http://'.$repoConfig['url']; + if (($localFilePath = realpath($repoConfig['url'])) !== false) { + // it is a local path, add file scheme + $repoConfig['url'] = 'file://'.$localFilePath; + } else { + // otherwise, assume http as the default protocol + $repoConfig['url'] = 'http://'.$repoConfig['url']; + } } $repoConfig['url'] = rtrim($repoConfig['url'], '/'); if ($repoConfig['url'] === '') { @@ -685,14 +690,15 @@ static function ($data) use ($name, $create) { } if ($apiUrl !== null && count($packageConstraintMap) > 0) { - $options = [ - 'http' => [ - 'method' => 'POST', - 'header' => ['Content-type: application/x-www-form-urlencoded'], - 'timeout' => 10, - 'content' => http_build_query(['packages' => array_keys($packageConstraintMap)]), - ], - ]; + $options = $this->options; + $options['http']['method'] = 'POST'; + if (isset($options['http']['header'])) { + $options['http']['header'] = (array) $options['http']['header']; + } + $options['http']['header'][] = 'Content-type: application/x-www-form-urlencoded'; + $options['http']['timeout'] = 10; + $options['http']['content'] = http_build_query(['packages' => array_keys($packageConstraintMap)]); + $response = $this->httpDownloader->get($apiUrl, $options); /** @var string $name */ foreach ($response->decodeJson()['advisories'] as $name => $list) { @@ -1065,6 +1071,9 @@ private function loadAsyncPackages(array $packageNames, ?array $acceptableStabil return ['namesFound' => $namesFound, 'packages' => $packages]; } + /** + * @phpstan-return PromiseInterface + */ private function startCachedAsyncDownload(string $fileName, ?string $packageName = null): PromiseInterface { if (null === $this->lazyProvidersUrl) { @@ -1593,6 +1602,9 @@ private function fetchFileIfLastModified(string $filename, string $cacheKey, str } } + /** + * @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) { @@ -1605,7 +1617,10 @@ private function asyncFetchFile(string $filename, string $cacheKey, ?string $las 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 43cc4eb989cd..9ab0f6c6bd3c 100644 --- a/src/Composer/Repository/PlatformRepository.php +++ b/src/Composer/Repository/PlatformRepository.php @@ -238,7 +238,12 @@ protected function initialize(): void $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 122b03fa7975..44766a185543 100644 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -310,7 +310,7 @@ public function getFileContent(string $file, string $identifier): ?string $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 e0f008d14892..19e0efa8a865 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -133,6 +133,7 @@ public function removeDirectory(string $directory) * * @throws \RuntimeException * @return PromiseInterface + * @phpstan-return PromiseInterface */ public function removeDirectoryAsync(string $directory) { @@ -784,6 +785,12 @@ public function junction(string $target, string $junction) 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/Git.php b/src/Composer/Util/Git.php index c1da74d427b7..f8e503d82855 100644 --- a/src/Composer/Util/Git.php +++ b/src/Composer/Util/Git.php @@ -223,6 +223,7 @@ public function runCommand(callable $commandCallable, string $url, ?string $cwd, } $this->io->writeError(' Authentication required (' . $match[2] . '):'); + $this->io->writeError('' . trim($errorMsg) . '', true, IOInterface::VERBOSE); $auth = [ 'username' => $this->io->ask(' Username: ', $defaultUsername), 'password' => $this->io->askAndHideAnswer(' Password: '), diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index edc668a60603..7bc9034d6ea2 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -357,6 +357,13 @@ public function tick(): void 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 58a64b663636..723ff0287e69 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -28,7 +28,7 @@ /** * @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 { @@ -107,7 +107,10 @@ public function get(string $url, array $options = []) if ('' === $url) { throw new \InvalidArgumentException('$url must not be an empty string'); } - [$job] = $this->addJob(['url' => $url, 'options' => $options, 'copyTo' => null], true); + [$job, $promise] = $this->addJob(['url' => $url, 'options' => $options, 'copyTo' => null], true); + $promise->then(null, function (\Throwable $e) { + // suppress error as it is rethrown to the caller by getResponse() a few lines below + }); $this->wait($job['id']); $response = $this->getResponse($job['id']); @@ -123,6 +126,7 @@ public function get(string $url, array $options = []) * 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 +168,7 @@ public function copy(string $url, string $to, array $options = []) * 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 +204,7 @@ public function setOptions(array $options) /** * @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 a1abed9a51d3..ca24e69b498e 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 @@ public function getProcessExecutor(): ?ProcessExecutor } /** - * @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 @@ static function ($e) use (&$uncaught): void { } unset($this->currentPromises[$waitIndex]); - if ($uncaught) { + if (null !== $uncaught) { throw $uncaught; } } @@ -116,9 +115,8 @@ public function abortJobs(): void { 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/PackageInfo.php b/src/Composer/Util/PackageInfo.php index 0b2607ac9cac..e93c584475c3 100644 --- a/src/Composer/Util/PackageInfo.php +++ b/src/Composer/Util/PackageInfo.php @@ -19,7 +19,7 @@ class PackageInfo { public static function getViewSourceUrl(PackageInterface $package): ?string { - if ($package instanceof CompletePackageInterface && isset($package->getSupport()['source'])) { + if ($package instanceof CompletePackageInterface && isset($package->getSupport()['source']) && '' !== $package->getSupport()['source']) { return $package->getSupport()['source']; } @@ -28,6 +28,12 @@ public static function getViewSourceUrl(PackageInterface $package): ?string public static function getViewSourceOrHomepageUrl(PackageInterface $package): ?string { - return self::getViewSourceUrl($package) ?? ($package instanceof CompletePackageInterface ? $package->getHomepage() : null); + $url = self::getViewSourceUrl($package) ?? ($package instanceof CompletePackageInterface ? $package->getHomepage() : null); + + if ($url === '') { + return null; + } + + return $url; } } diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index a8f69ecebe0c..25e4c903b57c 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -155,6 +155,7 @@ private function doExecute($command, ?string $cwd, bool $tty, &$output = null): * * @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 ffb51af6d95e..9a7398cc0590 100644 --- a/src/Composer/Util/SyncHelper.php +++ b/src/Composer/Util/SyncHelper.php @@ -58,6 +58,7 @@ public static function downloadAndInstallPackageSync(Loop $loop, $downloader, st * 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 b4a229e9a9c0..84b8693cf1b7 100644 --- a/tests/Composer/Test/Advisory/AuditorTest.php +++ b/tests/Composer/Test/Advisory/AuditorTest.php @@ -71,6 +71,149 @@ public function testAudit(array $data, int $expected, string $message): void $this->assertSame($expected, $result, $message); } + public function ignoredIdsProvider(): \Generator { + yield 'ignore by CVE' => [ + [ + new Package('vendor1/package1', '3.0.0.0', '3.0.0'), + ], + ['CVE1'], + 0, + [ + ['text' => 'Found 1 ignored security vulnerability advisory affecting 1 package:'], + ['text' => 'Package: vendor1/package1'], + ['text' => 'CVE: CVE1'], + ['text' => 'Title: advisory1'], + ['text' => 'URL: https://advisory.example.com/advisory1'], + ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'], + ['text' => 'Reported at: 2022-05-25T13:21:00+00:00'], + ] + ]; + yield 'ignore by CVE with reasoning' => [ + [ + new Package('vendor1/package1', '3.0.0.0', '3.0.0'), + ], + ['CVE1' => 'A good reason'], + 0, + [ + ['text' => 'Found 1 ignored security vulnerability advisory affecting 1 package:'], + ['text' => 'Package: vendor1/package1'], + ['text' => 'CVE: CVE1'], + ['text' => 'Title: advisory1'], + ['text' => 'URL: https://advisory.example.com/advisory1'], + ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'], + ['text' => 'Reported at: 2022-05-25T13:21:00+00:00'], + ['text' => 'Ignore reason: A good reason'], + ] + ]; + yield 'ignore by advisory id' => [ + [ + new Package('vendor1/package2', '3.0.0.0', '3.0.0'), + ], + ['ID2'], + 0, + [ + ['text' => 'Found 1 ignored security vulnerability advisory affecting 1 package:'], + ['text' => 'Package: vendor1/package2'], + ['text' => 'CVE: '], + ['text' => 'Title: advisory2'], + ['text' => 'URL: https://advisory.example.com/advisory2'], + ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'], + ['text' => 'Reported at: 2022-05-25T13:21:00+00:00'], + ] + ]; + yield 'ignore by remote id' => [ + [ + new Package('vendorx/packagex', '3.0.0.0', '3.0.0'), + ], + ['RemoteIDx'], + 0, + [ + ['text' => 'Found 1 ignored security vulnerability advisory affecting 1 package:'], + ['text' => 'Package: vendorx/packagex'], + ['text' => 'CVE: CVE5'], + ['text' => 'Title: advisory17'], + ['text' => 'URL: https://advisory.example.com/advisory17'], + ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'], + ['text' => 'Reported at: 2015-05-25T13:21:00+00:00'], + ] + ]; + yield '1 vulnerability, 0 ignored' => [ + [ + new Package('vendor1/package1', '3.0.0.0', '3.0.0'), + ], + [], + 1, + [ + ['text' => 'Found 1 security vulnerability advisory affecting 1 package:'], + ['text' => 'Package: vendor1/package1'], + ['text' => 'CVE: CVE1'], + ['text' => 'Title: advisory1'], + ['text' => 'URL: https://advisory.example.com/advisory1'], + ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'], + ['text' => 'Reported at: 2022-05-25T13:21:00+00:00'], + ] + ]; + yield '1 vulnerability, 3 ignored affecting 2 packages' => [ + [ + new Package('vendor3/package1', '3.0.0.0', '3.0.0'), + // RemoteIDx + new Package('vendorx/packagex', '3.0.0.0', '3.0.0'), + // ID3, ID6 + new Package('vendor2/package1', '3.0.0.0', '3.0.0'), + ], + ['RemoteIDx', 'ID3', 'ID6'], + 1, + [ + ['text' => 'Found 3 ignored security vulnerability advisories affecting 2 packages:'], + ['text' => 'Package: vendor2/package1'], + ['text' => 'CVE: CVE2'], + ['text' => 'Title: advisory3'], + ['text' => 'URL: https://advisory.example.com/advisory3'], + ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'], + ['text' => 'Reported at: 2022-05-25T13:21:00+00:00'], + ['text' => 'Ignore reason: None specified'], + ['text' => '--------'], + ['text' => 'Package: vendor2/package1'], + ['text' => 'CVE: CVE4'], + ['text' => 'Title: advisory6'], + ['text' => 'URL: https://advisory.example.com/advisory6'], + ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'], + ['text' => 'Reported at: 2015-05-25T13:21:00+00:00'], + ['text' => 'Ignore reason: None specified'], + ['text' => '--------'], + ['text' => 'Package: vendorx/packagex'], + ['text' => 'CVE: CVE5'], + ['text' => 'Title: advisory17'], + ['text' => 'URL: https://advisory.example.com/advisory17'], + ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'], + ['text' => 'Reported at: 2015-05-25T13:21:00+00:00'], + ['text' => 'Ignore reason: None specified'], + ['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'], + ] + ]; + } + + /** + * @dataProvider ignoredIdsProvider + * @phpstan-param array<\Composer\Package\Package> $packages + * @phpstan-param array|array $ignoredIds + * @phpstan-param 0|positive-int $exitCode + * @phpstan-param list $expectedOutput + */ + public function testAuditWithIgnore($packages, $ignoredIds, $exitCode, $expectedOutput): void + { + $auditor = new Auditor(); + $result = $auditor->audit($io = $this->getIOMock(), $this->getRepoSet(), $packages, Auditor::FORMAT_PLAIN, false, $ignoredIds); + $io->expects($expectedOutput, true); + $this->assertSame($exitCode, $result); + } + private function getRepoSet(): RepositorySet { $repo = $this @@ -160,7 +303,7 @@ public static function getMockAdvisories(): array 'sources' => [ [ 'name' => 'source2', - 'remoteId' => 'RemoteID2', + 'remoteId' => 'RemoteID4', ], ], 'reportedAt' => '2022-05-25 13:21:00', @@ -205,14 +348,14 @@ public static function getMockAdvisories(): array [ '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 new file mode 100644 index 000000000000..c8153b578263 --- /dev/null +++ b/tests/Composer/Test/Autoload/MinimumVersionSupport/.gitignore @@ -0,0 +1,2 @@ +/composer.lock +/vendor/ diff --git a/tests/Composer/Test/Command/ArchiveCommandTest.php b/tests/Composer/Test/Command/ArchiveCommandTest.php index fa7ce960ab2c..1ed93f5fdd81 100644 --- a/tests/Composer/Test/Command/ArchiveCommandTest.php +++ b/tests/Composer/Test/Command/ArchiveCommandTest.php @@ -15,9 +15,16 @@ use Composer\Composer; use Composer\Config; use Composer\Factory; +use Composer\Package\RootPackage; use Composer\Test\TestCase; use Composer\Util\Platform; use Symfony\Component\Console\Input\ArrayInput; +use Composer\Repository\RepositoryManager; +use Composer\Repository\InstalledRepositoryInterface; +use Composer\Package\Archiver\ArchiveManager; +use Composer\Command\ArchiveCommand; +use Composer\EventDispatcher\EventDispatcher; +use Symfony\Component\Console\Output\OutputInterface; class ArchiveCommandTest extends TestCase { @@ -98,4 +105,60 @@ public function testUsesConfigFromFactoryWhenComposerIsNotDefined(): void $this->assertEquals(0, $command->run($input, $output)); } + + public function testUsesConfigFromComposerObjectWithPackageName(): void + { + $input = new ArrayInput([ + 'package' => 'foo/bar', + ]); + + $output = $this->getMockBuilder(OutputInterface::class) + ->getMock(); + + $eventDispatcher = $this->getMockBuilder(EventDispatcher::class) + ->disableOriginalConstructor()->getMock(); + + $composer = new Composer; + $config = new Config; + $config->merge(['config' => ['archive-format' => 'zip']]); + $composer->setConfig($config); + + $manager = $this->getMockBuilder(ArchiveManager::class) + ->disableOriginalConstructor()->getMock(); + + $package = new RootPackage('foo/bar', '1.0.0', '1.0'); + + $installedRepository = $this->getMockBuilder(InstalledRepositoryInterface::class) + ->getMock(); + $installedRepository->expects($this->once())->method('loadPackages') + ->willReturn(['packages' => [$package], 'namesFound' => ['foo/bar']]); + + $repositoryManager = $this->getMockBuilder(RepositoryManager::class) + ->disableOriginalConstructor()->getMock(); + $repositoryManager->expects($this->once())->method('getLocalRepository') + ->willReturn($installedRepository); + $repositoryManager->expects($this->once())->method('getRepositories') + ->willReturn([]); + + $manager->expects($this->once())->method('archive') + ->with($package, 'zip', '.', null, false)->willReturn(Platform::getCwd()); + + $composer->setArchiveManager($manager); + $composer->setEventDispatcher($eventDispatcher); + $composer->setPackage($package); + $composer->setRepositoryManager($repositoryManager); + + $command = $this->getMockBuilder(ArchiveCommand::class) + ->onlyMethods([ + 'mergeApplicationDefinition', + 'getSynopsis', + 'initialize', + 'tryComposer', + 'requireComposer', + ])->getMock(); + $command->expects($this->atLeastOnce())->method('tryComposer') + ->willReturn($composer); + + $command->run($input, $output); + } } diff --git a/tests/Composer/Test/Command/BaseDependencyCommandTest.php b/tests/Composer/Test/Command/BaseDependencyCommandTest.php new file mode 100644 index 000000000000..78b41d223dda --- /dev/null +++ b/tests/Composer/Test/Command/BaseDependencyCommandTest.php @@ -0,0 +1,457 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + + +namespace Composer\Test\Command; + +use Composer\Semver\Constraint\Constraint; +use Composer\Semver\Constraint\MatchAllConstraint; +use Symfony\Component\Console\Command\Command; +use UnexpectedValueException; +use InvalidArgumentException; +use Composer\Test\TestCase; +use Composer\Package\Link; +use RuntimeException; +use Generator; + +class BaseDependencyCommandTest extends TestCase +{ + /** + * Test that an exception is throw when there weren't provided some parameters + * + * @covers \Composer\Command\BaseDependencyCommand + * @covers \Composer\Command\DependsCommand + * @covers \Composer\Command\ProhibitsCommand + * + * @dataProvider noParametersCaseProvider + * + * @param array $parameters + */ + public function testExceptionWhenNoRequiredParameters( + string $command, + array $parameters, + string $expectedExceptionMessage + ): void { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + + $appTester = $this->getApplicationTester(); + $this->assertEquals(Command::FAILURE, $appTester->run(['command' => $command] + $parameters)); + } + + /** + * @return Generator, string}> + */ + public static function noParametersCaseProvider(): Generator + { + yield '`why` command without package parameter' => [ + 'why', + [], + 'Not enough arguments (missing: "package").' + ]; + + yield '`why-not` command without package and version parameters' => [ + 'why-not', + [], + 'Not enough arguments (missing: "package, version").' + ]; + + yield '`why-not` command without package parameter' => [ + 'why-not', + ['version' => '*'], + 'Not enough arguments (missing: "package").' + ]; + + yield '`why-not` command without version parameter' => [ + 'why-not', + ['package' => 'vendor1/package1'], + 'Not enough arguments (missing: "version").' + ]; + } + + /** + * Test that an exception is throw when there wasn't provided the locked file alongside `--locked` parameter + * + * @covers \Composer\Command\BaseDependencyCommand + * @covers \Composer\Command\DependsCommand + * @covers \Composer\Command\ProhibitsCommand + * + * @dataProvider caseProvider + * + * @param array $parameters + */ + public function testExceptionWhenRunningLockedWithoutLockFile(string $command, array $parameters): void + { + $this->initTempComposer(); + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('A valid composer.lock file is required to run this command with --locked'); + + $appTester = $this->getApplicationTester(); + $this->assertEquals( + Command::FAILURE, + $appTester->run(['command' => $command] + $parameters + ['--locked' => true] + ) + ); + } + + /** + * Test that an exception is throw when the provided package to be inspected isn't required by the project + * + * @covers \Composer\Command\BaseDependencyCommand + * @covers \Composer\Command\DependsCommand + * @covers \Composer\Command\ProhibitsCommand + * + * @dataProvider caseProvider + * + * @param array $parameters + */ + public function testExceptionWhenItCouldNotFoundThePackage(string $command, array $parameters): void + { + $packageToBeInspected = $parameters['package']; + + $this->initTempComposer(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('Could not find package "%s" in your project', $packageToBeInspected)); + + $appTester = $this->getApplicationTester(); + $this->assertEquals( + Command::FAILURE, + $appTester->run(['command' => $command] + $parameters) + ); + } + + /** + * Test that it shows a warning message when the package to be inspected wasn't found in the project + * + * @covers \Composer\Command\BaseDependencyCommand + * @covers \Composer\Command\DependsCommand + * @covers \Composer\Command\ProhibitsCommand + * + * @dataProvider caseProvider + * + * @param array $parameters + */ + public function testExceptionWhenPackageWasNotFoundInProject(string $command, array $parameters): void + { + $packageToBeInspected = $parameters['package']; + + $this->initTempComposer([ + 'require' => [ + 'vendor1/package2' => '1.*', + 'vendor2/package1' => '2.*' + ] + ]); + + $firstRequiredPackage = self::getPackage('vendor1/package2'); + $secondRequiredPackage = self::getPackage('vendor2/package1'); + + $this->createInstalledJson([$firstRequiredPackage, $secondRequiredPackage]); + $this->createComposerLock([$firstRequiredPackage, $secondRequiredPackage]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('Could not find package "%s" in your project', $packageToBeInspected)); + + $appTester = $this->getApplicationTester(); + + $this->assertEquals(Command::FAILURE, $appTester->run(['command' => $command] + $parameters)); + } + + /** + * Test that it shows a warning message when the dependencies haven't been installed yet + * + * @covers \Composer\Command\BaseDependencyCommand + * @covers \Composer\Command\DependsCommand + * @covers \Composer\Command\ProhibitsCommand + * + * @dataProvider caseProvider + * + * @param array $parameters + */ + public function testWarningWhenDependenciesAreNotInstalled(string $command, array $parameters): void + { + $expectedWarningMessage = 'No dependencies installed. Try running composer install or update, or use --locked.'; + + $this->initTempComposer([ + 'require' => [ + 'vendor1/package1' => '1.*' + ], + 'require-dev' => [ + 'vendor2/package1' => '2.*' + ] + ]); + + $someRequiredPackage = self::getPackage('vendor1/package1'); + $someDevRequiredPackage = self::getPackage('vendor2/package1'); + + $this->createComposerLock([$someRequiredPackage], [$someDevRequiredPackage]); + + $appTester = $this->getApplicationTester(); + $appTester->run(['command' => $command] + $parameters); + + $this->assertSame($expectedWarningMessage, trim($appTester->getDisplay(true))); + } + + /** + * @return Generator}> + */ + public static function caseProvider(): Generator + { + yield '`why` command' => [ + 'why', + ['package' => 'vendor1/package1'] + ]; + + yield '`why-not` command' => [ + 'why-not', + ['package' => 'vendor1/package1', 'version' => '1.*'] + ]; + } + + /** + * Test that it finishes successfully and show some expected outputs depending on different command parameters + * + * @covers \Composer\Command\BaseDependencyCommand + * @covers \Composer\Command\DependsCommand + * + * @dataProvider caseWhyProvider + * + * @param array $parameters + */ + public function testWhyCommandOutputs(array $parameters, string $expectedOutput): void + { + $packageToBeInspected = $parameters['package']; + $renderAsTree = $parameters['--tree'] ?? false; + $renderRecursively = $parameters['--recursive'] ?? false; + + $this->initTempComposer([ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'vendor1/package1', 'version' => '1.3.0', 'require' => ['vendor1/package2' => '^2']], + ['name' => 'vendor1/package2', 'version' => '2.3.0', 'require' => ['vendor1/package3' => '^1']], + ['name' => 'vendor1/package3', 'version' => '2.1.0'] + ], + ], + ], + 'require' => [ + 'vendor1/package2' => '1.3.0', + 'vendor1/package3' => '2.3.0', + ], + 'require-dev' => [ + 'vendor2/package1' => '2.*' + ] + ]); + + $firstRequiredPackage = self::getPackage('vendor1/package1', '1.3.0'); + $firstRequiredPackage->setRequires([ + 'vendor1/package2' => new Link( + 'vendor1/package1', + 'vendor1/package2', + new MatchAllConstraint(), + Link::TYPE_REQUIRE, + '^2' + ) + ]); + $secondRequiredPackage = self::getPackage('vendor1/package2', '2.3.0'); + $secondRequiredPackage->setRequires([ + 'vendor1/package3' => new Link( + 'vendor1/package2', + 'vendor1/package3', + new MatchAllConstraint(), + Link::TYPE_REQUIRE, + '^1' + ) + ]); + $thirdRequiredPackage = self::getPackage('vendor1/package3', '2.1.0'); + $someDevRequiredPackage = self::getPackage('vendor2/package1'); + $this->createComposerLock( + [$firstRequiredPackage, $secondRequiredPackage, $thirdRequiredPackage], + [$someDevRequiredPackage] + ); + $this->createInstalledJson( + [$firstRequiredPackage, $secondRequiredPackage, $thirdRequiredPackage], + [$someDevRequiredPackage] + ); + + $appTester = $this->getApplicationTester(); + $appTester->run([ + 'command' => 'why', + 'package' => $packageToBeInspected, + '--tree' => $renderAsTree, + '--recursive' => $renderRecursively, + '--locked' => true + ]); + + $appTester->assertCommandIsSuccessful(); + + $this->assertEquals(trim($expectedOutput), trim($appTester->getDisplay(true))); + } + + /** + * @return Generator, string}> + */ + public static function caseWhyProvider(): Generator + { + yield 'there is no installed package depending on the package' => [ + ['package' => 'vendor1/package1'], + 'There is no installed package depending on "vendor1/package1"' + ]; + + yield 'a nested package dependency' => [ + ['package' => 'vendor1/package3'], + << [ + ['package' => 'vendor1/package3', '--tree' => true], + << [ + ['package' => 'vendor1/package3', '--recursive' => true], + << [ + ['package' => 'vendor2/package1'], + '__root__ - requires (for development) vendor2/package1 (2.*)' + ]; + } + + /** + * Test that it finishes successfully and show some expected outputs depending on different command parameters + * + * @covers \Composer\Command\BaseDependencyCommand + * @covers \Composer\Command\ProhibitsCommand + * + * @dataProvider caseWhyNotProvider + * + * @param array $parameters + */ + public function testWhyNotCommandOutputs(array $parameters, string $expectedOutput): void + { + $packageToBeInspected = $parameters['package']; + $packageVersionToBeInspected = $parameters['version']; + + $this->initTempComposer([ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'vendor1/package1', 'version' => '1.3.0'], + ['name' => 'vendor2/package1', 'version' => '2.0.0'], + ['name' => 'vendor2/package2', 'version' => '1.0.0', 'require' => ['vendor2/package3' => '1.4.*']], + ['name' => 'vendor2/package3', 'version' => '1.4.0'], + ['name' => 'vendor2/package3', 'version' => '1.5.0'] + ], + ], + ], + 'require' => [ + 'vendor1/package1' => '1.*' + ], + 'require-dev' => [ + 'vendor2/package1' => '2.*', + 'vendor2/package2' => '^1' + ] + ]); + + $someRequiredPackage = self::getPackage('vendor1/package1', '1.3.0'); + $firstDevRequiredPackage = self::getPackage('vendor2/package1', '2.0.0'); + $secondDevRequiredPackage = self::getPackage('vendor2/package2', '1.0.0'); + $secondDevRequiredPackage->setRequires([ + 'vendor2/package3' => new Link( + 'vendor2/package2', + 'vendor2/package3', + new MatchAllConstraint(), + Link::TYPE_REQUIRE, + '1.4.*' + ) + ]); + $secondDevNestedRequiredPackage = self::getPackage('vendor2/package3', '1.4.0'); + + $this->createComposerLock( + [$someRequiredPackage], + [$firstDevRequiredPackage, $secondDevRequiredPackage] + ); + $this->createInstalledJson( + [$someRequiredPackage], + [$firstDevRequiredPackage, $secondDevRequiredPackage, $secondDevNestedRequiredPackage] + ); + + $appTester = $this->getApplicationTester(); + $appTester->run([ + 'command' => 'why-not', + 'package' => $packageToBeInspected, + 'version' => $packageVersionToBeInspected + ]); + + $appTester->assertCommandIsSuccessful(); + $this->assertSame(trim($expectedOutput), trim($appTester->getDisplay(true))); + } + + /** + * @return Generator, string}> + */ + public function caseWhyNotProvider(): Generator + { + yield 'it could not found the package with a specific version' => [ + ['package' => 'vendor1/package1', 'version' => '3.*'], + << [ + ['package' => 'vendor1/package1', 'version' => '^1.4'], + << [ + ['package' => 'vendor1/package1', 'version' => '^1.3'], + << [ + ['package' => 'vendor2/package3', 'version' => '1.5.0'], + <<getApplicationTester(); + $this->assertSame(0, $appTester->run(['command' => 'dump-autoload'])); + + $output = $appTester->getDisplay(true); + $this->assertStringContainsString('Generating autoload files', $output); + $this->assertStringContainsString('Generated autoload files', $output); + } + + public function testDumpDevAutoload(): void + { + $appTester = $this->getApplicationTester(); + $this->assertSame(0, $appTester->run(['command' => 'dump-autoload', '--dev' => true])); + + $output = $appTester->getDisplay(true); + $this->assertStringContainsString('Generating autoload files', $output); + $this->assertStringContainsString('Generated autoload files', $output); + } + + public function testDumpNoDevAutoload(): void + { + $appTester = $this->getApplicationTester(); + $this->assertSame(0, $appTester->run(['command' => 'dump-autoload', '--dev' => true])); + + $output = $appTester->getDisplay(true); + $this->assertStringContainsString('Generating autoload files', $output); + $this->assertStringContainsString('Generated autoload files', $output); + } + + public function testUsingOptimizeAndStrictPsr(): void + { + $appTester = $this->getApplicationTester(); + $this->assertSame(0, $appTester->run(['command' => 'dump-autoload', '--optimize' => true, '--strict-psr' => true])); + + $output = $appTester->getDisplay(true); + $this->assertStringContainsString('Generating optimized autoload files', $output); + $this->assertMatchesRegularExpression('/Generated optimized autoload files containing \d+ classes/', $output); + } + + public function testFailsUsingStrictPsrIfClassMapViolationsAreFound(): void + { + $dir = $this->initTempComposer([ + 'autoload' => [ + 'psr-4' => [ + 'Application\\' => 'src', + ] + ] + ]); + mkdir($dir . '/src/'); + file_put_contents($dir . '/src/Foo.php', 'getApplicationTester(); + $this->assertSame(1, $appTester->run(['command' => 'dump-autoload', '--optimize' => true, '--strict-psr' => true])); + + $output = $appTester->getDisplay(true); + $this->assertMatchesRegularExpression('/Class Application\\\Src\\\Foo located in .*? does not comply with psr-4 autoloading standard. Skipping./', $output); + } + + public function testUsingClassmapAuthoritative(): void + { + $appTester = $this->getApplicationTester(); + $this->assertSame(0, $appTester->run(['command' => 'dump-autoload', '--classmap-authoritative' => true])); + + $output = $appTester->getDisplay(true); + $this->assertStringContainsString('Generating optimized autoload files (authoritative)', $output); + $this->assertMatchesRegularExpression('/Generated optimized autoload files \(authoritative\) containing \d+ classes/', $output); + } + + public function testUsingClassmapAuthoritativeAndStrictPsr(): void + { + $appTester = $this->getApplicationTester(); + $this->assertSame(0, $appTester->run(['command' => 'dump-autoload', '--classmap-authoritative' => true, '--strict-psr' => true])); + + $output = $appTester->getDisplay(true); + $this->assertStringContainsString('Generating optimized autoload files', $output); + $this->assertMatchesRegularExpression('/Generated optimized autoload files \(authoritative\) containing \d+ classes/', $output); + } + + public function testStrictPsrDoesNotWorkWithoutOptimizedAutoloader(): void + { + $appTester = $this->getApplicationTester(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('--strict-psr mode only works with optimized autoloader, use --optimize or --classmap-authoritative if you want a strict return value.'); + $appTester->run(['command' => 'dump-autoload', '--strict-psr' => true]); + } + + public function testDevAndNoDevCannotBeCombined(): void + { + $appTester = $this->getApplicationTester(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('You can not use both --no-dev and --dev as they conflict with each other.'); + $appTester->run(['command' => 'dump-autoload', '--dev' => true, '--no-dev' => true]); + } +} diff --git a/tests/Composer/Test/Command/HomeCommandTest.php b/tests/Composer/Test/Command/HomeCommandTest.php index 1755255a5faf..fd37e2f89fe3 100644 --- a/tests/Composer/Test/Command/HomeCommandTest.php +++ b/tests/Composer/Test/Command/HomeCommandTest.php @@ -57,7 +57,7 @@ public function testHomeCommandWithShowFlag( $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 0e1bf0adaf9b..c3b7196186e4 100644 --- a/tests/Composer/Test/Command/ShowCommandTest.php +++ b/tests/Composer/Test/Command/ShowCommandTest.php @@ -288,12 +288,19 @@ public function testShowPlatformWorksWithoutComposerJson(): void 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 @@ public function testShowAllShowsAllSections(): void 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 217f41157ed5..4d64d4bf6423 100644 --- a/tests/Composer/Test/Command/SuggestsCommandTest.php +++ b/tests/Composer/Test/Command/SuggestsCommandTest.php @@ -129,7 +129,7 @@ public function testSuggest(bool $hasLockFile, array $command, string $expected) 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/DocumentationTest.php b/tests/Composer/Test/DocumentationTest.php index e7a49abded3c..d7e7ef856713 100644 --- a/tests/Composer/Test/DocumentationTest.php +++ b/tests/Composer/Test/DocumentationTest.php @@ -53,6 +53,9 @@ public static function provideCommandCases(): \Generator { $application = new Application(); $application->setAutoExit(false); + if (method_exists($application, 'setCatchErrors')) { + $application->setCatchErrors(false); + } $application->setCatchExceptions(false); $description = new ApplicationDescription($application); diff --git a/tests/Composer/Test/Downloader/ZipDownloaderTest.php b/tests/Composer/Test/Downloader/ZipDownloaderTest.php index 56aa7a04d457..80e9378d99fd 100644 --- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php @@ -65,11 +65,7 @@ public function setPrivateProperty(string $name, $value, $obj = null): void $reflectionClass = new \ReflectionClass('Composer\Downloader\ZipDownloader'); $reflectedProperty = $reflectionClass->getProperty($name); $reflectedProperty->setAccessible(true); - if ($obj === null) { - $reflectedProperty->setValue($value); - } else { - $reflectedProperty->setValue($obj, $value); - } + $reflectedProperty->setValue($obj, $value); } public function testErrorMessages(): void @@ -314,7 +310,7 @@ public function testNonWindowsFallbackFailed(): void } /** - * @param ?\React\Promise\PromiseInterface $promise + * @param ?\React\Promise\PromiseInterface $promise */ private function wait($promise): void { @@ -329,7 +325,7 @@ private function wait($promise): void $e = $ex; }); - if ($e) { + if ($e !== null) { throw $e; } } diff --git a/tests/Composer/Test/InstalledVersionsTest.php b/tests/Composer/Test/InstalledVersionsTest.php index 6edd91e04cea..c227675ca73b 100644 --- a/tests/Composer/Test/InstalledVersionsTest.php +++ b/tests/Composer/Test/InstalledVersionsTest.php @@ -33,14 +33,14 @@ public static function setUpBeforeClass(): void $prop = new \ReflectionProperty('Composer\Autoload\ClassLoader', 'registeredLoaders'); $prop->setAccessible(true); self::$previousRegisteredLoaders = $prop->getValue(); - $prop->setValue([]); + $prop->setValue(null, []); } public static function tearDownAfterClass(): void { $prop = new \ReflectionProperty('Composer\Autoload\ClassLoader', 'registeredLoaders'); $prop->setAccessible(true); - $prop->setValue(self::$previousRegisteredLoaders); + $prop->setValue(null, self::$previousRegisteredLoaders); InstalledVersions::reload(null); // @phpstan-ignore-line } diff --git a/tests/Composer/Test/Json/Fixtures/tabs.json b/tests/Composer/Test/Json/Fixtures/tabs.json new file mode 100644 index 000000000000..460b5331d249 --- /dev/null +++ b/tests/Composer/Test/Json/Fixtures/tabs.json @@ -0,0 +1,3 @@ +{ + "foo": "bar" +} diff --git a/tests/Composer/Test/Json/JsonFileTest.php b/tests/Composer/Test/Json/JsonFileTest.php index 6f0ad2e74c68..74b124ec3891 100644 --- a/tests/Composer/Test/Json/JsonFileTest.php +++ b/tests/Composer/Test/Json/JsonFileTest.php @@ -364,6 +364,29 @@ public function testDoubleEscapedUnicode(): void $this->assertEquals($data, $doubleData); } + public function testPreserveIndentationAfterRead(): void + { + copy(__DIR__.'/Fixtures/tabs.json', __DIR__.'/Fixtures/tabs2.json'); + $jsonFile = new JsonFile(__DIR__.'/Fixtures/tabs2.json'); + $data = $jsonFile->read(); + $jsonFile->write(['foo' => 'baz']); + + self::assertSame("{\n\t\"foo\": \"baz\"\n}\n", file_get_contents(__DIR__.'/Fixtures/tabs2.json')); + + unlink(__DIR__.'/Fixtures/tabs2.json'); + } + + public function testOverwritesIndentationByDefault(): void + { + copy(__DIR__.'/Fixtures/tabs.json', __DIR__.'/Fixtures/tabs2.json'); + $jsonFile = new JsonFile(__DIR__.'/Fixtures/tabs2.json'); + $jsonFile->write(['foo' => 'baz']); + + self::assertSame("{\n \"foo\": \"baz\"\n}\n", file_get_contents(__DIR__.'/Fixtures/tabs2.json')); + + unlink(__DIR__.'/Fixtures/tabs2.json'); + } + private function expectParseException(string $text, string $json): void { try { diff --git a/tests/Composer/Test/Mock/HttpDownloaderMock.php b/tests/Composer/Test/Mock/HttpDownloaderMock.php index 538124a1d592..0005a5d87770 100644 --- a/tests/Composer/Test/Mock/HttpDownloaderMock.php +++ b/tests/Composer/Test/Mock/HttpDownloaderMock.php @@ -112,8 +112,10 @@ public function get($fileUrl, $options = []): Response } throw new AssertionFailedError( - 'Received unexpected request for "'.$fileUrl.'"'.PHP_EOL. - (is_array($this->expectations) && count($this->expectations) > 0 ? 'Expected "'.$this->expectations[0]['url'].'" at this point.' : 'Expected no more calls at this point.').PHP_EOL. + 'Received unexpected request for "'.$fileUrl.'" with options "'.json_encode($options).'"'.PHP_EOL. + (is_array($this->expectations) && count($this->expectations) > 0 + ? 'Expected "'.$this->expectations[0]['url'].($this->expectations[0]['options'] !== null ? '" with options "'.json_encode($this->expectations[0]['options']) : '').'" at this point.' + : 'Expected no more calls at this point.').PHP_EOL. 'Received calls:'.PHP_EOL.implode(PHP_EOL, array_slice($this->log, 0, -1)) ); } diff --git a/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php b/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php index 1d4a7d4b54b0..ca07aede41a8 100644 --- a/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php +++ b/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php @@ -21,7 +21,7 @@ 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 867bf11e742f..9e2cb1a3a502 100644 --- a/tests/Composer/Test/Package/Archiver/ArchiverTest.php +++ b/tests/Composer/Test/Package/Archiver/ArchiverTestCase.php @@ -17,7 +17,7 @@ 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 02f779b464e0..1d6e68e2ec60 100644 --- a/tests/Composer/Test/Package/Archiver/PharArchiverTest.php +++ b/tests/Composer/Test/Package/Archiver/PharArchiverTest.php @@ -15,7 +15,7 @@ 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 c672f4f296c8..80c7bb95ecf3 100644 --- a/tests/Composer/Test/Package/Archiver/ZipArchiverTest.php +++ b/tests/Composer/Test/Package/Archiver/ZipArchiverTest.php @@ -16,7 +16,7 @@ use ZipArchive; use Composer\Package\Archiver\ZipArchiver; -class ZipArchiverTest extends ArchiverTest +class ZipArchiverTest extends ArchiverTestCase { /** * @dataProvider provideGitignoreExcludeNegationTestCases diff --git a/tests/Composer/Test/Package/Version/VersionBumperTest.php b/tests/Composer/Test/Package/Version/VersionBumperTest.php index 1a53a898d029..b8f07844f6c0 100644 --- a/tests/Composer/Test/Package/Version/VersionBumperTest.php +++ b/tests/Composer/Test/Package/Version/VersionBumperTest.php @@ -63,7 +63,8 @@ public static function provideBumpRequirementTests(): Generator yield 'leave minor wildcard alone' => ['2.4.*', '2.4.3', '2.4.*']; yield 'leave patch wildcard alone' => ['2.4.3.*', '2.4.3.2', '2.4.3.*']; yield 'upgrade tilde to caret when compatible' => ['~2.2', '2.4.3', '^2.4.3']; - yield 'leave patch-only-tilde alone' => ['~2.2.3', '2.2.6', '~2.2.3']; + 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 'leave bigger-than untouched' => ['>2.2.3', '2.2.6', '>2.2.3']; } diff --git a/tests/Composer/Test/Repository/ComposerRepositoryTest.php b/tests/Composer/Test/Repository/ComposerRepositoryTest.php index 19192ffda0a0..6eba1081dc32 100644 --- a/tests/Composer/Test/Repository/ComposerRepositoryTest.php +++ b/tests/Composer/Test/Repository/ComposerRepositoryTest.php @@ -16,6 +16,7 @@ use Composer\Json\JsonFile; use Composer\Repository\ComposerRepository; use Composer\Repository\RepositoryInterface; +use Composer\Semver\Constraint\Constraint; use Composer\Test\Mock\FactoryMock; use Composer\Test\TestCase; use Composer\Package\Loader\ArrayLoader; @@ -160,7 +161,7 @@ public function testWhatProvides(): void ], ])); - $reflMethod = new \ReflectionMethod($repo, 'whatProvides'); + $reflMethod = new \ReflectionMethod(ComposerRepository::class, 'whatProvides'); $reflMethod->setAccessible(true); $packages = $reflMethod->invoke($repo, 'a'); @@ -380,4 +381,53 @@ public function testGetProviderNamesWillReturnPartialPackageNames(): void $this->assertEquals(['foo/bar'], $repository->getPackageNames()); } + + public function testGetSecurityAdvisoriesAssertRepositoryHttpOptionsAreUsed(): void + { + $httpDownloader = $this->getHttpDownloaderMock(); + $httpDownloader->expects( + [ + [ + 'url' => 'https://example.org/packages.json', + 'body' => JsonFile::encode([ + 'packages' => ['foo/bar' => [ + 'dev-branch' => ['name' => 'foo/bar'], + 'v1.0.0' => ['name' => 'foo/bar'], + ]], + 'metadata-url' => 'https://example.org/p2/%package%.json', + 'security-advisories' => [ + 'api-url' => 'https://example.org/security-advisories', + ], + ]), + 'options' => ['http' => ['verify_peer' => false]], + ], + [ + 'url' => 'https://example.org/security-advisories', + 'body' => JsonFile::encode(['advisories' => []]), + 'options' => ['http' => [ + 'verify_peer' => false, + 'method' => 'POST', + 'header' => [ + 'Content-type: application/x-www-form-urlencoded', + ], + 'timeout' => 10, + 'content' => http_build_query(['packages' => ['foo/bar']]), + ]], + ] + ], + true + ); + + $repository = new ComposerRepository( + ['url' => 'https://example.org/packages.json', 'options' => ['http' => ['verify_peer' => false]]], + new NullIO(), + FactoryMock::createConfig(), + $httpDownloader + ); + + $this->assertSame([ + 'namesFound' => [], + 'advisories' => [], + ], $repository->getSecurityAdvisories(['foo/bar' => new Constraint('=', '1.0.0.0')])); + } } diff --git a/tests/Composer/Test/Repository/PlatformRepositoryTest.php b/tests/Composer/Test/Repository/PlatformRepositoryTest.php index 0d897003ea4e..9e791a77684d 100644 --- a/tests/Composer/Test/Repository/PlatformRepositoryTest.php +++ b/tests/Composer/Test/Repository/PlatformRepositoryTest.php @@ -369,7 +369,6 @@ public static function provideLibraryTestCases(): array 'curl: libssh not libssh2' => [ 'curl', ' - curl cURL support => enabled @@ -412,6 +411,57 @@ public static function provideLibraryTestCases(): array ], [['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 8431a1b49e20..aa454e8946d5 100644 --- a/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php @@ -401,6 +401,34 @@ public static function supportsProvider(): array ]; } + 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/TestCase.php b/tests/Composer/Test/TestCase.php index 6a2d7ce86dd3..357fb9264155 100644 --- a/tests/Composer/Test/TestCase.php +++ b/tests/Composer/Test/TestCase.php @@ -198,6 +198,9 @@ public function getApplicationTester(): ApplicationTester $application = new Application(); $application->setAutoExit(false); $application->setCatchExceptions(false); + if (method_exists($application, 'setCatchErrors')) { + $application->setCatchErrors(false); + } return new ApplicationTester($application); } diff --git a/tests/Composer/Test/Util/FilesystemTest.php b/tests/Composer/Test/Util/FilesystemTest.php index e5ea5b7a96fe..83a28d75b5a1 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 @@ public function testJunctions(): void $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 c42f7de1dffd..a8455c7ab3af 100644 --- a/tests/Composer/Test/Util/ProcessExecutorTest.php +++ b/tests/Composer/Test/Util/ProcessExecutorTest.php @@ -120,7 +120,6 @@ public function testExecuteAsyncCancel(): void $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(); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 494fa689413e..05b88f2f69f5 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -27,6 +27,11 @@ Platform::putEnv('COMPOSER_TESTS_ARE_RUNNING', '1'); +// ensure Windows color support detection does not attempt to use colors +// as this is dependent on env vars and not actual stream capabilities, see +// https://github.com/composer/composer/issues/11598 +Platform::putEnv('NO_COLOR', '1'); + // symfony/phpunit-bridge sets some default env vars which we do not need polluting the test env Platform::clearEnv('COMPOSER'); Platform::clearEnv('COMPOSER_VENDOR_DIR');