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