PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 19.0
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v19.0
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / vendor_prefixed / league / oauth2-client / src / Provider / AbstractProvider.php

AbstractProvider.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 19.0, at vendor_prefixed/league/oauth2-client/src/Provider/AbstractProvider.php

710 lines 23.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * This file is part of the league/oauth2-client library
5 *
6 * For the full copyright and license information, please view the LICENSE
7 * file that was distributed with this source code.
8 *
9 * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com>
10 * @license http://opensource.org/licenses/MIT MIT
11 * @link http://thephpleague.com/oauth2-client/ Documentation
12 * @link https://packagist.org/packages/league/oauth2-client Packagist
13 * @link https://github.com/thephpleague/oauth2-client GitHub
14 */
15 namespace YoastSEO_Vendor\League\OAuth2\Client\Provider;
16
17 use YoastSEO_Vendor\GuzzleHttp\Client as HttpClient;
18 use YoastSEO_Vendor\GuzzleHttp\ClientInterface as HttpClientInterface;
19 use YoastSEO_Vendor\GuzzleHttp\Exception\BadResponseException;
20 use YoastSEO_Vendor\League\OAuth2\Client\Grant\AbstractGrant;
21 use YoastSEO_Vendor\League\OAuth2\Client\Grant\GrantFactory;
22 use YoastSEO_Vendor\League\OAuth2\Client\OptionProvider\OptionProviderInterface;
23 use YoastSEO_Vendor\League\OAuth2\Client\OptionProvider\PostAuthOptionProvider;
24 use YoastSEO_Vendor\League\OAuth2\Client\Provider\Exception\IdentityProviderException;
25 use YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken;
26 use YoastSEO_Vendor\League\OAuth2\Client\Token\AccessTokenInterface;
27 use YoastSEO_Vendor\League\OAuth2\Client\Tool\ArrayAccessorTrait;
28 use YoastSEO_Vendor\League\OAuth2\Client\Tool\GuardedPropertyTrait;
29 use YoastSEO_Vendor\League\OAuth2\Client\Tool\QueryBuilderTrait;
30 use YoastSEO_Vendor\League\OAuth2\Client\Tool\RequestFactory;
31 use YoastSEO_Vendor\Psr\Http\Message\RequestInterface;
32 use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface;
33 use UnexpectedValueException;
34 /**
35 * Represents a service provider (authorization server).
36 *
37 * @link http://tools.ietf.org/html/rfc6749#section-1.1 Roles (RFC 6749, §1.1)
38 */
39 abstract class AbstractProvider
40 {
41 use ArrayAccessorTrait;
42 use GuardedPropertyTrait;
43 use QueryBuilderTrait;
44 /**
45 * @var string Key used in a token response to identify the resource owner.
46 */
47 const ACCESS_TOKEN_RESOURCE_OWNER_ID = null;
48 /**
49 * @var string HTTP method used to fetch access tokens.
50 */
51 const METHOD_GET = 'GET';
52 /**
53 * @var string HTTP method used to fetch access tokens.
54 */
55 const METHOD_POST = 'POST';
56 /**
57 * @var string
58 */
59 protected $clientId;
60 /**
61 * @var string
62 */
63 protected $clientSecret;
64 /**
65 * @var string
66 */
67 protected $redirectUri;
68 /**
69 * @var string
70 */
71 protected $state;
72 /**
73 * @var GrantFactory
74 */
75 protected $grantFactory;
76 /**
77 * @var RequestFactory
78 */
79 protected $requestFactory;
80 /**
81 * @var HttpClientInterface
82 */
83 protected $httpClient;
84 /**
85 * @var OptionProviderInterface
86 */
87 protected $optionProvider;
88 /**
89 * Constructs an OAuth 2.0 service provider.
90 *
91 * @param array $options An array of options to set on this provider.
92 * Options include `clientId`, `clientSecret`, `redirectUri`, and `state`.
93 * Individual providers may introduce more options, as needed.
94 * @param array $collaborators An array of collaborators that may be used to
95 * override this provider's default behavior. Collaborators include
96 * `grantFactory`, `requestFactory`, and `httpClient`.
97 * Individual providers may introduce more collaborators, as needed.
98 */
99 public function __construct(array $options = [], array $collaborators = [])
100 {
101 // We'll let the GuardedPropertyTrait handle mass assignment of incoming
102 // options, skipping any blacklisted properties defined in the provider
103 $this->fillProperties($options);
104 if (empty($collaborators['grantFactory'])) {
105 $collaborators['grantFactory'] = new \YoastSEO_Vendor\League\OAuth2\Client\Grant\GrantFactory();
106 }
107 $this->setGrantFactory($collaborators['grantFactory']);
108 if (empty($collaborators['requestFactory'])) {
109 $collaborators['requestFactory'] = new \YoastSEO_Vendor\League\OAuth2\Client\Tool\RequestFactory();
110 }
111 $this->setRequestFactory($collaborators['requestFactory']);
112 if (empty($collaborators['httpClient'])) {
113 $client_options = $this->getAllowedClientOptions($options);
114 $collaborators['httpClient'] = new \YoastSEO_Vendor\GuzzleHttp\Client(\array_intersect_key($options, \array_flip($client_options)));
115 }
116 $this->setHttpClient($collaborators['httpClient']);
117 if (empty($collaborators['optionProvider'])) {
118 $collaborators['optionProvider'] = new \YoastSEO_Vendor\League\OAuth2\Client\OptionProvider\PostAuthOptionProvider();
119 }
120 $this->setOptionProvider($collaborators['optionProvider']);
121 }
122 /**
123 * Returns the list of options that can be passed to the HttpClient
124 *
125 * @param array $options An array of options to set on this provider.
126 * Options include `clientId`, `clientSecret`, `redirectUri`, and `state`.
127 * Individual providers may introduce more options, as needed.
128 * @return array The options to pass to the HttpClient constructor
129 */
130 protected function getAllowedClientOptions(array $options)
131 {
132 $client_options = ['timeout', 'proxy'];
133 // Only allow turning off ssl verification if it's for a proxy
134 if (!empty($options['proxy'])) {
135 $client_options[] = 'verify';
136 }
137 return $client_options;
138 }
139 /**
140 * Sets the grant factory instance.
141 *
142 * @param GrantFactory $factory
143 * @return self
144 */
145 public function setGrantFactory(\YoastSEO_Vendor\League\OAuth2\Client\Grant\GrantFactory $factory)
146 {
147 $this->grantFactory = $factory;
148 return $this;
149 }
150 /**
151 * Returns the current grant factory instance.
152 *
153 * @return GrantFactory
154 */
155 public function getGrantFactory()
156 {
157 return $this->grantFactory;
158 }
159 /**
160 * Sets the request factory instance.
161 *
162 * @param RequestFactory $factory
163 * @return self
164 */
165 public function setRequestFactory(\YoastSEO_Vendor\League\OAuth2\Client\Tool\RequestFactory $factory)
166 {
167 $this->requestFactory = $factory;
168 return $this;
169 }
170 /**
171 * Returns the request factory instance.
172 *
173 * @return RequestFactory
174 */
175 public function getRequestFactory()
176 {
177 return $this->requestFactory;
178 }
179 /**
180 * Sets the HTTP client instance.
181 *
182 * @param HttpClientInterface $client
183 * @return self
184 */
185 public function setHttpClient(\YoastSEO_Vendor\GuzzleHttp\ClientInterface $client)
186 {
187 $this->httpClient = $client;
188 return $this;
189 }
190 /**
191 * Returns the HTTP client instance.
192 *
193 * @return HttpClientInterface
194 */
195 public function getHttpClient()
196 {
197 return $this->httpClient;
198 }
199 /**
200 * Sets the option provider instance.
201 *
202 * @param OptionProviderInterface $provider
203 * @return self
204 */
205 public function setOptionProvider(\YoastSEO_Vendor\League\OAuth2\Client\OptionProvider\OptionProviderInterface $provider)
206 {
207 $this->optionProvider = $provider;
208 return $this;
209 }
210 /**
211 * Returns the option provider instance.
212 *
213 * @return OptionProviderInterface
214 */
215 public function getOptionProvider()
216 {
217 return $this->optionProvider;
218 }
219 /**
220 * Returns the current value of the state parameter.
221 *
222 * This can be accessed by the redirect handler during authorization.
223 *
224 * @return string
225 */
226 public function getState()
227 {
228 return $this->state;
229 }
230 /**
231 * Returns the base URL for authorizing a client.
232 *
233 * Eg. https://oauth.service.com/authorize
234 *
235 * @return string
236 */
237 public abstract function getBaseAuthorizationUrl();
238 /**
239 * Returns the base URL for requesting an access token.
240 *
241 * Eg. https://oauth.service.com/token
242 *
243 * @param array $params
244 * @return string
245 */
246 public abstract function getBaseAccessTokenUrl(array $params);
247 /**
248 * Returns the URL for requesting the resource owner's details.
249 *
250 * @param AccessToken $token
251 * @return string
252 */
253 public abstract function getResourceOwnerDetailsUrl(\YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken $token);
254 /**
255 * Returns a new random string to use as the state parameter in an
256 * authorization flow.
257 *
258 * @param int $length Length of the random string to be generated.
259 * @return string
260 */
261 protected function getRandomState($length = 32)
262 {
263 // Converting bytes to hex will always double length. Hence, we can reduce
264 // the amount of bytes by half to produce the correct length.
265 return \bin2hex(\random_bytes($length / 2));
266 }
267 /**
268 * Returns the default scopes used by this provider.
269 *
270 * This should only be the scopes that are required to request the details
271 * of the resource owner, rather than all the available scopes.
272 *
273 * @return array
274 */
275 protected abstract function getDefaultScopes();
276 /**
277 * Returns the string that should be used to separate scopes when building
278 * the URL for requesting an access token.
279 *
280 * @return string Scope separator, defaults to ','
281 */
282 protected function getScopeSeparator()
283 {
284 return ',';
285 }
286 /**
287 * Returns authorization parameters based on provided options.
288 *
289 * @param array $options
290 * @return array Authorization parameters
291 */
292 protected function getAuthorizationParameters(array $options)
293 {
294 if (empty($options['state'])) {
295 $options['state'] = $this->getRandomState();
296 }
297 if (empty($options['scope'])) {
298 $options['scope'] = $this->getDefaultScopes();
299 }
300 $options += ['response_type' => 'code', 'approval_prompt' => 'auto'];
301 if (\is_array($options['scope'])) {
302 $separator = $this->getScopeSeparator();
303 $options['scope'] = \implode($separator, $options['scope']);
304 }
305 // Store the state as it may need to be accessed later on.
306 $this->state = $options['state'];
307 // Business code layer might set a different redirect_uri parameter
308 // depending on the context, leave it as-is
309 if (!isset($options['redirect_uri'])) {
310 $options['redirect_uri'] = $this->redirectUri;
311 }
312 $options['client_id'] = $this->clientId;
313 return $options;
314 }
315 /**
316 * Builds the authorization URL's query string.
317 *
318 * @param array $params Query parameters
319 * @return string Query string
320 */
321 protected function getAuthorizationQuery(array $params)
322 {
323 return $this->buildQueryString($params);
324 }
325 /**
326 * Builds the authorization URL.
327 *
328 * @param array $options
329 * @return string Authorization URL
330 */
331 public function getAuthorizationUrl(array $options = [])
332 {
333 $base = $this->getBaseAuthorizationUrl();
334 $params = $this->getAuthorizationParameters($options);
335 $query = $this->getAuthorizationQuery($params);
336 return $this->appendQuery($base, $query);
337 }
338 /**
339 * Redirects the client for authorization.
340 *
341 * @param array $options
342 * @param callable|null $redirectHandler
343 * @return mixed
344 */
345 public function authorize(array $options = [], callable $redirectHandler = null)
346 {
347 $url = $this->getAuthorizationUrl($options);
348 if ($redirectHandler) {
349 return $redirectHandler($url, $this);
350 }
351 // @codeCoverageIgnoreStart
352 \header('Location: ' . $url);
353 exit;
354 // @codeCoverageIgnoreEnd
355 }
356 /**
357 * Appends a query string to a URL.
358 *
359 * @param string $url The URL to append the query to
360 * @param string $query The HTTP query string
361 * @return string The resulting URL
362 */
363 protected function appendQuery($url, $query)
364 {
365 $query = \trim($query, '?&');
366 if ($query) {
367 $glue = \strstr($url, '?') === \false ? '?' : '&';
368 return $url . $glue . $query;
369 }
370 return $url;
371 }
372 /**
373 * Returns the method to use when requesting an access token.
374 *
375 * @return string HTTP method
376 */
377 protected function getAccessTokenMethod()
378 {
379 return self::METHOD_POST;
380 }
381 /**
382 * Returns the key used in the access token response to identify the resource owner.
383 *
384 * @return string|null Resource owner identifier key
385 */
386 protected function getAccessTokenResourceOwnerId()
387 {
388 return static::ACCESS_TOKEN_RESOURCE_OWNER_ID;
389 }
390 /**
391 * Builds the access token URL's query string.
392 *
393 * @param array $params Query parameters
394 * @return string Query string
395 */
396 protected function getAccessTokenQuery(array $params)
397 {
398 return $this->buildQueryString($params);
399 }
400 /**
401 * Checks that a provided grant is valid, or attempts to produce one if the
402 * provided grant is a string.
403 *
404 * @param AbstractGrant|string $grant
405 * @return AbstractGrant
406 */
407 protected function verifyGrant($grant)
408 {
409 if (\is_string($grant)) {
410 return $this->grantFactory->getGrant($grant);
411 }
412 $this->grantFactory->checkGrant($grant);
413 return $grant;
414 }
415 /**
416 * Returns the full URL to use when requesting an access token.
417 *
418 * @param array $params Query parameters
419 * @return string
420 */
421 protected function getAccessTokenUrl(array $params)
422 {
423 $url = $this->getBaseAccessTokenUrl($params);
424 if ($this->getAccessTokenMethod() === self::METHOD_GET) {
425 $query = $this->getAccessTokenQuery($params);
426 return $this->appendQuery($url, $query);
427 }
428 return $url;
429 }
430 /**
431 * Returns a prepared request for requesting an access token.
432 *
433 * @param array $params Query string parameters
434 * @return RequestInterface
435 */
436 protected function getAccessTokenRequest(array $params)
437 {
438 $method = $this->getAccessTokenMethod();
439 $url = $this->getAccessTokenUrl($params);
440 $options = $this->optionProvider->getAccessTokenOptions($this->getAccessTokenMethod(), $params);
441 return $this->getRequest($method, $url, $options);
442 }
443 /**
444 * Requests an access token using a specified grant and option set.
445 *
446 * @param mixed $grant
447 * @param array $options
448 * @throws IdentityProviderException
449 * @return AccessTokenInterface
450 */
451 public function getAccessToken($grant, array $options = [])
452 {
453 $grant = $this->verifyGrant($grant);
454 $params = ['client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'redirect_uri' => $this->redirectUri];
455 $params = $grant->prepareRequestParameters($params, $options);
456 $request = $this->getAccessTokenRequest($params);
457 $response = $this->getParsedResponse($request);
458 if (\false === \is_array($response)) {
459 throw new \UnexpectedValueException('Invalid response received from Authorization Server. Expected JSON.');
460 }
461 $prepared = $this->prepareAccessTokenResponse($response);
462 $token = $this->createAccessToken($prepared, $grant);
463 return $token;
464 }
465 /**
466 * Returns a PSR-7 request instance that is not authenticated.
467 *
468 * @param string $method
469 * @param string $url
470 * @param array $options
471 * @return RequestInterface
472 */
473 public function getRequest($method, $url, array $options = [])
474 {
475 return $this->createRequest($method, $url, null, $options);
476 }
477 /**
478 * Returns an authenticated PSR-7 request instance.
479 *
480 * @param string $method
481 * @param string $url
482 * @param AccessTokenInterface|string $token
483 * @param array $options Any of "headers", "body", and "protocolVersion".
484 * @return RequestInterface
485 */
486 public function getAuthenticatedRequest($method, $url, $token, array $options = [])
487 {
488 return $this->createRequest($method, $url, $token, $options);
489 }
490 /**
491 * Creates a PSR-7 request instance.
492 *
493 * @param string $method
494 * @param string $url
495 * @param AccessTokenInterface|string|null $token
496 * @param array $options
497 * @return RequestInterface
498 */
499 protected function createRequest($method, $url, $token, array $options)
500 {
501 $defaults = ['headers' => $this->getHeaders($token)];
502 $options = \array_merge_recursive($defaults, $options);
503 $factory = $this->getRequestFactory();
504 return $factory->getRequestWithOptions($method, $url, $options);
505 }
506 /**
507 * Sends a request instance and returns a response instance.
508 *
509 * WARNING: This method does not attempt to catch exceptions caused by HTTP
510 * errors! It is recommended to wrap this method in a try/catch block.
511 *
512 * @param RequestInterface $request
513 * @return ResponseInterface
514 */
515 public function getResponse(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request)
516 {
517 return $this->getHttpClient()->send($request);
518 }
519 /**
520 * Sends a request and returns the parsed response.
521 *
522 * @param RequestInterface $request
523 * @throws IdentityProviderException
524 * @return mixed
525 */
526 public function getParsedResponse(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request)
527 {
528 try {
529 $response = $this->getResponse($request);
530 } catch (\YoastSEO_Vendor\GuzzleHttp\Exception\BadResponseException $e) {
531 $response = $e->getResponse();
532 }
533 $parsed = $this->parseResponse($response);
534 $this->checkResponse($response, $parsed);
535 return $parsed;
536 }
537 /**
538 * Attempts to parse a JSON response.
539 *
540 * @param string $content JSON content from response body
541 * @return array Parsed JSON data
542 * @throws UnexpectedValueException if the content could not be parsed
543 */
544 protected function parseJson($content)
545 {
546 $content = \json_decode($content, \true);
547 if (\json_last_error() !== \JSON_ERROR_NONE) {
548 throw new \UnexpectedValueException(\sprintf("Failed to parse JSON response: %s", \json_last_error_msg()));
549 }
550 return $content;
551 }
552 /**
553 * Returns the content type header of a response.
554 *
555 * @param ResponseInterface $response
556 * @return string Semi-colon separated join of content-type headers.
557 */
558 protected function getContentType(\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response)
559 {
560 return \join(';', (array) $response->getHeader('content-type'));
561 }
562 /**
563 * Parses the response according to its content-type header.
564 *
565 * @throws UnexpectedValueException
566 * @param ResponseInterface $response
567 * @return array
568 */
569 protected function parseResponse(\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response)
570 {
571 $content = (string) $response->getBody();
572 $type = $this->getContentType($response);
573 if (\strpos($type, 'urlencoded') !== \false) {
574 \parse_str($content, $parsed);
575 return $parsed;
576 }
577 // Attempt to parse the string as JSON regardless of content type,
578 // since some providers use non-standard content types. Only throw an
579 // exception if the JSON could not be parsed when it was expected to.
580 try {
581 return $this->parseJson($content);
582 } catch (\UnexpectedValueException $e) {
583 if (\strpos($type, 'json') !== \false) {
584 throw $e;
585 }
586 if ($response->getStatusCode() == 500) {
587 throw new \UnexpectedValueException('An OAuth server error was encountered that did not contain a JSON body', 0, $e);
588 }
589 return $content;
590 }
591 }
592 /**
593 * Checks a provider response for errors.
594 *
595 * @throws IdentityProviderException
596 * @param ResponseInterface $response
597 * @param array|string $data Parsed response data
598 * @return void
599 */
600 protected abstract function checkResponse(\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response, $data);
601 /**
602 * Prepares an parsed access token response for a grant.
603 *
604 * Custom mapping of expiration, etc should be done here. Always call the
605 * parent method when overloading this method.
606 *
607 * @param mixed $result
608 * @return array
609 */
610 protected function prepareAccessTokenResponse(array $result)
611 {
612 if ($this->getAccessTokenResourceOwnerId() !== null) {
613 $result['resource_owner_id'] = $this->getValueByKey($result, $this->getAccessTokenResourceOwnerId());
614 }
615 return $result;
616 }
617 /**
618 * Creates an access token from a response.
619 *
620 * The grant that was used to fetch the response can be used to provide
621 * additional context.
622 *
623 * @param array $response
624 * @param AbstractGrant $grant
625 * @return AccessTokenInterface
626 */
627 protected function createAccessToken(array $response, \YoastSEO_Vendor\League\OAuth2\Client\Grant\AbstractGrant $grant)
628 {
629 return new \YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken($response);
630 }
631 /**
632 * Generates a resource owner object from a successful resource owner
633 * details request.
634 *
635 * @param array $response
636 * @param AccessToken $token
637 * @return ResourceOwnerInterface
638 */
639 protected abstract function createResourceOwner(array $response, \YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken $token);
640 /**
641 * Requests and returns the resource owner of given access token.
642 *
643 * @param AccessToken $token
644 * @return ResourceOwnerInterface
645 */
646 public function getResourceOwner(\YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken $token)
647 {
648 $response = $this->fetchResourceOwnerDetails($token);
649 return $this->createResourceOwner($response, $token);
650 }
651 /**
652 * Requests resource owner details.
653 *
654 * @param AccessToken $token
655 * @return mixed
656 */
657 protected function fetchResourceOwnerDetails(\YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken $token)
658 {
659 $url = $this->getResourceOwnerDetailsUrl($token);
660 $request = $this->getAuthenticatedRequest(self::METHOD_GET, $url, $token);
661 $response = $this->getParsedResponse($request);
662 if (\false === \is_array($response)) {
663 throw new \UnexpectedValueException('Invalid response received from Authorization Server. Expected JSON.');
664 }
665 return $response;
666 }
667 /**
668 * Returns the default headers used by this provider.
669 *
670 * Typically this is used to set 'Accept' or 'Content-Type' headers.
671 *
672 * @return array
673 */
674 protected function getDefaultHeaders()
675 {
676 return [];
677 }
678 /**
679 * Returns the authorization headers used by this provider.
680 *
681 * Typically this is "Bearer" or "MAC". For more information see:
682 * http://tools.ietf.org/html/rfc6749#section-7.1
683 *
684 * No default is provided, providers must overload this method to activate
685 * authorization headers.
686 *
687 * @param mixed|null $token Either a string or an access token instance
688 * @return array
689 */
690 protected function getAuthorizationHeaders($token = null)
691 {
692 return [];
693 }
694 /**
695 * Returns all headers used by this provider for a request.
696 *
697 * The request will be authenticated if an access token is provided.
698 *
699 * @param mixed|null $token object or string
700 * @return array
701 */
702 public function getHeaders($token = null)
703 {
704 if ($token) {
705 return \array_merge($this->getDefaultHeaders(), $this->getAuthorizationHeaders($token));
706 }
707 return $this->getDefaultHeaders();
708 }
709 }
710