diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a48af7ba..6d75a0992 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +### [2.5.6] 2023-05-24 + + * BC Warning: Installers and `InstallationManager::getInstallPath` will now return `null` instead of an empty string for metapackages' paths. This may have adverse effects on plugin code using this expecting always a string but it is unlikely (#11455) + * Fixed metapackages showing their install path as the root package's path instead of empty (#11455) + * Fixed lock file verification on `install` to deal better with `replace`/`provide` (#11475) + * Fixed lock file having a more recent modification time than the vendor dir when `require` guesses the constraint after resolution (#11405) + * Fixed numeric default branches with a `v` prefix being treated as non-numeric ones and receiving an alias like e.g. dev-main would (e51d755a08) + * Fixed binary proxies not being transparent when included by another PHP process and returning a value (#11454) + * Fixed support for plugin classes being marked as `readonly` (#11404) + * Fixed `getmypid` being required as it is not always available (#11401) + * Fixed authentication issue when downloading several files from private Bitbucket in parallel (#11464) + ### [2.5.5] 2023-03-21 * Fixed basic auth failures resulting in infinite retry loop (#11320) @@ -1708,6 +1720,7 @@ * Initial release +[2.5.6]: https://github.com/composer/composer/compare/2.5.5...2.5.6 [2.5.5]: https://github.com/composer/composer/compare/2.5.4...2.5.5 [2.5.4]: https://github.com/composer/composer/compare/2.5.3...2.5.4 [2.5.3]: https://github.com/composer/composer/compare/2.5.2...2.5.3 diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index f085e5c41..e03b6bd6a 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -18,6 +18,7 @@ use Composer\Pcre\Preg; use Composer\Repository\InstalledRepository; use Composer\Repository\LockArrayRepository; use Composer\Repository\PlatformRepository; +use Composer\Repository\RootPackageRepository; use Composer\Util\ProcessExecutor; use Composer\Package\Dumper\ArrayDumper; use Composer\Package\Loader\ArrayLoader; @@ -514,9 +515,10 @@ class Locker if ($includeDev === true) { $sets[] = ['repo' => $this->getLockedRepository(true), 'method' => 'getDevRequires', 'description' => 'Required (in require-dev)']; } + $rootRepo = new RootPackageRepository($package); foreach ($sets as $set) { - $installedRepo = new InstalledRepository([$set['repo']]); + $installedRepo = new InstalledRepository([$set['repo'], $rootRepo]); foreach (call_user_func([$package, $set['method']]) as $link) { if (PlatformRepository::isPlatformPackage($link->getTarget())) { @@ -527,9 +529,21 @@ class Locker } if ($installedRepo->findPackagesWithReplacersAndProviders($link->getTarget(), $link->getConstraint()) === []) { $results = $installedRepo->findPackagesWithReplacersAndProviders($link->getTarget()); + if ($results !== []) { $provider = reset($results); - $missingRequirementInfo[] = '- ' . $set['description'].' package "' . $link->getTarget() . '" is in the lock file as "'.$provider->getPrettyVersion().'" but that does not satisfy your constraint "'.$link->getPrettyConstraint().'".'; + $description = $provider->getPrettyVersion(); + if ($provider->getName() !== $link->getTarget()) { + foreach (['getReplaces' => 'replaced as %s by %s', 'getProvides' => 'provided as %s by %s'] as $method => $text) { + foreach (call_user_func([$provider, $method]) as $providerLink) { + if ($providerLink->getTarget() === $link->getTarget()) { + $description = sprintf($text, $providerLink->getPrettyConstraint(), $provider->getPrettyName().' '.$provider->getPrettyVersion()); + break 2; + } + } + } + } + $missingRequirementInfo[] = '- ' . $set['description'].' package "' . $link->getTarget() . '" is in the lock file as "'.$description.'" but that does not satisfy your constraint "'.$link->getPrettyConstraint().'".'; } else { $missingRequirementInfo[] = '- ' . $set['description'].' package "' . $link->getTarget() . '" is not present in the lock file.'; } diff --git a/src/Composer/Util/AuthHelper.php b/src/Composer/Util/AuthHelper.php index ec3476670..3f0312602 100644 --- a/src/Composer/Util/AuthHelper.php +++ b/src/Composer/Util/AuthHelper.php @@ -28,6 +28,8 @@ class AuthHelper protected $config; /** @var array Map of origins to message displayed */ private $displayedOriginAuthentications = []; + /** @var array Map of URLs and whether they already retried with authentication from Bitbucket */ + private $bitbucketRetry = []; public function __construct(IOInterface $io, Config $config) { @@ -164,6 +166,12 @@ class AuthHelper $this->io->setAuthentication($origin, 'x-token-auth', $accessToken); $askForOAuthToken = false; } + } elseif (!isset($this->bitbucketRetry[$url])) { + // when multiple requests fire at the same time, they will all fail and the first one resets the token to be correct above but then the others + // reach the code path and without this fallback they would end up throwing below + // see https://github.com/composer/composer/pull/11464 for more details + $askForOAuthToken = false; + $this->bitbucketRetry[$url] = true; } else { throw new TransportException('Could not authenticate against ' . $origin, 401); } diff --git a/src/Composer/Util/Platform.php b/src/Composer/Util/Platform.php index 2d0c89d88..3c971d105 100644 --- a/src/Composer/Util/Platform.php +++ b/src/Composer/Util/Platform.php @@ -148,7 +148,7 @@ class Platform if ( !ini_get('open_basedir') && is_readable('/proc/version') - && false !== stripos(Silencer::call('file_get_contents', '/proc/version'), 'microsoft') + && false !== stripos((string)Silencer::call('file_get_contents', '/proc/version'), 'microsoft') && !file_exists('/.dockerenv') // docker running inside WSL should not be seen as WSL ) { return self::$isWindowsSubsystemForLinux = true; diff --git a/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test b/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test index f8db5ff03..32127b612 100644 --- a/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test +++ b/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test @@ -17,7 +17,15 @@ The locked version will not get overwritten by an install but fails on invalid p "require": { "foo/bar": "2.0.0", "foo/baz": "2.0.0", - "foo/self": "self.version" + "foo/self": "self.version", + "foo/provided": "1.0.0", + "foo/provided-wrong-version": "1.0.0", + "foo/root-replaced": "1.0.0", + "foo/root-replaced-wrong-version": "1.0.0" + }, + "replace": { + "foo/root-replaced": "^1", + "foo/root-replaced-wrong-version": "^2" } } --LOCK-- @@ -25,7 +33,14 @@ The locked version will not get overwritten by an install but fails on invalid p "packages": [ { "name": "foo/bar", "version": "1.0.0" }, { "name": "foo/baz", "version": "2.0.0" }, - { "name": "foo/self", "version": "1.2.2" } + { + "name": "foo/self", + "version": "1.2.2", + "provide": { + "foo/provided": "^1", + "foo/provided-wrong-version": "^3" + } + } ], "packages-dev": [], "aliases": [], @@ -45,6 +60,8 @@ install Installing dependencies from lock file (including require-dev) Verifying lock file contents can be installed on current platform. - Required package "foo/bar" is in the lock file as "1.0.0" but that does not satisfy your constraint "2.0.0". +- Required package "foo/provided-wrong-version" is in the lock file as "provided as ^3 by foo/self 1.2.2" but that does not satisfy your constraint "1.0.0". +- Required package "foo/root-replaced-wrong-version" is in the lock file as "replaced as ^2 by __root__ 1.2.3" but that does not satisfy your constraint "1.0.0". This usually happens when composer files are incorrectly merged or the composer.json file is manually edited. Read more about correctly resolving merge conflicts https://getcomposer.org/doc/articles/resolving-merge-conflicts.md and prefer using the "require" command over editing the composer.json file directly https://getcomposer.org/doc/03-cli.md#require-r diff --git a/tests/Composer/Test/Util/AuthHelperTest.php b/tests/Composer/Test/Util/AuthHelperTest.php index e9dd992c4..ee5802321 100644 --- a/tests/Composer/Test/Util/AuthHelperTest.php +++ b/tests/Composer/Test/Util/AuthHelperTest.php @@ -538,6 +538,71 @@ class AuthHelperTest extends TestCase $this->authHelper->promptAuthIfNeeded('https://gitlab.com/acme/archive.zip', $origin, 404, 'GitLab requires authentication and it was not provided'); } + public function testPromptAuthIfNeededMultipleBitbucketDownloads(): void + { + $origin = 'bitbucket.org'; + + $expectedResult = [ + 'retry' => true, + 'storeAuth' => false, + ]; + + $authConfig = [ + 'bitbucket.org' => [ + 'access-token' => 'bitbucket_access_token', + 'access-token-expiration' => time() + 1800, + ] + ]; + + $this->config + ->method('get') + ->willReturnMap([ + ['github-domains', 0, []], + ['gitlab-domains', 0, []], + ['bitbucket-oauth', 0, $authConfig], + ['github-domains', 0, []], + ['gitlab-domains', 0, []], + ]); + + $this->io + ->expects($this->exactly(2)) + ->method('hasAuthentication') + ->with($origin) + ->willReturn(true); + + $getAuthenticationReturnValues = [ + ['username' => 'bitbucket_client_id', 'password' => 'bitbucket_client_secret'], + ['username' => 'x-token-auth', 'password' => 'bitbucket_access_token'], + ]; + + $this->io + ->expects($this->exactly(2)) + ->method('getAuthentication') + ->willReturnCallback( + function ($repositoryName) use (&$getAuthenticationReturnValues) { + return array_shift($getAuthenticationReturnValues); + } + ); + + $this->io + ->expects($this->once()) + ->method('setAuthentication') + ->with($origin, 'x-token-auth', 'bitbucket_access_token'); + + $result1 = $this->authHelper->promptAuthIfNeeded('https://bitbucket.org/workspace/repo1/get/hash1.zip', $origin, 401, 'HTTP/2 401 '); + $result2 = $this->authHelper->promptAuthIfNeeded('https://bitbucket.org/workspace/repo2/get/hash2.zip', $origin, 401, 'HTTP/2 401 '); + + $this->assertSame( + $expectedResult, + $result1 + ); + + $this->assertSame( + $expectedResult, + $result2 + ); + } + /** * @param array $auth *