// @codeCoverageIgnoreEnd } /** * Appends a query string to a URL. * * @param string $url The URL to append the query to * @param string $query The HTTP query string * @return string The resulting URL */ protected function appendQuery($url, $query) { $query = \trim($query, '?&'); if ($query) { $glue = \strstr($url, '?') === \false ? '?' : '&'; return $url . $glue . $query; } return $url; } /** * Returns the method to use when requesting an access token. * * @return string HTTP method */ protected function getAccessTokenMethod() { return self::METHOD_POST; } /** * Returns the key used in the access token response to identify the resource owner. * * @return string|null Resource owner identifier key */ protected function getAccessTokenResourceOwnerId() { return static::ACCESS_TOKEN_RESOURCE_OWNER_ID; } /** * Builds the access token URL's query string. * * @param array $params Query parameters * @return string Query string */ protected function getAccessTokenQuery(array $params) { return $this->buildQueryString($params); } /** * Checks that a provided grant is valid, or attempts to produce one if the * provided grant is a string. * * @param AbstractGrant|string $grant * @return AbstractGrant */ protected function verifyGrant($grant) { if (\is_string($grant)) { return $this->grantFactory->getGrant($grant); } $this->grantFactory->checkGrant($grant); return $grant; } /** * Returns the full URL to use when requesting an access token. * * @param array $params Query parameters * @return string */ protected function getAccessTokenUrl(array $params) { $url = $this->getBaseAccessTokenUrl($params); if ($this->getAccessTokenMethod() === self::METHOD_GET) { $query = $this->getAccessTokenQuery($params); return $this->appendQuery($url, $query); } return $url; } /** * Returns a prepared request for requesting an access token. * * @param array $params Query string parameters * @return RequestInterface */ protected function getAccessTokenRequest(array $params) { $method = $this->getAccessTokenMethod(); $url = $this->getAccessTokenUrl($params); $options = $this->optionProvider->getAccessTokenOptions($this->getAccessTokenMethod(), $params); return $this->getRequest($method, $url, $options); } /** * Requests an access token using a specified grant and option set. * * @param mixed $grant * @param array $options * @throws IdentityProviderException * @return AccessTokenInterface */ public function getAccessToken($grant, array $options = []) { $grant = $this->verifyGrant($grant); $params = ['client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'redirect_uri' => $this->redirectUri]; $params = $grant->prepareRequestParameters($params, $options); $request = $this->getAccessTokenRequest($params); $response = $this->getParsedResponse($request); if (\false === \is_array($response)) { throw new \UnexpectedValueException('Invalid response received from Authorization Server. Expected JSON.'); } $prepared = $this->prepareAccessTokenResponse($response); $token = $this->createAccessToken($prepared, $grant); return $token; } /** * Returns a PSR-7 request instance that is not authenticated. * * @param string $method * @param string $url * @param array $options * @return RequestInterface */ public function getRequest($method, $url, array $options = []) { return $this->createRequest($method, $url, null, $options); } /** * Returns an authenticated PSR-7 request instance. * * @param string $method * @param string $url * @param AccessTokenInterface|string $token * @param array $options Any of "headers", "body", and "protocolVersion". * @return RequestInterface */ public function getAuthenticatedRequest($method, $url, $token, array $options = []) { return $this->createRequest($method, $url, $token, $options); } /** * Creates a PSR-7 request instance. * * @param string $method * @param string $url * @param AccessTokenInterface|string|null $token * @param array $options * @return RequestInterface */ protected function createRequest($method, $url, $token, array $options) { $defaults = ['headers' => $this->getHeaders($token)]; $options = \array_merge_recursive($defaults, $options); $factory = $this->getRequestFactory(); return $factory->getRequestWithOptions($method, $url, $options); } /** * Sends a request instance and returns a response instance. * * WARNING: This method does not attempt to catch exceptions caused by HTTP * errors! It is recommended to wrap this method in a try/catch block. * * @param RequestInterface $request * @return ResponseInterface */ public function getResponse(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request) { return $this->getHttpClient()->send($request); } /** * Sends a request and returns the parsed response. * * @param RequestInterface $request * @throws IdentityProviderException * @return mixed */ public function getParsedResponse(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request) { try { $response = $this->getResponse($request); } catch (\YoastSEO_Vendor\GuzzleHttp\Exception\BadResponseException $e) { $response = $e->getResponse(); } $parsed = $this->parseResponse($response); $this->checkResponse($response, $parsed); return $parsed; } /** * Attempts to parse a JSON response. * * @param string $content JSON content from response body * @return array Parsed JSON data * @throws UnexpectedValueException if the content could not be parsed */ protected function parseJson($content) { $content = \json_decode($content, \true); if (\json_last_error() !== \JSON_ERROR_NONE) { throw new \UnexpectedValueException(\sprintf("Failed to parse JSON response: %s", \json_last_error_msg())); } return $content; } /** * Returns the content type header of a response. * * @param ResponseInterface $response * @return string Semi-colon separated join of content-type headers. */ protected function getContentType(\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response) { return \join(';', (array) $response->getHeader('content-type')); } /** * Parses the response according to its content-type header. * * @throws UnexpectedValueException * @param ResponseInterface $response * @return array */ protected function parseResponse(\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response) { $content = (string) $response->getBody(); $type = $this->getContentType($response); if (\strpos($type, 'urlencoded') !== \false) { \parse_str($content, $parsed); return $parsed; } // Attempt to parse the string as JSON regardless of content type, // since some providers use non-standard content types. Only throw an // exception if the JSON could not be parsed when it was expected to. try { return $this->parseJson($content); } catch (\UnexpectedValueException $e) { if (\strpos($type, 'json') !== \false) { throw $e; } if ($response->getStatusCode() == 500) { throw new \UnexpectedValueException('An OAuth server error was encountered that did not contain a JSON body', 0, $e); } return $content; } } /** * Checks a provider response for errors. * * @throws IdentityProviderException * @param ResponseInterface $response * @param array|string $data Parsed response data * @return void */ protected abstract function checkResponse(\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response, $data); /** * Prepares an parsed access token response for a grant. * * Custom mapping of expiration, etc should be done here. Always call the * parent method when overloading this method. * * @param mixed $result * @return array */ protected function prepareAccessTokenResponse(array $result) { if ($this->getAccessTokenResourceOwnerId() !== null) { $result['resource_owner_id'] = $this->getValueByKey($result, $this->getAccessTokenResourceOwnerId()); } return $result; } /** * Creates an access token from a response. * * The grant that was used to fetch the response can be used to provide * additional context. * * @param array $response * @param AbstractGrant $grant * @return AccessTokenInterface */ protected function createAccessToken(array $response, \YoastSEO_Vendor\League\OAuth2\Client\Grant\AbstractGrant $grant) { return new \YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken($response); } /** * Generates a resource owner object from a successful resource owner * details request. * * @param array $response * @param AccessToken $token * @return ResourceOwnerInterface */ protected abstract function createResourceOwner(array $response, \YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken $token); /** * Requests and returns the resource owner of given access token. * * @param AccessToken $token * @return ResourceOwnerInterface */ public function getResourceOwner(\YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken $token) { $response = $this->fetchResourceOwnerDetails($token); return $this->createResourceOwner($response, $token); } /** * Requests resource owner details. * * @param AccessToken $token * @return mixed */ protected function fetchResourceOwnerDetails(\YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken $token) { $url = $this->getResourceOwnerDetailsUrl($token); $request = $this->getAuthenticatedRequest(self::METHOD_GET, $url, $token); $response = $this->getParsedResponse($request); if (\false === \is_array($response)) { throw new \UnexpectedValueException('Invalid response received from Authorization Server. Expected JSON.'); } return $response; } /** * Returns the default headers used by this provider. * * Typically this is used to set 'Accept' or 'Content-Type' headers. * * @return array */ protected function getDefaultHeaders() { return []; } /** * Returns the authorization headers used by this provider. * * Typically this is "Bearer" or "MAC". For more information see: * * * No default is provided, providers must overload this method to activate * authorization headers. * * @param mixed|null $token Either a string or an access token instance * @return array */ protected function getAuthorizationHeaders($token = null) { return []; } /** * Returns all headers used by this provider for a request. * * The request will be authenticated if an access token is provided. * * @param mixed|null $token object or string * @return array */ public function getHeaders($token = null) { if ($token) { return \array_merge($this->getDefaultHeaders(), $this->getAuthorizationHeaders($token)); } return $this->getDefaultHeaders(); } }