diff --git a/src/auth/PhutilAmazonAuthAdapter.php b/src/auth/PhutilAmazonAuthAdapter.php index df1f988..94c529a 100644 --- a/src/auth/PhutilAmazonAuthAdapter.php +++ b/src/auth/PhutilAmazonAuthAdapter.php @@ -1,80 +1,80 @@ getOAuthAccountData('user_id'); } public function getAccountEmail() { return $this->getOAuthAccountData('email'); } public function getAccountName() { return null; } public function getAccountImageURI() { return null; } public function getAccountURI() { return null; } public function getAccountRealName() { return $this->getOAuthAccountData('name'); } protected function getAuthenticateBaseURI() { return 'https://www.amazon.com/ap/oa'; } protected function getTokenBaseURI() { return 'https://api.amazon.com/auth/o2/token'; } public function getScope() { return 'profile'; } public function getExtraAuthenticateParameters() { return array( 'response_type' => 'code', ); } public function getExtraTokenParameters() { return array( 'grant_type' => 'authorization_code', ); } protected function loadOAuthAccountData() { $uri = new PhutilURI('https://api.amazon.com/user/profile'); - $uri->setQueryParam('access_token', $this->getAccessToken()); + $uri->replaceQueryParam('access_token', $this->getAccessToken()); $future = new HTTPSFuture($uri); list($body) = $future->resolvex(); try { return phutil_json_decode($body); } catch (PhutilJSONParserException $ex) { throw new PhutilProxyException( pht('Expected valid JSON response from Amazon account data request.'), $ex); } } } diff --git a/src/auth/PhutilDisqusAuthAdapter.php b/src/auth/PhutilDisqusAuthAdapter.php index e3d0fd4..b9a33b2 100644 --- a/src/auth/PhutilDisqusAuthAdapter.php +++ b/src/auth/PhutilDisqusAuthAdapter.php @@ -1,84 +1,84 @@ getOAuthAccountData('id'); } public function getAccountEmail() { return $this->getOAuthAccountData('email'); } public function getAccountName() { return $this->getOAuthAccountData('username'); } public function getAccountImageURI() { return $this->getOAuthAccountData('avatar', 'permalink'); } public function getAccountURI() { return $this->getOAuthAccountData('profileUrl'); } public function getAccountRealName() { return $this->getOAuthAccountData('name'); } protected function getAuthenticateBaseURI() { return 'https://disqus.com/api/oauth/2.0/authorize/'; } protected function getTokenBaseURI() { return 'https://disqus.com/api/oauth/2.0/access_token/'; } public function getScope() { return 'read'; } public function getExtraAuthenticateParameters() { return array( 'response_type' => 'code', ); } public function getExtraTokenParameters() { return array( 'grant_type' => 'authorization_code', ); } protected function loadOAuthAccountData() { $uri = new PhutilURI('https://disqus.com/api/3.0/users/details.json'); - $uri->setQueryParam('api_key', $this->getClientID()); - $uri->setQueryParam('access_token', $this->getAccessToken()); + $uri->replaceQueryParam('api_key', $this->getClientID()); + $uri->replaceQueryParam('access_token', $this->getAccessToken()); $uri = (string)$uri; $future = new HTTPSFuture($uri); $future->setMethod('GET'); list($body) = $future->resolvex(); try { $data = phutil_json_decode($body); return $data['response']; } catch (PhutilJSONParserException $ex) { throw new PhutilProxyException( pht('Expected valid JSON response from Disqus account data request.'), $ex); } } } diff --git a/src/auth/PhutilFacebookAuthAdapter.php b/src/auth/PhutilFacebookAuthAdapter.php index c294eb7..ab569a1 100644 --- a/src/auth/PhutilFacebookAuthAdapter.php +++ b/src/auth/PhutilFacebookAuthAdapter.php @@ -1,114 +1,114 @@ requireSecureBrowsing = $require_secure_browsing; return $this; } public function getAdapterType() { return 'facebook'; } public function getAdapterDomain() { return 'facebook.com'; } public function getAccountID() { return $this->getOAuthAccountData('id'); } public function getAccountEmail() { return $this->getOAuthAccountData('email'); } public function getAccountName() { $link = $this->getOAuthAccountData('link'); if (!$link) { return null; } $matches = null; if (!preg_match('@/([^/]+)$@', $link, $matches)) { return null; } return $matches[1]; } public function getAccountImageURI() { $picture = $this->getOAuthAccountData('picture'); if ($picture) { $picture_data = idx($picture, 'data'); if ($picture_data) { return idx($picture_data, 'url'); } } return null; } public function getAccountURI() { return $this->getOAuthAccountData('link'); } public function getAccountRealName() { return $this->getOAuthAccountData('name'); } public function getAccountSecuritySettings() { return $this->getOAuthAccountData('security_settings'); } protected function getAuthenticateBaseURI() { return 'https://www.facebook.com/dialog/oauth'; } protected function getTokenBaseURI() { return 'https://graph.facebook.com/oauth/access_token'; } protected function loadOAuthAccountData() { $fields = array( 'id', 'name', 'email', 'link', 'security_settings', 'picture', ); $uri = new PhutilURI('https://graph.facebook.com/me'); - $uri->setQueryParam('access_token', $this->getAccessToken()); - $uri->setQueryParam('fields', implode(',', $fields)); + $uri->replaceQueryParam('access_token', $this->getAccessToken()); + $uri->replaceQueryParam('fields', implode(',', $fields)); list($body) = id(new HTTPSFuture($uri))->resolvex(); $data = null; try { $data = phutil_json_decode($body); } catch (PhutilJSONParserException $ex) { throw new PhutilProxyException( pht('Expected valid JSON response from Facebook account data request.'), $ex); } if ($this->requireSecureBrowsing) { if (empty($data['security_settings']['secure_browsing']['enabled'])) { throw new Exception( pht( 'This Phabricator install requires you to enable Secure Browsing '. 'on your Facebook account in order to use it to log in to '. 'Phabricator. For more information, see %s', 'https://www.facebook.com/help/156201551113407/')); } } return $data; } } diff --git a/src/auth/PhutilGitHubAuthAdapter.php b/src/auth/PhutilGitHubAuthAdapter.php index 7abac01..6bd5b63 100644 --- a/src/auth/PhutilGitHubAuthAdapter.php +++ b/src/auth/PhutilGitHubAuthAdapter.php @@ -1,72 +1,72 @@ getOAuthAccountData('id'); } public function getAccountEmail() { return $this->getOAuthAccountData('email'); } public function getAccountName() { return $this->getOAuthAccountData('login'); } public function getAccountImageURI() { return $this->getOAuthAccountData('avatar_url'); } public function getAccountURI() { $name = $this->getAccountName(); if (strlen($name)) { return 'https://github.com/'.$name; } return null; } public function getAccountRealName() { return $this->getOAuthAccountData('name'); } protected function getAuthenticateBaseURI() { return 'https://github.com/login/oauth/authorize'; } protected function getTokenBaseURI() { return 'https://github.com/login/oauth/access_token'; } protected function loadOAuthAccountData() { $uri = new PhutilURI('https://api.github.com/user'); - $uri->setQueryParam('access_token', $this->getAccessToken()); + $uri->replaceQueryParam('access_token', $this->getAccessToken()); $future = new HTTPSFuture($uri); // NOTE: GitHub requires a User-Agent string. $future->addHeader('User-Agent', __CLASS__); list($body) = $future->resolvex(); try{ return phutil_json_decode($body); } catch (PhutilJSONParserException $ex) { throw new PhutilProxyException( pht('Expected valid JSON response from GitHub account data request.'), $ex); } } } diff --git a/src/auth/PhutilGoogleAuthAdapter.php b/src/auth/PhutilGoogleAuthAdapter.php index 56e1f0f..11c1008 100644 --- a/src/auth/PhutilGoogleAuthAdapter.php +++ b/src/auth/PhutilGoogleAuthAdapter.php @@ -1,105 +1,105 @@ getAccountEmail(); } public function getAccountEmail() { return $this->getOAuthAccountData('email'); } public function getAccountName() { // Guess account name from email address, this is just a hint anyway. $email = $this->getAccountEmail(); $email = explode('@', $email); $email = head($email); return $email; } public function getAccountImageURI() { $uri = $this->getOAuthAccountData('picture'); // Change the "sz" parameter ("size") from the default to 100 to ask for // a 100x100px image. if ($uri !== null) { $uri = new PhutilURI($uri); - $uri->setQueryParam('sz', 100); + $uri->replaceQueryParam('sz', 100); $uri = (string)$uri; } return $uri; } public function getAccountURI() { return $this->getOAuthAccountData('link'); } public function getAccountRealName() { return $this->getOAuthAccountData('name'); } protected function getAuthenticateBaseURI() { return 'https://accounts.google.com/o/oauth2/auth'; } protected function getTokenBaseURI() { return 'https://accounts.google.com/o/oauth2/token'; } public function getScope() { $scopes = array( 'email', 'profile', ); return implode(' ', $scopes); } public function getExtraAuthenticateParameters() { return array( 'response_type' => 'code', ); } public function getExtraTokenParameters() { return array( 'grant_type' => 'authorization_code', ); } protected function loadOAuthAccountData() { $uri = new PhutilURI('https://www.googleapis.com/userinfo/v2/me'); - $uri->setQueryParam('access_token', $this->getAccessToken()); + $uri->replaceQueryParam('access_token', $this->getAccessToken()); $future = new HTTPSFuture($uri); list($status, $body) = $future->resolve(); if ($status->isError()) { throw $status; } try { $result = phutil_json_decode($body); } catch (PhutilJSONParserException $ex) { throw new PhutilProxyException( pht('Expected valid JSON response from Google account data request.'), $ex); } return $result; } } diff --git a/src/auth/PhutilJIRAAuthAdapter.php b/src/auth/PhutilJIRAAuthAdapter.php index 57d20b9..a045065 100644 --- a/src/auth/PhutilJIRAAuthAdapter.php +++ b/src/auth/PhutilJIRAAuthAdapter.php @@ -1,162 +1,164 @@ jiraBaseURI = $jira_base_uri; return $this; } public function getJIRABaseURI() { return $this->jiraBaseURI; } public function getAccountID() { // Make sure the handshake is finished; this method is used for its // side effect by Auth providers. $this->getHandshakeData(); return idx($this->getUserInfo(), 'key'); } public function getAccountName() { return idx($this->getUserInfo(), 'name'); } public function getAccountImageURI() { $avatars = idx($this->getUserInfo(), 'avatarUrls'); if ($avatars) { return idx($avatars, '48x48'); } return null; } public function getAccountRealName() { return idx($this->getUserInfo(), 'displayName'); } public function getAccountEmail() { return idx($this->getUserInfo(), 'emailAddress'); } public function getAdapterType() { return 'jira'; } public function getAdapterDomain() { return $this->adapterDomain; } public function setAdapterDomain($domain) { $this->adapterDomain = $domain; return $this; } protected function getSignatureMethod() { return 'RSA-SHA1'; } protected function getRequestTokenURI() { return $this->getJIRAURI('plugins/servlet/oauth/request-token'); } protected function getAuthorizeTokenURI() { return $this->getJIRAURI('plugins/servlet/oauth/authorize'); } protected function getValidateTokenURI() { return $this->getJIRAURI('plugins/servlet/oauth/access-token'); } private function getJIRAURI($path) { return rtrim($this->jiraBaseURI, '/').'/'.ltrim($path, '/'); } private function getUserInfo() { if ($this->userInfo === null) { $this->currentSession = $this->newJIRAFuture('rest/auth/1/session', 'GET') ->resolveJSON(); // The session call gives us the username, but not the user key or other // information. Make a second call to get additional information. $params = array( 'username' => $this->currentSession['name'], ); $this->userInfo = $this->newJIRAFuture('rest/api/2/user', 'GET', $params) ->resolveJSON(); } return $this->userInfo; } public static function newJIRAKeypair() { $config = array( 'digest_alg' => 'sha512', 'private_key_bits' => 4096, 'private_key_type' => OPENSSL_KEYTYPE_RSA, ); $res = openssl_pkey_new($config); if (!$res) { throw new Exception(pht('%s failed!', 'openssl_pkey_new()')); } $private_key = null; $ok = openssl_pkey_export($res, $private_key); if (!$ok) { throw new Exception(pht('%s failed!', 'openssl_pkey_export()')); } $public_key = openssl_pkey_get_details($res); if (!$ok || empty($public_key['key'])) { throw new Exception(pht('%s failed!', 'openssl_pkey_get_details()')); } $public_key = $public_key['key']; return array($public_key, $private_key); } /** * JIRA indicates that the user has clicked the "Deny" button by passing a * well known `oauth_verifier` value ("denied"), which we check for here. */ protected function willFinishOAuthHandshake() { $jira_magic_word = 'denied'; if ($this->getVerifier() == $jira_magic_word) { throw new PhutilAuthUserAbortedException(); } } public function newJIRAFuture($path, $method, $params = array()) { - $uri = new PhutilURI($this->getJIRAURI($path)); if ($method == 'GET') { - $uri->setQueryParams($params); - $params = array(); + $uri_params = $params; + $body_params = array(); } else { // For other types of requests, JIRA expects the request body to be // JSON encoded. - $params = json_encode($params); + $uri_params = array(); + $body_params = phutil_json_encode($params); } + $uri = new PhutilURI($this->getJIRAURI($path), $uri_params); + // JIRA returns a 415 error if we don't provide a Content-Type header. - return $this->newOAuth1Future($uri, $params) + return $this->newOAuth1Future($uri, $body_params) ->setMethod($method) ->addHeader('Content-Type', 'application/json'); } } diff --git a/src/auth/PhutilOAuth1AuthAdapter.php b/src/auth/PhutilOAuth1AuthAdapter.php index fe5e3b4..aad5f06 100644 --- a/src/auth/PhutilOAuth1AuthAdapter.php +++ b/src/auth/PhutilOAuth1AuthAdapter.php @@ -1,211 +1,211 @@ privateKey = $private_key; return $this; } public function getPrivateKey() { return $this->privateKey; } public function setCallbackURI($callback_uri) { $this->callbackURI = $callback_uri; return $this; } public function getCallbackURI() { return $this->callbackURI; } public function setVerifier($verifier) { $this->verifier = $verifier; return $this; } public function getVerifier() { return $this->verifier; } public function setConsumerSecret(PhutilOpaqueEnvelope $consumer_secret) { $this->consumerSecret = $consumer_secret; return $this; } public function getConsumerSecret() { return $this->consumerSecret; } public function setConsumerKey($consumer_key) { $this->consumerKey = $consumer_key; return $this; } public function getConsumerKey() { return $this->consumerKey; } public function setTokenSecret($token_secret) { $this->tokenSecret = $token_secret; return $this; } public function getTokenSecret() { return $this->tokenSecret; } public function setToken($token) { $this->token = $token; return $this; } public function getToken() { return $this->token; } protected function getHandshakeData() { if ($this->handshakeData === null) { $this->finishOAuthHandshake(); } return $this->handshakeData; } abstract protected function getRequestTokenURI(); abstract protected function getAuthorizeTokenURI(); abstract protected function getValidateTokenURI(); protected function getSignatureMethod() { return 'HMAC-SHA1'; } public function getContentSecurityPolicyFormActions() { return array( $this->getAuthorizeTokenURI(), ); } protected function newOAuth1Future($uri, $data = array()) { $future = id(new PhutilOAuth1Future($uri, $data)) ->setMethod('POST') ->setSignatureMethod($this->getSignatureMethod()); $consumer_key = $this->getConsumerKey(); if (strlen($consumer_key)) { $future->setConsumerKey($consumer_key); } else { throw new Exception( pht( '%s is required!', 'setConsumerKey()')); } $consumer_secret = $this->getConsumerSecret(); if ($consumer_secret) { $future->setConsumerSecret($consumer_secret); } if (strlen($this->getToken())) { $future->setToken($this->getToken()); } if (strlen($this->getTokenSecret())) { $future->setTokenSecret($this->getTokenSecret()); } if ($this->getPrivateKey()) { $future->setPrivateKey($this->getPrivateKey()); } return $future; } public function getClientRedirectURI() { $request_token_uri = $this->getRequestTokenURI(); $future = $this->newOAuth1Future($request_token_uri); if (strlen($this->getCallbackURI())) { $future->setCallbackURI($this->getCallbackURI()); } list($body) = $future->resolvex(); $data = id(new PhutilQueryStringParser())->parseQueryString($body); // NOTE: Per the spec, this value MUST be the string 'true'. $confirmed = idx($data, 'oauth_callback_confirmed'); if ($confirmed !== 'true') { throw new Exception( pht("Expected '%s' to be '%s'!", 'oauth_callback_confirmed', 'true')); } $this->readTokenAndTokenSecret($data); $authorize_token_uri = new PhutilURI($this->getAuthorizeTokenURI()); - $authorize_token_uri->setQueryParam('oauth_token', $this->getToken()); + $authorize_token_uri->replaceQueryParam('oauth_token', $this->getToken()); return (string)$authorize_token_uri; } protected function finishOAuthHandshake() { $this->willFinishOAuthHandshake(); if (!$this->getToken()) { throw new Exception(pht('Expected token to finish OAuth handshake!')); } if (!$this->getVerifier()) { throw new Exception(pht('Expected verifier to finish OAuth handshake!')); } $validate_uri = $this->getValidateTokenURI(); $params = array( 'oauth_verifier' => $this->getVerifier(), ); list($body) = $this->newOAuth1Future($validate_uri, $params)->resolvex(); $data = id(new PhutilQueryStringParser())->parseQueryString($body); $this->readTokenAndTokenSecret($data); $this->handshakeData = $data; } private function readTokenAndTokenSecret(array $data) { $token = idx($data, 'oauth_token'); if (!$token) { throw new Exception(pht("Expected '%s' in response!", 'oauth_token')); } $token_secret = idx($data, 'oauth_token_secret'); if (!$token_secret) { throw new Exception( pht("Expected '%s' in response!", 'oauth_token_secret')); } $this->setToken($token); $this->setTokenSecret($token_secret); return $this; } /** * Hook that allows subclasses to take actions before the OAuth handshake * is completed. */ protected function willFinishOAuthHandshake() { return; } } diff --git a/src/auth/PhutilOAuthAuthAdapter.php b/src/auth/PhutilOAuthAuthAdapter.php index 4289f3f..439f800 100644 --- a/src/auth/PhutilOAuthAuthAdapter.php +++ b/src/auth/PhutilOAuthAuthAdapter.php @@ -1,229 +1,229 @@ getAuthenticateBaseURI()); - $uri->setQueryParam('client_id', $this->getClientID()); - $uri->setQueryParam('scope', $this->getScope()); - $uri->setQueryParam('redirect_uri', $this->getRedirectURI()); - $uri->setQueryParam('state', $this->getState()); + $uri->replaceQueryParam('client_id', $this->getClientID()); + $uri->replaceQueryParam('scope', $this->getScope()); + $uri->replaceQueryParam('redirect_uri', $this->getRedirectURI()); + $uri->replaceQueryParam('state', $this->getState()); foreach ($this->getExtraAuthenticateParameters() as $key => $value) { - $uri->setQueryParam($key, $value); + $uri->replaceQueryParam($key, $value); } return (string)$uri; } public function getAdapterType() { $this_class = get_class($this); $type_name = str_replace('PhutilAuthAdapterOAuth', '', $this_class); return strtolower($type_name); } public function setState($state) { $this->state = $state; return $this; } public function getState() { return $this->state; } public function setCode($code) { $this->code = $code; return $this; } public function getCode() { return $this->code; } public function setRedirectURI($redirect_uri) { $this->redirectURI = $redirect_uri; return $this; } public function getRedirectURI() { return $this->redirectURI; } public function getExtraAuthenticateParameters() { return array(); } public function getExtraTokenParameters() { return array(); } public function getExtraRefreshParameters() { return array(); } public function setScope($scope) { $this->scope = $scope; return $this; } public function getScope() { return $this->scope; } public function setClientSecret(PhutilOpaqueEnvelope $client_secret) { $this->clientSecret = $client_secret; return $this; } public function getClientSecret() { return $this->clientSecret; } public function setClientID($client_id) { $this->clientID = $client_id; return $this; } public function getClientID() { return $this->clientID; } public function getAccessToken() { return $this->getAccessTokenData('access_token'); } public function getAccessTokenExpires() { return $this->getAccessTokenData('expires_epoch'); } public function getRefreshToken() { return $this->getAccessTokenData('refresh_token'); } protected function getAccessTokenData($key, $default = null) { if ($this->accessTokenData === null) { $this->accessTokenData = $this->loadAccessTokenData(); } return idx($this->accessTokenData, $key, $default); } public function supportsTokenRefresh() { return false; } public function refreshAccessToken($refresh_token) { $this->accessTokenData = $this->loadRefreshTokenData($refresh_token); return $this; } protected function loadRefreshTokenData($refresh_token) { $params = array( 'refresh_token' => $refresh_token, ) + $this->getExtraRefreshParameters(); // NOTE: Make sure we return the refresh_token so that subsequent // calls to getRefreshToken() return it; providers normally do not echo // it back for token refresh requests. return $this->makeTokenRequest($params) + array( 'refresh_token' => $refresh_token, ); } protected function loadAccessTokenData() { $code = $this->getCode(); if (!$code) { throw new PhutilInvalidStateException('setCode'); } $params = array( 'code' => $this->getCode(), ) + $this->getExtraTokenParameters(); return $this->makeTokenRequest($params); } private function makeTokenRequest(array $params) { $uri = $this->getTokenBaseURI(); $query_data = array( 'client_id' => $this->getClientID(), 'client_secret' => $this->getClientSecret()->openEnvelope(), 'redirect_uri' => $this->getRedirectURI(), ) + $params; $future = new HTTPSFuture($uri, $query_data); $future->setMethod('POST'); list($body) = $future->resolvex(); $data = $this->readAccessTokenResponse($body); if (isset($data['expires_in'])) { $data['expires_epoch'] = $data['expires_in']; } else if (isset($data['expires'])) { $data['expires_epoch'] = $data['expires']; } // If we got some "expires" value back, interpret it as an epoch timestamp // if it's after the year 2010 and as a relative number of seconds // otherwise. if (isset($data['expires_epoch'])) { if ($data['expires_epoch'] < (60 * 60 * 24 * 365 * 40)) { $data['expires_epoch'] += time(); } } if (isset($data['error'])) { throw new Exception(pht('Access token error: %s', $data['error'])); } return $data; } protected function readAccessTokenResponse($body) { // NOTE: Most providers either return JSON or HTTP query strings, so try // both mechanisms. If your provider does something else, override this // method. $data = json_decode($body, true); if (!is_array($data)) { $data = array(); parse_str($body, $data); } if (empty($data['access_token']) && empty($data['error'])) { throw new Exception( pht('Failed to decode OAuth access token response: %s', $body)); } return $data; } protected function getOAuthAccountData($key, $default = null) { if ($this->oauthAccountData === null) { $this->oauthAccountData = $this->loadOAuthAccountData(); } return idx($this->oauthAccountData, $key, $default); } } diff --git a/src/auth/PhutilPhabricatorAuthAdapter.php b/src/auth/PhutilPhabricatorAuthAdapter.php index 205d20b..e66ba32 100644 --- a/src/auth/PhutilPhabricatorAuthAdapter.php +++ b/src/auth/PhutilPhabricatorAuthAdapter.php @@ -1,102 +1,102 @@ phabricatorBaseURI = $uri; return $this; } public function getPhabricatorBaseURI() { return $this->phabricatorBaseURI; } public function getAdapterDomain() { return $this->adapterDomain; } public function setAdapterDomain($domain) { $this->adapterDomain = $domain; return $this; } public function getAdapterType() { return 'phabricator'; } public function getAccountID() { return $this->getOAuthAccountData('phid'); } public function getAccountEmail() { return $this->getOAuthAccountData('primaryEmail'); } public function getAccountName() { return $this->getOAuthAccountData('userName'); } public function getAccountImageURI() { return $this->getOAuthAccountData('image'); } public function getAccountURI() { return $this->getOAuthAccountData('uri'); } public function getAccountRealName() { return $this->getOAuthAccountData('realName'); } protected function getAuthenticateBaseURI() { return $this->getPhabricatorURI('oauthserver/auth/'); } protected function getTokenBaseURI() { return $this->getPhabricatorURI('oauthserver/token/'); } public function getScope() { return ''; } public function getExtraAuthenticateParameters() { return array( 'response_type' => 'code', ); } public function getExtraTokenParameters() { return array( 'grant_type' => 'authorization_code', ); } protected function loadOAuthAccountData() { $uri = id(new PhutilURI($this->getPhabricatorURI('api/user.whoami'))) - ->setQueryParam('access_token', $this->getAccessToken()); + ->replaceQueryParam('access_token', $this->getAccessToken()); list($body) = id(new HTTPSFuture($uri))->resolvex(); try { $data = phutil_json_decode($body); return $data['result']; } catch (PhutilJSONParserException $ex) { throw new Exception( pht( 'Expected valid JSON response from Phabricator %s request.', 'user.whoami'), $ex); } } private function getPhabricatorURI($path) { return rtrim($this->phabricatorBaseURI, '/').'/'.ltrim($path, '/'); } } diff --git a/src/auth/PhutilTwitterAuthAdapter.php b/src/auth/PhutilTwitterAuthAdapter.php index 9189ff4..6f738c7 100644 --- a/src/auth/PhutilTwitterAuthAdapter.php +++ b/src/auth/PhutilTwitterAuthAdapter.php @@ -1,73 +1,75 @@ getHandshakeData(), 'user_id'); } public function getAccountName() { return idx($this->getHandshakeData(), 'screen_name'); } public function getAccountURI() { $name = $this->getAccountName(); if (strlen($name)) { return 'https://twitter.com/'.$name; } return null; } public function getAccountImageURI() { $info = $this->getUserInfo(); return idx($info, 'profile_image_url'); } public function getAccountRealName() { $info = $this->getUserInfo(); return idx($info, 'name'); } public function getAdapterType() { return 'twitter'; } public function getAdapterDomain() { return 'twitter.com'; } protected function getRequestTokenURI() { return 'https://api.twitter.com/oauth/request_token'; } protected function getAuthorizeTokenURI() { return 'https://api.twitter.com/oauth/authorize'; } protected function getValidateTokenURI() { return 'https://api.twitter.com/oauth/access_token'; } private function getUserInfo() { if ($this->userInfo === null) { - $uri = new PhutilURI('https://api.twitter.com/1.1/users/show.json'); - $uri->setQueryParams( - array( - 'user_id' => $this->getAccountID(), - )); + $params = array( + 'user_id' => $this->getAccountID(), + ); + + $uri = new PhutilURI( + 'https://api.twitter.com/1.1/users/show.json', + $params); $data = $this->newOAuth1Future($uri) ->setMethod('GET') ->resolveJSON(); $this->userInfo = $data; } return $this->userInfo; } } diff --git a/src/future/aws/PhutilAWSFuture.php b/src/future/aws/PhutilAWSFuture.php index 176a8e7..16fd363 100644 --- a/src/future/aws/PhutilAWSFuture.php +++ b/src/future/aws/PhutilAWSFuture.php @@ -1,171 +1,170 @@ accessKey = $access_key; return $this; } public function getAccessKey() { return $this->accessKey; } public function setSecretKey(PhutilOpaqueEnvelope $secret_key) { $this->secretKey = $secret_key; return $this; } public function getSecretKey() { return $this->secretKey; } public function getRegion() { return $this->region; } public function setRegion($region) { $this->region = $region; return $this; } public function setEndpoint($endpoint) { $this->endpoint = $endpoint; return $this; } public function getEndpoint() { return $this->endpoint; } public function setHTTPMethod($method) { $this->httpMethod = $method; return $this; } public function getHTTPMethod() { return $this->httpMethod; } public function setPath($path) { $this->path = $path; return $this; } public function getPath() { return $this->path; } public function setData($data) { $this->data = $data; return $this; } public function getData() { return $this->data; } protected function getParameters() { return array(); } public function addHeader($key, $value) { $this->headers[] = array($key, $value); return $this; } protected function getProxiedFuture() { if (!$this->future) { $params = $this->getParameters(); $method = $this->getHTTPMethod(); $host = $this->getEndpoint(); $path = $this->getPath(); $data = $this->getData(); - $uri = id(new PhutilURI("https://{$host}/")) - ->setPath($path) - ->setQueryParams($params); + $uri = id(new PhutilURI("https://{$host}/", $params)) + ->setPath($path); $future = id(new HTTPSFuture($uri, $data)) ->setMethod($method); foreach ($this->headers as $header) { list($key, $value) = $header; $future->addHeader($key, $value); } $this->signRequest($future); $this->future = $future; } return $this->future; } protected function signRequest(HTTPSFuture $future) { $access_key = $this->getAccessKey(); $secret_key = $this->getSecretKey(); $region = $this->getRegion(); id(new PhutilAWSv4Signature()) ->setRegion($region) ->setService($this->getServiceName()) ->setAccessKey($access_key) ->setSecretKey($secret_key) ->setSignContent($this->shouldSignContent()) ->signRequest($future); } protected function shouldSignContent() { return false; } protected function didReceiveResult($result) { list($status, $body, $headers) = $result; try { $xml = @(new SimpleXMLElement($body)); } catch (Exception $ex) { $xml = null; } if ($status->isError() || !$xml) { if (!($status instanceof HTTPFutureHTTPResponseStatus)) { throw $status; } $params = array( 'body' => $body, ); if ($xml) { $params['RequestID'] = $xml->RequestID[0]; $errors = array($xml->Error); foreach ($errors as $error) { $params['Errors'][] = array($error->Code, $error->Message); } } throw new PhutilAWSException($status->getStatusCode(), $params); } return $xml; } } diff --git a/src/future/oauth/PhutilOAuth1Future.php b/src/future/oauth/PhutilOAuth1Future.php index aed9fd7..1e63ae2 100644 --- a/src/future/oauth/PhutilOAuth1Future.php +++ b/src/future/oauth/PhutilOAuth1Future.php @@ -1,291 +1,291 @@ callbackURI = $callback_uri; return $this; } public function setTimestamp($timestamp) { $this->timestamp = $timestamp; return $this; } public function setNonce($nonce) { $this->nonce = $nonce; return $this; } public function setTokenSecret($token_secret) { $this->tokenSecret = $token_secret; return $this; } public function setToken($token) { $this->token = $token; return $this; } public function setPrivateKey(PhutilOpaqueEnvelope $private_key) { $this->privateKey = $private_key; return $this; } public function setSignatureMethod($signature_method) { $this->signatureMethod = $signature_method; return $this; } public function setConsumerKey($consumer_key) { $this->consumerKey = $consumer_key; return $this; } public function setConsumerSecret(PhutilOpaqueEnvelope $consumer_secret) { $this->consumerSecret = $consumer_secret; return $this; } public function setMethod($method) { $this->method = $method; return $this; } public function __construct($uri, $data = array()) { $this->uri = new PhutilURI((string)$uri); $this->data = $data; $this->setProxiedFuture(new HTTPSFuture($uri, $data)); } public function getSignature() { $params = array(); // NOTE: The JIRA API uses JSON-encoded request bodies which are not // signed, and OAuth1 provides no real way to sign a nonparameterized // request body. Possibly we should split this apart into flags which // control which data is signed, but for now this rule seems to cover // all the use cases. if (is_array($this->data)) { $params = $this->data; } $params = $params + $this->uri->getQueryParamsAsMap() + $this->getOAuth1Headers(); return $this->sign($params); } public function addHeader($name, $value) { // If we haven't built the future yet, hold on to the header until after // we do, since there might be more changes coming which will affect the // signature process. if (!$this->hasConstructedFuture) { $this->headers[] = array($name, $value); } else { $this->getProxiedFuture()->addHeader($name, $value); } return $this; } protected function getProxiedFuture() { $future = parent::getProxiedFuture(); if (!$this->hasConstructedFuture) { $future->setMethod($this->method); $oauth_headers = $this->getOAuth1Headers(); $oauth_headers['oauth_signature'] = $this->getSignature(); $full_oauth_header = array(); foreach ($oauth_headers as $header => $value) { $full_oauth_header[] = $header.'="'.urlencode($value).'"'; } $full_oauth_header = 'OAuth '.implode(', ', $full_oauth_header); $future->addHeader('Authorization', $full_oauth_header); foreach ($this->headers as $header) { $future->addHeader($header[0], $header[1]); } $this->headers = array(); $this->hasConstructedFuture = true; } return $future; } protected function didReceiveResult($result) { return $result; } private function getOAuth1Headers() { if (!$this->nonce) { $this->nonce = Filesystem::readRandomCharacters(32); } if (!$this->timestamp) { $this->timestamp = time(); } $oauth_headers = array( 'oauth_consumer_key' => $this->consumerKey, 'oauth_signature_method' => $this->signatureMethod, 'oauth_timestamp' => $this->timestamp, 'oauth_nonce' => $this->nonce, 'oauth_version' => '1.0', ); if ($this->callbackURI) { $oauth_headers['oauth_callback'] = (string)$this->callbackURI; } if ($this->token) { $oauth_headers['oauth_token'] = $this->token; } return $oauth_headers; } private function sign(array $params) { ksort($params); $pstr = array(); foreach ($params as $key => $value) { $pstr[] = rawurlencode($key).'='.rawurlencode($value); } $pstr = implode('&', $pstr); $sign_uri = clone $this->uri; $sign_uri->setFragment(''); - $sign_uri->setQueryParams(array()); + $sign_uri->removeAllQueryParams(); $sign_uri->setProtocol(phutil_utf8_strtolower($sign_uri->getProtocol())); $protocol = $sign_uri->getProtocol(); switch ($protocol) { case 'http': if ($sign_uri->getPort() == 80) { $sign_uri->setPort(null); } break; case 'https': if ($sign_uri->getPort() == 443) { $sign_uri->setPort(null); } break; } $method = rawurlencode(phutil_utf8_strtoupper($this->method)); $sign_uri = rawurlencode((string)$sign_uri); $pstr = rawurlencode($pstr); $sign_input = "{$method}&{$sign_uri}&{$pstr}"; return $this->signString($sign_input); } private function signString($string) { $consumer_secret = null; if ($this->consumerSecret) { $consumer_secret = $this->consumerSecret->openEnvelope(); } $key = urlencode($consumer_secret).'&'.urlencode($this->tokenSecret); switch ($this->signatureMethod) { case 'HMAC-SHA1': if (!$this->consumerSecret) { throw new Exception( pht( "Signature method '%s' requires %s!", 'HMAC-SHA1', 'setConsumerSecret()')); } $hash = hash_hmac('sha1', $string, $key, true); return base64_encode($hash); case 'RSA-SHA1': if (!$this->privateKey) { throw new Exception( pht( "Signature method '%s' requires %s!", 'RSA-SHA1', 'setPrivateKey()')); } $cert = @openssl_pkey_get_private($this->privateKey->openEnvelope()); if (!$cert) { throw new Exception(pht('%s failed!', 'openssl_pkey_get_private()')); } $pkey = @openssl_get_privatekey($cert); if (!$pkey) { throw new Exception(pht('%s failed!', 'openssl_get_privatekey()')); } $signature = null; $ok = openssl_sign($string, $signature, $pkey, OPENSSL_ALGO_SHA1); if (!$ok) { throw new Exception(pht('%s failed!', 'openssl_sign()')); } openssl_free_key($pkey); return base64_encode($signature); case 'PLAINTEXT': if (!$this->consumerSecret) { throw new Exception( pht( "Signature method '%s' requires %s!", 'PLAINTEXT', 'setConsumerSecret()')); } return $key; default: throw new Exception(pht("Unknown signature method '%s'!", $string)); } } public function resolvex() { $result = $this->getProxiedFuture()->resolvex(); return $this->didReceiveResult($result); } public function resolveJSON() { $result = $this->getProxiedFuture()->resolvex(); $result = $this->didReceiveResult($result); list($body) = $result; try { return phutil_json_decode($body); } catch (PhutilJSONParserException $ex) { throw new PhutilProxyException(pht('Expected JSON.'), $ex); } } } diff --git a/src/future/slack/PhutilSlackFuture.php b/src/future/slack/PhutilSlackFuture.php index 0f23290..7ae8b0b 100644 --- a/src/future/slack/PhutilSlackFuture.php +++ b/src/future/slack/PhutilSlackFuture.php @@ -1,87 +1,87 @@ accessToken = $token; return $this; } public function setClientID($client_id) { $this->clientID = $client_id; return $this; } public function setRawSlackQuery($action, array $params = array()) { $this->action = $action; $this->params = $params; return $this; } public function setMethod($method) { $this->method = $method; return $this; } protected function getProxiedFuture() { if (!$this->future) { $params = $this->params; if (!$this->action) { throw new Exception(pht('You must %s!', 'setRawSlackQuery()')); } if (!$this->accessToken) { throw new Exception(pht('You must %s!', 'setAccessToken()')); } $uri = new PhutilURI('https://slack.com/'); $uri->setPath('/api/'.$this->action); - $uri->setQueryParam('token', $this->accessToken); + $uri->replaceQueryParam('token', $this->accessToken); $future = new HTTPSFuture($uri); $future->setData($this->params); $future->setMethod($this->method); $this->future = $future; } return $this->future; } protected function didReceiveResult($result) { list($status, $body, $headers) = $result; if ($status->isError()) { throw $status; } $data = null; try { $data = phutil_json_decode($body); } catch (PhutilJSONParserException $ex) { throw new PhutilProxyException( pht('Expected JSON response from Slack.'), $ex); } if (idx($data, 'error')) { $error = $data['error']; throw new Exception(pht('Received error from Slack: %s', $error)); } return $data; } } diff --git a/src/future/twitch/PhutilTwitchFuture.php b/src/future/twitch/PhutilTwitchFuture.php index 4c40e09..9dc06c3 100644 --- a/src/future/twitch/PhutilTwitchFuture.php +++ b/src/future/twitch/PhutilTwitchFuture.php @@ -1,93 +1,93 @@ accessToken = $token; return $this; } public function setClientID($client_id) { $this->clientID = $client_id; return $this; } public function setRawTwitchQuery($action, array $params = array()) { $this->action = $action; $this->params = $params; return $this; } public function setMethod($method) { $this->method = $method; return $this; } protected function getProxiedFuture() { if (!$this->future) { $params = $this->params; if (!$this->action) { throw new Exception(pht('You must %s!', 'setRawTwitchQuery()')); } if (!$this->accessToken) { throw new Exception(pht('You must %s!', 'setAccessToken()')); } $uri = new PhutilURI('https://api.twitch.tv/'); $uri->setPath('/kraken/'.ltrim($this->action, '/')); - $uri->setQueryParam('oauth_token', $this->accessToken); + $uri->replaceQueryParam('oauth_token', $this->accessToken); $future = new HTTPSFuture($uri); $future->setData($this->params); $future->setMethod($this->method); // NOTE: This is how the Twitch API is versioned. $future->addHeader('Accept', 'application/vnd.twitchtv.2+json'); // NOTE: This is required to avoid rate limiting. $future->addHeader('Client-ID', $this->clientID); $this->future = $future; } return $this->future; } protected function didReceiveResult($result) { list($status, $body, $headers) = $result; if ($status->isError()) { throw $status; } $data = null; try { $data = phutil_json_decode($body); } catch (PhutilJSONParserException $ex) { throw new PhutilProxyException( pht('Expected JSON response from Twitch.'), $ex); } if (idx($data, 'error')) { $error = $data['error']; throw new Exception(pht('Received error from Twitch: %s', $error)); } return $data; } }