diff --git a/composer.lock b/composer.lock index 873419108..768b822b5 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", diff --git a/src/Composer/Json/JsonFormatter.php b/src/Composer/Json/JsonFormatter.php index 680a57baf..44acaff59 100644 --- a/src/Composer/Json/JsonFormatter.php +++ b/src/Composer/Json/JsonFormatter.php @@ -69,6 +69,13 @@ class JsonFormatter $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 5c5c08cf2..e150ccd10 100644 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -378,12 +378,7 @@ class GitHubDriver extends VcsDriver 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 @@ class GitHubDriver extends VcsDriver } 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 @@ class GitHubDriver extends VcsDriver } } - /** - * 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 8415c9a5c..2f5dbe5cd 100644 --- a/src/Composer/Util/GitHub.php +++ b/src/Composer/Util/GitHub.php @@ -126,4 +126,55 @@ class GitHub 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/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index ee18ad62d..dc2b33089 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -327,7 +327,7 @@ class RemoteFilesystem $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 @@ class RemoteFilesystem } } - 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 7be8fafe2..417f267e3 100644 --- a/tests/Composer/Test/Json/JsonFormatterTest.php +++ b/tests/Composer/Test/Json/JsonFormatterTest.php @@ -18,33 +18,31 @@ use PHPUnit\Framework\TestCase; 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)); } }