diff --git a/CHANGELOG.md b/CHANGELOG.md index 6200d2123f98..b5efd1683b79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +### [1.7.2] 2018-08-16 + + * Fixed reporting of authentication/rate limiting issues for GitHub API access + * Fixed `create-project` not checking the checking the latest commit out when a cache was already present + * Fixed reporting of errors when `global` command can not switch the working directory + * Fixed PHP 5.3 JSON encoding issues with complex unicode character sequences + * Updated to latest ca-bundle and xdebug-handler projects, see related changelogs + ### [1.7.1] 2018-08-07 * Fixed issue autoloading plugins in require-dev in some conditions @@ -679,6 +687,7 @@ * Initial release +[1.7.2]: https://github.com/composer/composer/compare/1.7.1...1.7.2 [1.7.1]: https://github.com/composer/composer/compare/1.7.0...1.7.1 [1.7.0]: https://github.com/composer/composer/compare/1.7.0-RC...1.7.0 [1.7.0-RC]: https://github.com/composer/composer/compare/1.6.5...1.7.0-RC diff --git a/composer.lock b/composer.lock index 873419108ff4..690aca23edd9 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "composer/ca-bundle", - "version": "1.1.1", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "d2c0a83b7533d6912e8d516756ebd34f893e9169" + "reference": "46afded9720f40b9dc63542af4e3e43a1177acb0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/d2c0a83b7533d6912e8d516756ebd34f893e9169", - "reference": "d2c0a83b7533d6912e8d516756ebd34f893e9169", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/46afded9720f40b9dc63542af4e3e43a1177acb0", + "reference": "46afded9720f40b9dc63542af4e3e43a1177acb0", "shasum": "" }, "require": { @@ -60,7 +60,7 @@ "ssl", "tls" ], - "time": "2018-03-29T19:57:20+00:00" + "time": "2018-08-08T08:57:40+00:00" }, { "name": "composer/semver", @@ -187,16 +187,16 @@ }, { "name": "composer/xdebug-handler", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "c919dc6c62e221fc6406f861ea13433c0aa24f08" + "reference": "e1809da56ce1bd1b547a752936817341ac244d8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/c919dc6c62e221fc6406f861ea13433c0aa24f08", - "reference": "c919dc6c62e221fc6406f861ea13433c0aa24f08", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/e1809da56ce1bd1b547a752936817341ac244d8e", + "reference": "e1809da56ce1bd1b547a752936817341ac244d8e", "shasum": "" }, "require": { @@ -227,7 +227,7 @@ "Xdebug", "performance" ], - "time": "2018-04-11T15:42:36+00:00" + "time": "2018-08-16T10:54:23+00:00" }, { "name": "justinrainbow/json-schema", diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index a7cc6650b4cb..cca5f18711c5 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -345,10 +345,6 @@ protected function installRootPackage(IOInterface $io, Config $config, $packageN $package = $package->getAliasOf(); } - if (0 === strpos($package->getPrettyVersion(), 'dev-') && in_array($package->getSourceType(), array('git', 'hg'))) { - $package->setSourceReference(substr($package->getPrettyVersion(), 4)); - } - $dm = $this->createDownloadManager($io, $config); $dm->setPreferSource($preferSource) ->setPreferDist($preferDist) diff --git a/src/Composer/Command/GlobalCommand.php b/src/Composer/Command/GlobalCommand.php index 9e87c7e175e7..e13b8aaf2e72 100644 --- a/src/Composer/Command/GlobalCommand.php +++ b/src/Composer/Command/GlobalCommand.php @@ -13,6 +13,7 @@ namespace Composer\Command; use Composer\Factory; +use Composer\Util\Filesystem; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\StringInput; @@ -75,8 +76,22 @@ public function run(InputInterface $input, OutputInterface $output) // change to global dir $config = Factory::createConfig(); - chdir($config->get('home')); - $this->getIO()->writeError('Changed current directory to '.$config->get('home').''); + $home = $config->get('home'); + + if (!is_dir($home)) { + $fs = new Filesystem(); + $fs->ensureDirectoryExists($home); + if (!is_dir($home)) { + throw new \RuntimeException('Could not create home directory'); + } + } + + try { + chdir($home); + } catch (\Exception $e) { + throw new \RuntimeException('Could not switch to home directory "'.$home.'"', 0, $e); + } + $this->getIO()->writeError('Changed current directory to '.$home.''); // create new input without "global" command prefix $input = new StringInput(preg_replace('{\bg(?:l(?:o(?:b(?:a(?:l)?)?)?)?)?\b}', '', $input->__toString(), 1)); diff --git a/src/Composer/Composer.php b/src/Composer/Composer.php index a3972f44fc85..b1a131a4f03a 100644 --- a/src/Composer/Composer.php +++ b/src/Composer/Composer.php @@ -29,9 +29,9 @@ */ class Composer { - const VERSION = '@package_version@'; - const BRANCH_ALIAS_VERSION = '@package_branch_alias_version@'; - const RELEASE_DATE = '@release_date@'; + const VERSION = '1.7.2'; + const BRANCH_ALIAS_VERSION = ''; + const RELEASE_DATE = '2018-08-16 16:57:12'; /** * @var Package\RootPackageInterface diff --git a/src/Composer/Json/JsonFormatter.php b/src/Composer/Json/JsonFormatter.php index 680a57bafc96..44acaff5929b 100644 --- a/src/Composer/Json/JsonFormatter.php +++ b/src/Composer/Json/JsonFormatter.php @@ -69,6 +69,13 @@ public static function format($json, $unescapeUnicode, $unescapeSlashes) $l = strlen($match[1]); if ($l % 2) { + $code = hexdec($match[2]); + // 0xD800..0xDFFF denotes UTF-16 surrogate pair which won't be unescaped + // see https://github.com/composer/composer/issues/7510 + if (0xD800 <= $code && 0xDFFF >= $code) { + return $match[0]; + } + return str_repeat('\\', $l - 1) . mb_convert_encoding( pack('H*', $match[2]), 'UTF-8', diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php index 5c5c08cf2423..e150ccd10110 100644 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -378,12 +378,7 @@ protected function getContents($url, $fetchingRepoData = false) return $this->attemptCloneFallback(); } - $rateLimited = false; - foreach ($e->getHeaders() as $header) { - if (preg_match('{^X-RateLimit-Remaining: *0$}i', trim($header))) { - $rateLimited = true; - } - } + $rateLimited = $githubUtil->isRateLimited($e->getHeaders()); if (!$this->io->hasAuthentication($this->originUrl)) { if (!$this->io->isInteractive()) { @@ -397,7 +392,7 @@ protected function getContents($url, $fetchingRepoData = false) } if ($rateLimited) { - $rateLimit = $this->getRateLimit($e->getHeaders()); + $rateLimit = $githubUtil->getRateLimit($e->getHeaders()); $this->io->writeError(sprintf( 'GitHub API limit (%d calls/hr) is exhausted. You are already authorized so you have to wait until %s before doing more requests', $rateLimit['limit'], @@ -413,39 +408,6 @@ protected function getContents($url, $fetchingRepoData = false) } } - /** - * Extract ratelimit from response. - * - * @param array $headers Headers from Composer\Downloader\TransportException. - * - * @return array Associative array with the keys limit and reset. - */ - protected function getRateLimit(array $headers) - { - $rateLimit = array( - 'limit' => '?', - 'reset' => '?', - ); - - foreach ($headers as $header) { - $header = trim($header); - if (false === strpos($header, 'X-RateLimit-')) { - continue; - } - list($type, $value) = explode(':', $header, 2); - switch ($type) { - case 'X-RateLimit-Limit': - $rateLimit['limit'] = (int) trim($value); - break; - case 'X-RateLimit-Reset': - $rateLimit['reset'] = date('Y-m-d H:i:s', (int) trim($value)); - break; - } - } - - return $rateLimit; - } - /** * Fetch root identifier from GitHub * diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php index 8415c9a5c9a9..2f5dbe5cda28 100644 --- a/src/Composer/Util/GitHub.php +++ b/src/Composer/Util/GitHub.php @@ -126,4 +126,55 @@ public function authorizeOAuthInteractively($originUrl, $message = null) return true; } + + /** + * Extract ratelimit from response. + * + * @param array $headers Headers from Composer\Downloader\TransportException. + * + * @return array Associative array with the keys limit and reset. + */ + public function getRateLimit(array $headers) + { + $rateLimit = array( + 'limit' => '?', + 'reset' => '?', + ); + + foreach ($headers as $header) { + $header = trim($header); + if (false === strpos($header, 'X-RateLimit-')) { + continue; + } + list($type, $value) = explode(':', $header, 2); + switch ($type) { + case 'X-RateLimit-Limit': + $rateLimit['limit'] = (int) trim($value); + break; + case 'X-RateLimit-Reset': + $rateLimit['reset'] = date('Y-m-d H:i:s', (int) trim($value)); + break; + } + } + + return $rateLimit; + } + + /** + * Finds whether a request failed due to rate limiting + * + * @param array $headers Headers from Composer\Downloader\TransportException. + * + * @return bool + */ + public function isRateLimited(array $headers) + { + foreach ($headers as $header) { + if (preg_match('{^X-RateLimit-Remaining: *0$}i', trim($header))) { + return true; + } + } + + return false; + } } diff --git a/src/Composer/Util/Hg.php b/src/Composer/Util/Hg.php index 48d0b3687f13..8cf6241a628a 100644 --- a/src/Composer/Util/Hg.php +++ b/src/Composer/Util/Hg.php @@ -60,7 +60,7 @@ public function runCommand($commandCallable, $url, $cwd) $command = call_user_func($commandCallable, $authenticatedUrl); - if (0 === $this->process->execute($command)) { + if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { return; } diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index ee18ad62d7b9..dc2b33089fc2 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -327,7 +327,7 @@ protected function get($originUrl, $fileUrl, $additionalOptions = array(), $file $warning = $data['warning']; } } - $this->promptAuthAndRetry($statusCode, $this->findStatusMessage($http_response_header), $warning); + $this->promptAuthAndRetry($statusCode, $this->findStatusMessage($http_response_header), $warning, $http_response_header); } } @@ -639,11 +639,35 @@ protected function callbackGet($notificationCode, $severity, $message, $messageC } } - protected function promptAuthAndRetry($httpStatus, $reason = null, $warning = null) + protected function promptAuthAndRetry($httpStatus, $reason = null, $warning = null, $headers = array()) { if ($this->config && in_array($this->originUrl, $this->config->get('github-domains'), true)) { - $message = "\n".'Could not fetch '.$this->fileUrl.', please create a GitHub OAuth token '.($httpStatus === 404 ? 'to access private repos' : 'to go over the API rate limit'); $gitHubUtil = new GitHub($this->io, $this->config, null); + $message = "\n"; + + $rateLimited = $gitHubUtil->isRateLimited($headers); + if ($rateLimited) { + $rateLimit = $gitHubUtil->getRateLimit($headers); + if ($this->io->hasAuthentication($this->originUrl)) { + $message = 'Review your configured GitHub OAuth token or enter a new one to go over the API rate limit.'; + } else { + $message = 'Create a GitHub OAuth token to go over the API rate limit.'; + } + + $message = sprintf( + 'GitHub API limit (%d calls/hr) is exhausted, could not fetch '.$this->fileUrl.'. '.$message.' You can also wait until %s for the rate limit to reset.', + $rateLimit['limit'], + $rateLimit['reset'] + )."\n"; + } else { + $message .= 'Could not fetch '.$this->fileUrl.', please '; + if ($this->io->hasAuthentication($this->originUrl)) { + $message .= 'review your configured GitHub OAuth token or enter a new one to access private repos'; + } else { + $message .= 'create a GitHub OAuth token to access private repos'; + } + } + if (!$gitHubUtil->authorizeOAuth($this->originUrl) && (!$this->io->isInteractive() || !$gitHubUtil->authorizeOAuthInteractively($this->originUrl, $message)) ) { diff --git a/tests/Composer/Test/Json/JsonFormatterTest.php b/tests/Composer/Test/Json/JsonFormatterTest.php index 7be8fafe2df0..417f267e310b 100644 --- a/tests/Composer/Test/Json/JsonFormatterTest.php +++ b/tests/Composer/Test/Json/JsonFormatterTest.php @@ -18,33 +18,31 @@ class JsonFormatterTest extends TestCase { /** - * Test if \u0119 (196+153) will get correctly formatted - * See ticket #2613 + * Test if \u0119 will get correctly formatted (unescaped) + * https://github.com/composer/composer/issues/2613 */ public function testUnicodeWithPrependedSlash() { if (!extension_loaded('mbstring')) { $this->markTestSkipped('Test requires the mbstring extension'); } - - $data = '"' . chr(92) . chr(92) . chr(92) . 'u0119"'; - $encodedData = JsonFormatter::format($data, true, true); - $expected = '34+92+92+196+153+34'; - $this->assertEquals($expected, $this->getCharacterCodes($encodedData)); + $backslash = chr(92); + $data = '"' . $backslash . $backslash . $backslash . 'u0119"'; + $expected = '"' . $backslash . $backslash . 'ę"'; + $this->assertEquals($expected, JsonFormatter::format($data, true, true)); } /** - * Convert string to character codes split by a plus sign - * @param string $string - * @return string + * Surrogate pairs are intentionally skipped and not unescaped + * https://github.com/composer/composer/issues/7510 */ - protected function getCharacterCodes($string) + public function testUtf16SurrogatePair() { - $codes = array(); - for ($i = 0; $i < strlen($string); $i++) { - $codes[] = ord($string[$i]); + if (!extension_loaded('mbstring')) { + $this->markTestSkipped('Test requires the mbstring extension'); } - return implode('+', $codes); + $escaped = '"\ud83d\ude00"'; + $this->assertEquals($escaped, JsonFormatter::format($escaped, true, true)); } }