1<?php 2/* 3 * Copyright 2010 Google Inc. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18namespace Google; 19 20use Google\AccessToken\Revoke; 21use Google\AccessToken\Verify; 22use Google\Auth\ApplicationDefaultCredentials; 23use Google\Auth\Cache\MemoryCacheItemPool; 24use Google\Auth\CredentialsLoader; 25use Google\Auth\FetchAuthTokenCache; 26use Google\Auth\HttpHandler\HttpHandlerFactory; 27use Google\Auth\OAuth2; 28use Google\Auth\Credentials\ServiceAccountCredentials; 29use Google\Auth\Credentials\UserRefreshCredentials; 30use Google\AuthHandler\AuthHandlerFactory; 31use Google\Http\REST; 32use GuzzleHttp\Client as GuzzleClient; 33use GuzzleHttp\ClientInterface; 34use GuzzleHttp\Ring\Client\StreamHandler; 35use Psr\Cache\CacheItemPoolInterface; 36use Psr\Http\Message\RequestInterface; 37use Psr\Log\LoggerInterface; 38use Monolog\Logger; 39use Monolog\Handler\StreamHandler as MonologStreamHandler; 40use Monolog\Handler\SyslogHandler as MonologSyslogHandler; 41use BadMethodCallException; 42use DomainException; 43use InvalidArgumentException; 44use LogicException; 45 46/** 47 * The Google API Client 48 * https://github.com/google/google-api-php-client 49 */ 50class Client 51{ 52 const LIBVER = "2.12.1"; 53 const USER_AGENT_SUFFIX = "google-api-php-client/"; 54 const OAUTH2_REVOKE_URI = 'https://oauth2.googleapis.com/revoke'; 55 const OAUTH2_TOKEN_URI = 'https://oauth2.googleapis.com/token'; 56 const OAUTH2_AUTH_URL = 'https://accounts.google.com/o/oauth2/auth'; 57 const API_BASE_PATH = 'https://www.googleapis.com'; 58 59 /** 60 * @var OAuth2 $auth 61 */ 62 private $auth; 63 64 /** 65 * @var ClientInterface $http 66 */ 67 private $http; 68 69 /** 70 * @var CacheItemPoolInterface $cache 71 */ 72 private $cache; 73 74 /** 75 * @var array access token 76 */ 77 private $token; 78 79 /** 80 * @var array $config 81 */ 82 private $config; 83 84 /** 85 * @var LoggerInterface $logger 86 */ 87 private $logger; 88 89 /** 90 * @var CredentialsLoader $credentials 91 */ 92 private $credentials; 93 94 /** 95 * @var boolean $deferExecution 96 */ 97 private $deferExecution = false; 98 99 /** @var array $scopes */ 100 // Scopes requested by the client 101 protected $requestedScopes = []; 102 103 /** 104 * Construct the Google Client. 105 * 106 * @param array $config 107 */ 108 public function __construct(array $config = array()) 109 { 110 $this->config = array_merge( 111 [ 112 'application_name' => '', 113 114 // Don't change these unless you're working against a special development 115 // or testing environment. 116 'base_path' => self::API_BASE_PATH, 117 118 // https://developers.google.com/console 119 'client_id' => '', 120 'client_secret' => '', 121 122 // Can be a path to JSON credentials or an array representing those 123 // credentials (@see Google\Client::setAuthConfig), or an instance of 124 // Google\Auth\CredentialsLoader. 125 'credentials' => null, 126 // @see Google\Client::setScopes 127 'scopes' => null, 128 // Sets X-Goog-User-Project, which specifies a user project to bill 129 // for access charges associated with the request 130 'quota_project' => null, 131 132 'redirect_uri' => null, 133 'state' => null, 134 135 // Simple API access key, also from the API console. Ensure you get 136 // a Server key, and not a Browser key. 137 'developer_key' => '', 138 139 // For use with Google Cloud Platform 140 // fetch the ApplicationDefaultCredentials, if applicable 141 // @see https://developers.google.com/identity/protocols/application-default-credentials 142 'use_application_default_credentials' => false, 143 'signing_key' => null, 144 'signing_algorithm' => null, 145 'subject' => null, 146 147 // Other OAuth2 parameters. 148 'hd' => '', 149 'prompt' => '', 150 'openid.realm' => '', 151 'include_granted_scopes' => null, 152 'login_hint' => '', 153 'request_visible_actions' => '', 154 'access_type' => 'online', 155 'approval_prompt' => 'auto', 156 157 // Task Runner retry configuration 158 // @see Google\Task\Runner 159 'retry' => array(), 160 'retry_map' => null, 161 162 // Cache class implementing Psr\Cache\CacheItemPoolInterface. 163 // Defaults to Google\Auth\Cache\MemoryCacheItemPool. 164 'cache' => null, 165 // cache config for downstream auth caching 166 'cache_config' => [], 167 168 // function to be called when an access token is fetched 169 // follows the signature function ($cacheKey, $accessToken) 170 'token_callback' => null, 171 172 // Service class used in Google\Client::verifyIdToken. 173 // Explicitly pass this in to avoid setting JWT::$leeway 174 'jwt' => null, 175 176 // Setting api_format_v2 will return more detailed error messages 177 // from certain APIs. 178 'api_format_v2' => false 179 ], 180 $config 181 ); 182 183 if (!is_null($this->config['credentials'])) { 184 if ($this->config['credentials'] instanceof CredentialsLoader) { 185 $this->credentials = $this->config['credentials']; 186 } else { 187 $this->setAuthConfig($this->config['credentials']); 188 } 189 unset($this->config['credentials']); 190 } 191 192 if (!is_null($this->config['scopes'])) { 193 $this->setScopes($this->config['scopes']); 194 unset($this->config['scopes']); 195 } 196 197 // Set a default token callback to update the in-memory access token 198 if (is_null($this->config['token_callback'])) { 199 $this->config['token_callback'] = function ($cacheKey, $newAccessToken) { 200 $this->setAccessToken( 201 [ 202 'access_token' => $newAccessToken, 203 'expires_in' => 3600, // Google default 204 'created' => time(), 205 ] 206 ); 207 }; 208 } 209 210 if (!is_null($this->config['cache'])) { 211 $this->setCache($this->config['cache']); 212 unset($this->config['cache']); 213 } 214 } 215 216 /** 217 * Get a string containing the version of the library. 218 * 219 * @return string 220 */ 221 public function getLibraryVersion() 222 { 223 return self::LIBVER; 224 } 225 226 /** 227 * For backwards compatibility 228 * alias for fetchAccessTokenWithAuthCode 229 * 230 * @param $code string code from accounts.google.com 231 * @return array access token 232 * @deprecated 233 */ 234 public function authenticate($code) 235 { 236 return $this->fetchAccessTokenWithAuthCode($code); 237 } 238 239 /** 240 * Attempt to exchange a code for an valid authentication token. 241 * Helper wrapped around the OAuth 2.0 implementation. 242 * 243 * @param $code string code from accounts.google.com 244 * @return array access token 245 */ 246 public function fetchAccessTokenWithAuthCode($code) 247 { 248 if (strlen($code) == 0) { 249 throw new InvalidArgumentException("Invalid code"); 250 } 251 252 $auth = $this->getOAuth2Service(); 253 $auth->setCode($code); 254 $auth->setRedirectUri($this->getRedirectUri()); 255 256 $httpHandler = HttpHandlerFactory::build($this->getHttpClient()); 257 $creds = $auth->fetchAuthToken($httpHandler); 258 if ($creds && isset($creds['access_token'])) { 259 $creds['created'] = time(); 260 $this->setAccessToken($creds); 261 } 262 263 return $creds; 264 } 265 266 /** 267 * For backwards compatibility 268 * alias for fetchAccessTokenWithAssertion 269 * 270 * @return array access token 271 * @deprecated 272 */ 273 public function refreshTokenWithAssertion() 274 { 275 return $this->fetchAccessTokenWithAssertion(); 276 } 277 278 /** 279 * Fetches a fresh access token with a given assertion token. 280 * @param ClientInterface $authHttp optional. 281 * @return array access token 282 */ 283 public function fetchAccessTokenWithAssertion(ClientInterface $authHttp = null) 284 { 285 if (!$this->isUsingApplicationDefaultCredentials()) { 286 throw new DomainException( 287 'set the JSON service account credentials using' 288 . ' Google\Client::setAuthConfig or set the path to your JSON file' 289 . ' with the "GOOGLE_APPLICATION_CREDENTIALS" environment variable' 290 . ' and call Google\Client::useApplicationDefaultCredentials to' 291 . ' refresh a token with assertion.' 292 ); 293 } 294 295 $this->getLogger()->log( 296 'info', 297 'OAuth2 access token refresh with Signed JWT assertion grants.' 298 ); 299 300 $credentials = $this->createApplicationDefaultCredentials(); 301 302 $httpHandler = HttpHandlerFactory::build($authHttp); 303 $creds = $credentials->fetchAuthToken($httpHandler); 304 if ($creds && isset($creds['access_token'])) { 305 $creds['created'] = time(); 306 $this->setAccessToken($creds); 307 } 308 309 return $creds; 310 } 311 312 /** 313 * For backwards compatibility 314 * alias for fetchAccessTokenWithRefreshToken 315 * 316 * @param string $refreshToken 317 * @return array access token 318 */ 319 public function refreshToken($refreshToken) 320 { 321 return $this->fetchAccessTokenWithRefreshToken($refreshToken); 322 } 323 324 /** 325 * Fetches a fresh OAuth 2.0 access token with the given refresh token. 326 * @param string $refreshToken 327 * @return array access token 328 */ 329 public function fetchAccessTokenWithRefreshToken($refreshToken = null) 330 { 331 if (null === $refreshToken) { 332 if (!isset($this->token['refresh_token'])) { 333 throw new LogicException( 334 'refresh token must be passed in or set as part of setAccessToken' 335 ); 336 } 337 $refreshToken = $this->token['refresh_token']; 338 } 339 $this->getLogger()->info('OAuth2 access token refresh'); 340 $auth = $this->getOAuth2Service(); 341 $auth->setRefreshToken($refreshToken); 342 343 $httpHandler = HttpHandlerFactory::build($this->getHttpClient()); 344 $creds = $auth->fetchAuthToken($httpHandler); 345 if ($creds && isset($creds['access_token'])) { 346 $creds['created'] = time(); 347 if (!isset($creds['refresh_token'])) { 348 $creds['refresh_token'] = $refreshToken; 349 } 350 $this->setAccessToken($creds); 351 } 352 353 return $creds; 354 } 355 356 /** 357 * Create a URL to obtain user authorization. 358 * The authorization endpoint allows the user to first 359 * authenticate, and then grant/deny the access request. 360 * @param string|array $scope The scope is expressed as an array or list of space-delimited strings. 361 * @return string 362 */ 363 public function createAuthUrl($scope = null) 364 { 365 if (empty($scope)) { 366 $scope = $this->prepareScopes(); 367 } 368 if (is_array($scope)) { 369 $scope = implode(' ', $scope); 370 } 371 372 // only accept one of prompt or approval_prompt 373 $approvalPrompt = $this->config['prompt'] 374 ? null 375 : $this->config['approval_prompt']; 376 377 // include_granted_scopes should be string "true", string "false", or null 378 $includeGrantedScopes = $this->config['include_granted_scopes'] === null 379 ? null 380 : var_export($this->config['include_granted_scopes'], true); 381 382 $params = array_filter( 383 [ 384 'access_type' => $this->config['access_type'], 385 'approval_prompt' => $approvalPrompt, 386 'hd' => $this->config['hd'], 387 'include_granted_scopes' => $includeGrantedScopes, 388 'login_hint' => $this->config['login_hint'], 389 'openid.realm' => $this->config['openid.realm'], 390 'prompt' => $this->config['prompt'], 391 'response_type' => 'code', 392 'scope' => $scope, 393 'state' => $this->config['state'], 394 ] 395 ); 396 397 // If the list of scopes contains plus.login, add request_visible_actions 398 // to auth URL. 399 $rva = $this->config['request_visible_actions']; 400 if (strlen($rva) > 0 && false !== strpos($scope, 'plus.login')) { 401 $params['request_visible_actions'] = $rva; 402 } 403 404 $auth = $this->getOAuth2Service(); 405 406 return (string) $auth->buildFullAuthorizationUri($params); 407 } 408 409 /** 410 * Adds auth listeners to the HTTP client based on the credentials 411 * set in the Google API Client object 412 * 413 * @param ClientInterface $http the http client object. 414 * @return ClientInterface the http client object 415 */ 416 public function authorize(ClientInterface $http = null) 417 { 418 $http = $http ?: $this->getHttpClient(); 419 $authHandler = $this->getAuthHandler(); 420 421 // These conditionals represent the decision tree for authentication 422 // 1. Check if a Google\Auth\CredentialsLoader instance has been supplied via the "credentials" option 423 // 2. Check for Application Default Credentials 424 // 3a. Check for an Access Token 425 // 3b. If access token exists but is expired, try to refresh it 426 // 4. Check for API Key 427 if ($this->credentials) { 428 return $authHandler->attachCredentials( 429 $http, 430 $this->credentials, 431 $this->config['token_callback'] 432 ); 433 } 434 435 if ($this->isUsingApplicationDefaultCredentials()) { 436 $credentials = $this->createApplicationDefaultCredentials(); 437 return $authHandler->attachCredentialsCache( 438 $http, 439 $credentials, 440 $this->config['token_callback'] 441 ); 442 } 443 444 if ($token = $this->getAccessToken()) { 445 $scopes = $this->prepareScopes(); 446 // add refresh subscriber to request a new token 447 if (isset($token['refresh_token']) && $this->isAccessTokenExpired()) { 448 $credentials = $this->createUserRefreshCredentials( 449 $scopes, 450 $token['refresh_token'] 451 ); 452 return $authHandler->attachCredentials( 453 $http, 454 $credentials, 455 $this->config['token_callback'] 456 ); 457 } 458 459 return $authHandler->attachToken($http, $token, (array) $scopes); 460 } 461 462 if ($key = $this->config['developer_key']) { 463 return $authHandler->attachKey($http, $key); 464 } 465 466 return $http; 467 } 468 469 /** 470 * Set the configuration to use application default credentials for 471 * authentication 472 * 473 * @see https://developers.google.com/identity/protocols/application-default-credentials 474 * @param boolean $useAppCreds 475 */ 476 public function useApplicationDefaultCredentials($useAppCreds = true) 477 { 478 $this->config['use_application_default_credentials'] = $useAppCreds; 479 } 480 481 /** 482 * To prevent useApplicationDefaultCredentials from inappropriately being 483 * called in a conditional 484 * 485 * @see https://developers.google.com/identity/protocols/application-default-credentials 486 */ 487 public function isUsingApplicationDefaultCredentials() 488 { 489 return $this->config['use_application_default_credentials']; 490 } 491 492 /** 493 * Set the access token used for requests. 494 * 495 * Note that at the time requests are sent, tokens are cached. A token will be 496 * cached for each combination of service and authentication scopes. If a 497 * cache pool is not provided, creating a new instance of the client will 498 * allow modification of access tokens. If a persistent cache pool is 499 * provided, in order to change the access token, you must clear the cached 500 * token by calling `$client->getCache()->clear()`. (Use caution in this case, 501 * as calling `clear()` will remove all cache items, including any items not 502 * related to Google API PHP Client.) 503 * 504 * @param string|array $token 505 * @throws InvalidArgumentException 506 */ 507 public function setAccessToken($token) 508 { 509 if (is_string($token)) { 510 if ($json = json_decode($token, true)) { 511 $token = $json; 512 } else { 513 // assume $token is just the token string 514 $token = array( 515 'access_token' => $token, 516 ); 517 } 518 } 519 if ($token == null) { 520 throw new InvalidArgumentException('invalid json token'); 521 } 522 if (!isset($token['access_token'])) { 523 throw new InvalidArgumentException("Invalid token format"); 524 } 525 $this->token = $token; 526 } 527 528 public function getAccessToken() 529 { 530 return $this->token; 531 } 532 533 /** 534 * @return string|null 535 */ 536 public function getRefreshToken() 537 { 538 if (isset($this->token['refresh_token'])) { 539 return $this->token['refresh_token']; 540 } 541 542 return null; 543 } 544 545 /** 546 * Returns if the access_token is expired. 547 * @return bool Returns True if the access_token is expired. 548 */ 549 public function isAccessTokenExpired() 550 { 551 if (!$this->token) { 552 return true; 553 } 554 555 $created = 0; 556 if (isset($this->token['created'])) { 557 $created = $this->token['created']; 558 } elseif (isset($this->token['id_token'])) { 559 // check the ID token for "iat" 560 // signature verification is not required here, as we are just 561 // using this for convenience to save a round trip request 562 // to the Google API server 563 $idToken = $this->token['id_token']; 564 if (substr_count($idToken, '.') == 2) { 565 $parts = explode('.', $idToken); 566 $payload = json_decode(base64_decode($parts[1]), true); 567 if ($payload && isset($payload['iat'])) { 568 $created = $payload['iat']; 569 } 570 } 571 } 572 if (!isset($this->token['expires_in'])) { 573 // if the token does not have an "expires_in", then it's considered expired 574 return true; 575 } 576 577 // If the token is set to expire in the next 30 seconds. 578 return ($created + ($this->token['expires_in'] - 30)) < time(); 579 } 580 581 /** 582 * @deprecated See UPGRADING.md for more information 583 */ 584 public function getAuth() 585 { 586 throw new BadMethodCallException( 587 'This function no longer exists. See UPGRADING.md for more information' 588 ); 589 } 590 591 /** 592 * @deprecated See UPGRADING.md for more information 593 */ 594 public function setAuth($auth) 595 { 596 throw new BadMethodCallException( 597 'This function no longer exists. See UPGRADING.md for more information' 598 ); 599 } 600 601 /** 602 * Set the OAuth 2.0 Client ID. 603 * @param string $clientId 604 */ 605 public function setClientId($clientId) 606 { 607 $this->config['client_id'] = $clientId; 608 } 609 610 public function getClientId() 611 { 612 return $this->config['client_id']; 613 } 614 615 /** 616 * Set the OAuth 2.0 Client Secret. 617 * @param string $clientSecret 618 */ 619 public function setClientSecret($clientSecret) 620 { 621 $this->config['client_secret'] = $clientSecret; 622 } 623 624 public function getClientSecret() 625 { 626 return $this->config['client_secret']; 627 } 628 629 /** 630 * Set the OAuth 2.0 Redirect URI. 631 * @param string $redirectUri 632 */ 633 public function setRedirectUri($redirectUri) 634 { 635 $this->config['redirect_uri'] = $redirectUri; 636 } 637 638 public function getRedirectUri() 639 { 640 return $this->config['redirect_uri']; 641 } 642 643 /** 644 * Set OAuth 2.0 "state" parameter to achieve per-request customization. 645 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-3.1.2.2 646 * @param string $state 647 */ 648 public function setState($state) 649 { 650 $this->config['state'] = $state; 651 } 652 653 /** 654 * @param string $accessType Possible values for access_type include: 655 * {@code "offline"} to request offline access from the user. 656 * {@code "online"} to request online access from the user. 657 */ 658 public function setAccessType($accessType) 659 { 660 $this->config['access_type'] = $accessType; 661 } 662 663 /** 664 * @param string $approvalPrompt Possible values for approval_prompt include: 665 * {@code "force"} to force the approval UI to appear. 666 * {@code "auto"} to request auto-approval when possible. (This is the default value) 667 */ 668 public function setApprovalPrompt($approvalPrompt) 669 { 670 $this->config['approval_prompt'] = $approvalPrompt; 671 } 672 673 /** 674 * Set the login hint, email address or sub id. 675 * @param string $loginHint 676 */ 677 public function setLoginHint($loginHint) 678 { 679 $this->config['login_hint'] = $loginHint; 680 } 681 682 /** 683 * Set the application name, this is included in the User-Agent HTTP header. 684 * @param string $applicationName 685 */ 686 public function setApplicationName($applicationName) 687 { 688 $this->config['application_name'] = $applicationName; 689 } 690 691 /** 692 * If 'plus.login' is included in the list of requested scopes, you can use 693 * this method to define types of app activities that your app will write. 694 * You can find a list of available types here: 695 * @link https://developers.google.com/+/api/moment-types 696 * 697 * @param array $requestVisibleActions Array of app activity types 698 */ 699 public function setRequestVisibleActions($requestVisibleActions) 700 { 701 if (is_array($requestVisibleActions)) { 702 $requestVisibleActions = implode(" ", $requestVisibleActions); 703 } 704 $this->config['request_visible_actions'] = $requestVisibleActions; 705 } 706 707 /** 708 * Set the developer key to use, these are obtained through the API Console. 709 * @see http://code.google.com/apis/console-help/#generatingdevkeys 710 * @param string $developerKey 711 */ 712 public function setDeveloperKey($developerKey) 713 { 714 $this->config['developer_key'] = $developerKey; 715 } 716 717 /** 718 * Set the hd (hosted domain) parameter streamlines the login process for 719 * Google Apps hosted accounts. By including the domain of the user, you 720 * restrict sign-in to accounts at that domain. 721 * @param $hd string - the domain to use. 722 */ 723 public function setHostedDomain($hd) 724 { 725 $this->config['hd'] = $hd; 726 } 727 728 /** 729 * Set the prompt hint. Valid values are none, consent and select_account. 730 * If no value is specified and the user has not previously authorized 731 * access, then the user is shown a consent screen. 732 * @param $prompt string 733 * {@code "none"} Do not display any authentication or consent screens. Must not be specified with other values. 734 * {@code "consent"} Prompt the user for consent. 735 * {@code "select_account"} Prompt the user to select an account. 736 */ 737 public function setPrompt($prompt) 738 { 739 $this->config['prompt'] = $prompt; 740 } 741 742 /** 743 * openid.realm is a parameter from the OpenID 2.0 protocol, not from OAuth 744 * 2.0. It is used in OpenID 2.0 requests to signify the URL-space for which 745 * an authentication request is valid. 746 * @param $realm string - the URL-space to use. 747 */ 748 public function setOpenidRealm($realm) 749 { 750 $this->config['openid.realm'] = $realm; 751 } 752 753 /** 754 * If this is provided with the value true, and the authorization request is 755 * granted, the authorization will include any previous authorizations 756 * granted to this user/application combination for other scopes. 757 * @param $include boolean - the URL-space to use. 758 */ 759 public function setIncludeGrantedScopes($include) 760 { 761 $this->config['include_granted_scopes'] = $include; 762 } 763 764 /** 765 * sets function to be called when an access token is fetched 766 * @param callable $tokenCallback - function ($cacheKey, $accessToken) 767 */ 768 public function setTokenCallback(callable $tokenCallback) 769 { 770 $this->config['token_callback'] = $tokenCallback; 771 } 772 773 /** 774 * Revoke an OAuth2 access token or refresh token. This method will revoke the current access 775 * token, if a token isn't provided. 776 * 777 * @param string|array|null $token The token (access token or a refresh token) that should be revoked. 778 * @return boolean Returns True if the revocation was successful, otherwise False. 779 */ 780 public function revokeToken($token = null) 781 { 782 $tokenRevoker = new Revoke($this->getHttpClient()); 783 784 return $tokenRevoker->revokeToken($token ?: $this->getAccessToken()); 785 } 786 787 /** 788 * Verify an id_token. This method will verify the current id_token, if one 789 * isn't provided. 790 * 791 * @throws LogicException If no token was provided and no token was set using `setAccessToken`. 792 * @throws UnexpectedValueException If the token is not a valid JWT. 793 * @param string|null $idToken The token (id_token) that should be verified. 794 * @return array|false Returns the token payload as an array if the verification was 795 * successful, false otherwise. 796 */ 797 public function verifyIdToken($idToken = null) 798 { 799 $tokenVerifier = new Verify( 800 $this->getHttpClient(), 801 $this->getCache(), 802 $this->config['jwt'] 803 ); 804 805 if (null === $idToken) { 806 $token = $this->getAccessToken(); 807 if (!isset($token['id_token'])) { 808 throw new LogicException( 809 'id_token must be passed in or set as part of setAccessToken' 810 ); 811 } 812 $idToken = $token['id_token']; 813 } 814 815 return $tokenVerifier->verifyIdToken( 816 $idToken, 817 $this->getClientId() 818 ); 819 } 820 821 /** 822 * Set the scopes to be requested. Must be called before createAuthUrl(). 823 * Will remove any previously configured scopes. 824 * @param string|array $scope_or_scopes, ie: 825 * array( 826 * 'https://www.googleapis.com/auth/plus.login', 827 * 'https://www.googleapis.com/auth/moderator' 828 * ); 829 */ 830 public function setScopes($scope_or_scopes) 831 { 832 $this->requestedScopes = array(); 833 $this->addScope($scope_or_scopes); 834 } 835 836 /** 837 * This functions adds a scope to be requested as part of the OAuth2.0 flow. 838 * Will append any scopes not previously requested to the scope parameter. 839 * A single string will be treated as a scope to request. An array of strings 840 * will each be appended. 841 * @param $scope_or_scopes string|array e.g. "profile" 842 */ 843 public function addScope($scope_or_scopes) 844 { 845 if (is_string($scope_or_scopes) && !in_array($scope_or_scopes, $this->requestedScopes)) { 846 $this->requestedScopes[] = $scope_or_scopes; 847 } else if (is_array($scope_or_scopes)) { 848 foreach ($scope_or_scopes as $scope) { 849 $this->addScope($scope); 850 } 851 } 852 } 853 854 /** 855 * Returns the list of scopes requested by the client 856 * @return array the list of scopes 857 * 858 */ 859 public function getScopes() 860 { 861 return $this->requestedScopes; 862 } 863 864 /** 865 * @return string|null 866 * @visible For Testing 867 */ 868 public function prepareScopes() 869 { 870 if (empty($this->requestedScopes)) { 871 return null; 872 } 873 874 return implode(' ', $this->requestedScopes); 875 } 876 877 /** 878 * Helper method to execute deferred HTTP requests. 879 * 880 * @param $request RequestInterface|\Google\Http\Batch 881 * @param string $expectedClass 882 * @throws \Google\Exception 883 * @return mixed|$expectedClass|ResponseInterface 884 */ 885 public function execute(RequestInterface $request, $expectedClass = null) 886 { 887 $request = $request 888 ->withHeader( 889 'User-Agent', 890 sprintf( 891 '%s %s%s', 892 $this->config['application_name'], 893 self::USER_AGENT_SUFFIX, 894 $this->getLibraryVersion() 895 ) 896 ) 897 ->withHeader( 898 'x-goog-api-client', 899 sprintf( 900 'gl-php/%s gdcl/%s', 901 phpversion(), 902 $this->getLibraryVersion() 903 ) 904 ); 905 906 if ($this->config['api_format_v2']) { 907 $request = $request->withHeader( 908 'X-GOOG-API-FORMAT-VERSION', 909 2 910 ); 911 } 912 913 // call the authorize method 914 // this is where most of the grunt work is done 915 $http = $this->authorize(); 916 917 return REST::execute( 918 $http, 919 $request, 920 $expectedClass, 921 $this->config['retry'], 922 $this->config['retry_map'] 923 ); 924 } 925 926 /** 927 * Declare whether batch calls should be used. This may increase throughput 928 * by making multiple requests in one connection. 929 * 930 * @param boolean $useBatch True if the batch support should 931 * be enabled. Defaults to False. 932 */ 933 public function setUseBatch($useBatch) 934 { 935 // This is actually an alias for setDefer. 936 $this->setDefer($useBatch); 937 } 938 939 /** 940 * Are we running in Google AppEngine? 941 * return bool 942 */ 943 public function isAppEngine() 944 { 945 return (isset($_SERVER['SERVER_SOFTWARE']) && 946 strpos($_SERVER['SERVER_SOFTWARE'], 'Google App Engine') !== false); 947 } 948 949 public function setConfig($name, $value) 950 { 951 $this->config[$name] = $value; 952 } 953 954 public function getConfig($name, $default = null) 955 { 956 return isset($this->config[$name]) ? $this->config[$name] : $default; 957 } 958 959 /** 960 * For backwards compatibility 961 * alias for setAuthConfig 962 * 963 * @param string $file the configuration file 964 * @throws \Google\Exception 965 * @deprecated 966 */ 967 public function setAuthConfigFile($file) 968 { 969 $this->setAuthConfig($file); 970 } 971 972 /** 973 * Set the auth config from new or deprecated JSON config. 974 * This structure should match the file downloaded from 975 * the "Download JSON" button on in the Google Developer 976 * Console. 977 * @param string|array $config the configuration json 978 * @throws \Google\Exception 979 */ 980 public function setAuthConfig($config) 981 { 982 if (is_string($config)) { 983 if (!file_exists($config)) { 984 throw new InvalidArgumentException(sprintf('file "%s" does not exist', $config)); 985 } 986 987 $json = file_get_contents($config); 988 989 if (!$config = json_decode($json, true)) { 990 throw new LogicException('invalid json for auth config'); 991 } 992 } 993 994 $key = isset($config['installed']) ? 'installed' : 'web'; 995 if (isset($config['type']) && $config['type'] == 'service_account') { 996 // application default credentials 997 $this->useApplicationDefaultCredentials(); 998 999 // set the information from the config 1000 $this->setClientId($config['client_id']); 1001 $this->config['client_email'] = $config['client_email']; 1002 $this->config['signing_key'] = $config['private_key']; 1003 $this->config['signing_algorithm'] = 'HS256'; 1004 } elseif (isset($config[$key])) { 1005 // old-style 1006 $this->setClientId($config[$key]['client_id']); 1007 $this->setClientSecret($config[$key]['client_secret']); 1008 if (isset($config[$key]['redirect_uris'])) { 1009 $this->setRedirectUri($config[$key]['redirect_uris'][0]); 1010 } 1011 } else { 1012 // new-style 1013 $this->setClientId($config['client_id']); 1014 $this->setClientSecret($config['client_secret']); 1015 if (isset($config['redirect_uris'])) { 1016 $this->setRedirectUri($config['redirect_uris'][0]); 1017 } 1018 } 1019 } 1020 1021 /** 1022 * Use when the service account has been delegated domain wide access. 1023 * 1024 * @param string $subject an email address account to impersonate 1025 */ 1026 public function setSubject($subject) 1027 { 1028 $this->config['subject'] = $subject; 1029 } 1030 1031 /** 1032 * Declare whether making API calls should make the call immediately, or 1033 * return a request which can be called with ->execute(); 1034 * 1035 * @param boolean $defer True if calls should not be executed right away. 1036 */ 1037 public function setDefer($defer) 1038 { 1039 $this->deferExecution = $defer; 1040 } 1041 1042 /** 1043 * Whether or not to return raw requests 1044 * @return boolean 1045 */ 1046 public function shouldDefer() 1047 { 1048 return $this->deferExecution; 1049 } 1050 1051 /** 1052 * @return OAuth2 implementation 1053 */ 1054 public function getOAuth2Service() 1055 { 1056 if (!isset($this->auth)) { 1057 $this->auth = $this->createOAuth2Service(); 1058 } 1059 1060 return $this->auth; 1061 } 1062 1063 /** 1064 * create a default google auth object 1065 */ 1066 protected function createOAuth2Service() 1067 { 1068 $auth = new OAuth2( 1069 [ 1070 'clientId' => $this->getClientId(), 1071 'clientSecret' => $this->getClientSecret(), 1072 'authorizationUri' => self::OAUTH2_AUTH_URL, 1073 'tokenCredentialUri' => self::OAUTH2_TOKEN_URI, 1074 'redirectUri' => $this->getRedirectUri(), 1075 'issuer' => $this->config['client_id'], 1076 'signingKey' => $this->config['signing_key'], 1077 'signingAlgorithm' => $this->config['signing_algorithm'], 1078 ] 1079 ); 1080 1081 return $auth; 1082 } 1083 1084 /** 1085 * Set the Cache object 1086 * @param CacheItemPoolInterface $cache 1087 */ 1088 public function setCache(CacheItemPoolInterface $cache) 1089 { 1090 $this->cache = $cache; 1091 } 1092 1093 /** 1094 * @return CacheItemPoolInterface 1095 */ 1096 public function getCache() 1097 { 1098 if (!$this->cache) { 1099 $this->cache = $this->createDefaultCache(); 1100 } 1101 1102 return $this->cache; 1103 } 1104 1105 /** 1106 * @param array $cacheConfig 1107 */ 1108 public function setCacheConfig(array $cacheConfig) 1109 { 1110 $this->config['cache_config'] = $cacheConfig; 1111 } 1112 1113 /** 1114 * Set the Logger object 1115 * @param LoggerInterface $logger 1116 */ 1117 public function setLogger(LoggerInterface $logger) 1118 { 1119 $this->logger = $logger; 1120 } 1121 1122 /** 1123 * @return LoggerInterface 1124 */ 1125 public function getLogger() 1126 { 1127 if (!isset($this->logger)) { 1128 $this->logger = $this->createDefaultLogger(); 1129 } 1130 1131 return $this->logger; 1132 } 1133 1134 protected function createDefaultLogger() 1135 { 1136 $logger = new Logger('google-api-php-client'); 1137 if ($this->isAppEngine()) { 1138 $handler = new MonologSyslogHandler('app', LOG_USER, Logger::NOTICE); 1139 } else { 1140 $handler = new MonologStreamHandler('php://stderr', Logger::NOTICE); 1141 } 1142 $logger->pushHandler($handler); 1143 1144 return $logger; 1145 } 1146 1147 protected function createDefaultCache() 1148 { 1149 return new MemoryCacheItemPool; 1150 } 1151 1152 /** 1153 * Set the Http Client object 1154 * @param ClientInterface $http 1155 */ 1156 public function setHttpClient(ClientInterface $http) 1157 { 1158 $this->http = $http; 1159 } 1160 1161 /** 1162 * @return ClientInterface 1163 */ 1164 public function getHttpClient() 1165 { 1166 if (null === $this->http) { 1167 $this->http = $this->createDefaultHttpClient(); 1168 } 1169 1170 return $this->http; 1171 } 1172 1173 /** 1174 * Set the API format version. 1175 * 1176 * `true` will use V2, which may return more useful error messages. 1177 * 1178 * @param bool $value 1179 */ 1180 public function setApiFormatV2($value) 1181 { 1182 $this->config['api_format_v2'] = (bool) $value; 1183 } 1184 1185 protected function createDefaultHttpClient() 1186 { 1187 $guzzleVersion = null; 1188 if (defined('\GuzzleHttp\ClientInterface::MAJOR_VERSION')) { 1189 $guzzleVersion = ClientInterface::MAJOR_VERSION; 1190 } elseif (defined('\GuzzleHttp\ClientInterface::VERSION')) { 1191 $guzzleVersion = (int)substr(ClientInterface::VERSION, 0, 1); 1192 } 1193 1194 if (5 === $guzzleVersion) { 1195 $options = [ 1196 'base_url' => $this->config['base_path'], 1197 'defaults' => ['exceptions' => false], 1198 ]; 1199 if ($this->isAppEngine()) { 1200 // set StreamHandler on AppEngine by default 1201 $options['handler'] = new StreamHandler(); 1202 $options['defaults']['verify'] = '/etc/ca-certificates.crt'; 1203 } 1204 } elseif (6 === $guzzleVersion || 7 === $guzzleVersion) { 1205 // guzzle 6 or 7 1206 $options = [ 1207 'base_uri' => $this->config['base_path'], 1208 'http_errors' => false, 1209 ]; 1210 } else { 1211 throw new LogicException('Could not find supported version of Guzzle.'); 1212 } 1213 1214 return new GuzzleClient($options); 1215 } 1216 1217 /** 1218 * @return FetchAuthTokenCache 1219 */ 1220 private function createApplicationDefaultCredentials() 1221 { 1222 $scopes = $this->prepareScopes(); 1223 $sub = $this->config['subject']; 1224 $signingKey = $this->config['signing_key']; 1225 1226 // create credentials using values supplied in setAuthConfig 1227 if ($signingKey) { 1228 $serviceAccountCredentials = array( 1229 'client_id' => $this->config['client_id'], 1230 'client_email' => $this->config['client_email'], 1231 'private_key' => $signingKey, 1232 'type' => 'service_account', 1233 'quota_project_id' => $this->config['quota_project'], 1234 ); 1235 $credentials = CredentialsLoader::makeCredentials( 1236 $scopes, 1237 $serviceAccountCredentials 1238 ); 1239 } else { 1240 // When $sub is provided, we cannot pass cache classes to ::getCredentials 1241 // because FetchAuthTokenCache::setSub does not exist. 1242 // The result is when $sub is provided, calls to ::onGce are not cached. 1243 $credentials = ApplicationDefaultCredentials::getCredentials( 1244 $scopes, 1245 null, 1246 $sub ? null : $this->config['cache_config'], 1247 $sub ? null : $this->getCache(), 1248 $this->config['quota_project'] 1249 ); 1250 } 1251 1252 // for service account domain-wide authority (impersonating a user) 1253 // @see https://developers.google.com/identity/protocols/OAuth2ServiceAccount 1254 if ($sub) { 1255 if (!$credentials instanceof ServiceAccountCredentials) { 1256 throw new DomainException('domain-wide authority requires service account credentials'); 1257 } 1258 1259 $credentials->setSub($sub); 1260 } 1261 1262 // If we are not using FetchAuthTokenCache yet, create it now 1263 if (!$credentials instanceof FetchAuthTokenCache) { 1264 $credentials = new FetchAuthTokenCache( 1265 $credentials, 1266 $this->config['cache_config'], 1267 $this->getCache() 1268 ); 1269 } 1270 return $credentials; 1271 } 1272 1273 protected function getAuthHandler() 1274 { 1275 // Be very careful using the cache, as the underlying auth library's cache 1276 // implementation is naive, and the cache keys do not account for user 1277 // sessions. 1278 // 1279 // @see https://github.com/google/google-api-php-client/issues/821 1280 return AuthHandlerFactory::build( 1281 $this->getCache(), 1282 $this->config['cache_config'] 1283 ); 1284 } 1285 1286 private function createUserRefreshCredentials($scope, $refreshToken) 1287 { 1288 $creds = array_filter( 1289 array( 1290 'client_id' => $this->getClientId(), 1291 'client_secret' => $this->getClientSecret(), 1292 'refresh_token' => $refreshToken, 1293 ) 1294 ); 1295 1296 return new UserRefreshCredentials($scope, $creds); 1297 } 1298} 1299