diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -199,6 +199,8 @@ 'PhutilFileLockTestCase' => 'filesystem/__tests__/PhutilFileLockTestCase.php', 'PhutilFileTree' => 'filesystem/PhutilFileTree.php', 'PhutilGitHubAuthAdapter' => 'auth/PhutilGitHubAuthAdapter.php', + 'PhutilGitHubFuture' => 'future/github/PhutilGitHubFuture.php', + 'PhutilGitHubResponse' => 'future/github/PhutilGitHubResponse.php', 'PhutilGitURI' => 'parser/PhutilGitURI.php', 'PhutilGitURITestCase' => 'parser/__tests__/PhutilGitURITestCase.php', 'PhutilGoogleAuthAdapter' => 'auth/PhutilGoogleAuthAdapter.php', @@ -736,6 +738,8 @@ 'PhutilFileLockTestCase' => 'PhutilTestCase', 'PhutilFileTree' => 'Phobject', 'PhutilGitHubAuthAdapter' => 'PhutilOAuthAuthAdapter', + 'PhutilGitHubFuture' => 'FutureProxy', + 'PhutilGitHubResponse' => 'Phobject', 'PhutilGitURI' => 'Phobject', 'PhutilGitURITestCase' => 'PhutilTestCase', 'PhutilGoogleAuthAdapter' => 'PhutilOAuthAuthAdapter', diff --git a/src/future/github/PhutilGitHubFuture.php b/src/future/github/PhutilGitHubFuture.php new file mode 100644 --- /dev/null +++ b/src/future/github/PhutilGitHubFuture.php @@ -0,0 +1,130 @@ +accessToken = $token; + return $this; + } + + public function setRawGitHubQuery($action, array $params = array()) { + $this->action = $action; + $this->params = $params; + return $this; + } + + public function setMethod($method) { + $this->method = $method; + return $this; + } + + public function addHeader($key, $value) { + $this->headers[] = array($key, $value); + return $this; + } + + protected function getProxiedFuture() { + if (!$this->future) { + $params = $this->params; + + if (!$this->action) { + throw new Exception( + pht( + 'You must %s!', + 'setRawGitHubQuery()')); + } + + if (!$this->accessToken) { + throw new Exception( + pht( + 'You must %s!', + 'setAccessToken()')); + } + + $uri = new PhutilURI('https://api.github.com/'); + $uri->setPath('/'.ltrim($this->action, '/')); + + $future = new HTTPSFuture($uri); + $future->setData($this->params); + $future->addHeader('Authorization', 'token '.$this->accessToken); + // NOTE: GitHub requires a 'User-Agent' header. + $future->addHeader('User-Agent', __CLASS__); + $future->setMethod($this->method); + + foreach ($this->headers as $header) { + list($key, $value) = $header; + $future->addHeader($key, $value); + } + + $this->future = $future; + } + + return $this->future; + } + + protected function didReceiveResult($result) { + list($status, $body, $headers) = $result; + + if ($status->isError()) { + if ($this->isRateLimitResponse($status, $headers)) { + // Do nothing, this is a rate limit. + } else if ($this->isNotModifiedResponse($status)) { + // Do nothing, this is a "Not Modified" response. + } else { + // This is an error condition we do not expect. + throw $status; + } + } + + try { + if (strlen($body)) { + $data = phutil_json_decode($body); + } else { + // This can happen for 304 responses. + $data = array(); + } + } catch (PhutilJSONParserException $ex) { + throw new PhutilProxyException( + pht('Expected JSON response from GitHub.'), + $ex); + } + + return id(new PhutilGitHubResponse()) + ->setStatus($status) + ->setHeaders($headers) + ->setBody($data); + } + + private function isNotModifiedResponse($status) { + return ($status->getCode() == 304); + } + + private function isRateLimitResponse($status, array $headers) { + if ($status->getCode() != 403) { + return false; + } + + foreach ($headers as $header) { + list($key, $value) = $header; + if (phutil_utf8_strtolower($key) === 'x-ratelimit-remaining') { + if (!(int)$value) { + return true; + } + } + } + + return false; + } + +} diff --git a/src/future/github/PhutilGitHubResponse.php b/src/future/github/PhutilGitHubResponse.php new file mode 100644 --- /dev/null +++ b/src/future/github/PhutilGitHubResponse.php @@ -0,0 +1,49 @@ +status = $status; + return $this; + } + + public function getStatus() { + return $this->status; + } + + public function setBody(array $body) { + $this->body = $body; + return $this; + } + + public function getBody() { + return $this->body; + } + + public function setHeaders(array $headers) { + $this->headers = $headers; + return $this; + } + + public function getHeaders() { + return $this->headers; + } + + public function getHeaderValue($key, $default = null) { + $key = phutil_utf8_strtolower($key); + + foreach ($this->headers as $header) { + list($hkey, $value) = $header; + if (phutil_utf8_strtolower($hkey) === $key) { + return $value; + } + } + + return $default; + } + +}