<?php
/*
 * Copyright 2010 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use Google\Auth\ApplicationDefaultCredentials;
use Google\Auth\Cache\MemoryCacheItemPool;
use Google\Auth\CredentialsLoader;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use Google\Auth\OAuth2;
use Google\Auth\Credentials\ServiceAccountCredentials;
use Google\Auth\Credentials\UserRefreshCredentials;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Ring\Client\StreamHandler;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Log\LoggerInterface;
use Monolog\Logger;
use Monolog\Handler\StreamHandler as MonologStreamHandler;
use Monolog\Handler\SyslogHandler as MonologSyslogHandler;

/**
 * The Google API Client
 * https://github.com/google/google-api-php-client
 */
class Google_Client
{
  const LIBVER = "2.2.2";
  const USER_AGENT_SUFFIX = "google-api-php-client/";
  const OAUTH2_REVOKE_URI = 'https://oauth2.googleapis.com/revoke';
  const OAUTH2_TOKEN_URI = 'https://oauth2.googleapis.com/token';
  const OAUTH2_AUTH_URL = 'https://accounts.google.com/o/oauth2/auth';
  const API_BASE_PATH = 'https://www.googleapis.com';

  /**
   * @var Google\Auth\OAuth2 $auth
   */
  private $auth;

  /**
   * @var GuzzleHttp\ClientInterface $http
   */
  private $http;

  /**
   * @var Psr\Cache\CacheItemPoolInterface $cache
   */
  private $cache;

  /**
   * @var array access token
   */
  private $token;

  /**
   * @var array $config
   */
  private $config;

  /**
   * @var Psr\Log\LoggerInterface $logger
   */
  private $logger;

  /**
   * @var boolean $deferExecution
   */
  private $deferExecution = false;

  /** @var array $scopes */
  // Scopes requested by the client
  protected $requestedScopes = [];

  /**
   * Construct the Google Client.
   *
   * @param array $config
   */
  public function __construct(array $config = array())
  {
    $this->config = array_merge(
        [
          'application_name' => '',

          // Don't change these unless you're working against a special development
          // or testing environment.
          'base_path' => self::API_BASE_PATH,

          // https://developers.google.com/console
          'client_id' => '',
          'client_secret' => '',
          'redirect_uri' => null,
          'state' => null,

          // Simple API access key, also from the API console. Ensure you get
          // a Server key, and not a Browser key.
          'developer_key' => '',

          // For use with Google Cloud Platform
          // fetch the ApplicationDefaultCredentials, if applicable
          // @see https://developers.google.com/identity/protocols/application-default-credentials
          'use_application_default_credentials' => false,
          'signing_key' => null,
          'signing_algorithm' => null,
          'subject' => null,

          // Other OAuth2 parameters.
          'hd' => '',
          'prompt' => '',
          'openid.realm' => '',
          'include_granted_scopes' => null,
          'login_hint' => '',
          'request_visible_actions' => '',
          'access_type' => 'online',
          'approval_prompt' => 'auto',

          // Task Runner retry configuration
          // @see Google_Task_Runner
          'retry' => array(),
          'retry_map' => null,

          // cache config for downstream auth caching
          'cache_config' => [],

          // function to be called when an access token is fetched
          // follows the signature function ($cacheKey, $accessToken)
          'token_callback' => null,

          // Service class used in Google_Client::verifyIdToken.
          // Explicitly pass this in to avoid setting JWT::$leeway
          'jwt' => null,
        ],
        $config
    );
  }

  /**
   * Get a string containing the version of the library.
   *
   * @return string
   */
  public function getLibraryVersion()
  {
    return self::LIBVER;
  }

  /**
   * For backwards compatibility
   * alias for fetchAccessTokenWithAuthCode
   *
   * @param $code string code from accounts.google.com
   * @return array access token
   * @deprecated
   */
  public function authenticate($code)
  {
    return $this->fetchAccessTokenWithAuthCode($code);
  }

  /**
   * Attempt to exchange a code for an valid authentication token.
   * Helper wrapped around the OAuth 2.0 implementation.
   *
   * @param $code string code from accounts.google.com
   * @return array access token
   */
  public function fetchAccessTokenWithAuthCode($code)
  {
    if (strlen($code) == 0) {
      throw new InvalidArgumentException("Invalid code");
    }

    $auth = $this->getOAuth2Service();
    $auth->setCode($code);
    $auth->setRedirectUri($this->getRedirectUri());

    $httpHandler = HttpHandlerFactory::build($this->getHttpClient());
    $creds = $auth->fetchAuthToken($httpHandler);
    if ($creds && isset($creds['access_token'])) {
      $creds['created'] = time();
      $this->setAccessToken($creds);
    }

    return $creds;
  }

  /**
   * For backwards compatibility
   * alias for fetchAccessTokenWithAssertion
   *
   * @return array access token
   * @deprecated
   */
  public function refreshTokenWithAssertion()
  {
    return $this->fetchAccessTokenWithAssertion();
  }

  /**
   * Fetches a fresh access token with a given assertion token.
   * @param ClientInterface $authHttp optional.
   * @return array access token
   */
  public function fetchAccessTokenWithAssertion(ClientInterface $authHttp = null)
  {
    if (!$this->isUsingApplicationDefaultCredentials()) {
      throw new DomainException(
          'set the JSON service account credentials using'
          . ' Google_Client::setAuthConfig or set the path to your JSON file'
          . ' with the "GOOGLE_APPLICATION_CREDENTIALS" environment variable'
          . ' and call Google_Client::useApplicationDefaultCredentials to'
          . ' refresh a token with assertion.'
      );
    }

    $this->getLogger()->log(
        'info',
        'OAuth2 access token refresh with Signed JWT assertion grants.'
    );

    $credentials = $this->createApplicationDefaultCredentials();

    $httpHandler = HttpHandlerFactory::build($authHttp);
    $creds = $credentials->fetchAuthToken($httpHandler);
    if ($creds && isset($creds['access_token'])) {
      $creds['created'] = time();
      $this->setAccessToken($creds);
    }

    return $creds;
  }

  /**
   * For backwards compatibility
   * alias for fetchAccessTokenWithRefreshToken
   *
   * @param string $refreshToken
   * @return array access token
   */
  public function refreshToken($refreshToken)
  {
    return $this->fetchAccessTokenWithRefreshToken($refreshToken);
  }

  /**
   * Fetches a fresh OAuth 2.0 access token with the given refresh token.
   * @param string $refreshToken
   * @return array access token
   */
  public function fetchAccessTokenWithRefreshToken($refreshToken = null)
  {
    if (null === $refreshToken) {
      if (!isset($this->token['refresh_token'])) {
        throw new LogicException(
            'refresh token must be passed in or set as part of setAccessToken'
        );
      }
      $refreshToken = $this->token['refresh_token'];
    }
    $this->getLogger()->info('OAuth2 access token refresh');
    $auth = $this->getOAuth2Service();
    $auth->setRefreshToken($refreshToken);

    $httpHandler = HttpHandlerFactory::build($this->getHttpClient());
    $creds = $auth->fetchAuthToken($httpHandler);
    if ($creds && isset($creds['access_token'])) {
      $creds['created'] = time();
      if (!isset($creds['refresh_token'])) {
        $creds['refresh_token'] = $refreshToken;
      }
      $this->setAccessToken($creds);
    }

    return $creds;
  }

  /**
   * Create a URL to obtain user authorization.
   * The authorization endpoint allows the user to first
   * authenticate, and then grant/deny the access request.
   * @param string|array $scope The scope is expressed as an array or list of space-delimited strings.
   * @return string
   */
  public function createAuthUrl($scope = null)
  {
    if (empty($scope)) {
      $scope = $this->prepareScopes();
    }
    if (is_array($scope)) {
      $scope = implode(' ', $scope);
    }

    // only accept one of prompt or approval_prompt
    $approvalPrompt = $this->config['prompt']
      ? null
      : $this->config['approval_prompt'];

    // include_granted_scopes should be string "true", string "false", or null
    $includeGrantedScopes = $this->config['include_granted_scopes'] === null
      ? null
      : var_export($this->config['include_granted_scopes'], true);

    $params = array_filter(
        [
          'access_type' => $this->config['access_type'],
          'approval_prompt' => $approvalPrompt,
          'hd' => $this->config['hd'],
          'include_granted_scopes' => $includeGrantedScopes,
          'login_hint' => $this->config['login_hint'],
          'openid.realm' => $this->config['openid.realm'],
          'prompt' => $this->config['prompt'],
          'response_type' => 'code',
          'scope' => $scope,
          'state' => $this->config['state'],
        ]
    );

    // If the list of scopes contains plus.login, add request_visible_actions
    // to auth URL.
    $rva = $this->config['request_visible_actions'];
    if (strlen($rva) > 0 && false !== strpos($scope, 'plus.login')) {
        $params['request_visible_actions'] = $rva;
    }

    $auth = $this->getOAuth2Service();

    return (string) $auth->buildFullAuthorizationUri($params);
  }

  /**
   * Adds auth listeners to the HTTP client based on the credentials
   * set in the Google API Client object
   *
   * @param GuzzleHttp\ClientInterface $http the http client object.
   * @return GuzzleHttp\ClientInterface the http client object
   */
  public function authorize(ClientInterface $http = null)
  {
    $credentials = null;
    $token = null;
    $scopes = null;
    if (null === $http) {
      $http = $this->getHttpClient();
    }

    // These conditionals represent the decision tree for authentication
    //   1.  Check for Application Default Credentials
    //   2.  Check for API Key
    //   3a. Check for an Access Token
    //   3b. If access token exists but is expired, try to refresh it
    if ($this->isUsingApplicationDefaultCredentials()) {
      $credentials = $this->createApplicationDefaultCredentials();
    } elseif ($token = $this->getAccessToken()) {
      $scopes = $this->prepareScopes();
      // add refresh subscriber to request a new token
      if (isset($token['refresh_token']) && $this->isAccessTokenExpired()) {
        $credentials = $this->createUserRefreshCredentials(
            $scopes,
            $token['refresh_token']
        );
      }
    }

    $authHandler = $this->getAuthHandler();

    if ($credentials) {
      $callback = $this->config['token_callback'];
      $http = $authHandler->attachCredentials($http, $credentials, $callback);
    } elseif ($token) {
      $http = $authHandler->attachToken($http, $token, (array) $scopes);
    } elseif ($key = $this->config['developer_key']) {
      $http = $authHandler->attachKey($http, $key);
    }

    return $http;
  }

  /**
   * Set the configuration to use application default credentials for
   * authentication
   *
   * @see https://developers.google.com/identity/protocols/application-default-credentials
   * @param boolean $useAppCreds
   */
  public function useApplicationDefaultCredentials($useAppCreds = true)
  {
    $this->config['use_application_default_credentials'] = $useAppCreds;
  }

  /**
   * To prevent useApplicationDefaultCredentials from inappropriately being
   * called in a conditional
   *
   * @see https://developers.google.com/identity/protocols/application-default-credentials
   */
  public function isUsingApplicationDefaultCredentials()
  {
    return $this->config['use_application_default_credentials'];
  }

  /**
   * @param string|array $token
   * @throws InvalidArgumentException
   */
  public function setAccessToken($token)
  {
    if (is_string($token)) {
      if ($json = json_decode($token, true)) {
        $token = $json;
      } else {
        // assume $token is just the token string
        $token = array(
          'access_token' => $token,
        );
      }
    }
    if ($token == null) {
      throw new InvalidArgumentException('invalid json token');
    }
    if (!isset($token['access_token'])) {
      throw new InvalidArgumentException("Invalid token format");
    }
    $this->token = $token;
  }

  public function getAccessToken()
  {
    return $this->token;
  }

  /**
   * @return string|null
   */
  public function getRefreshToken()
  {
    if (isset($this->token['refresh_token'])) {
      return $this->token['refresh_token'];
    }

    return null;
  }

  /**
   * Returns if the access_token is expired.
   * @return bool Returns True if the access_token is expired.
   */
  public function isAccessTokenExpired()
  {
    if (!$this->token) {
      return true;
    }

    $created = 0;
    if (isset($this->token['created'])) {
      $created = $this->token['created'];
    } elseif (isset($this->token['id_token'])) {
      // check the ID token for "iat"
      // signature verification is not required here, as we are just
      // using this for convenience to save a round trip request
      // to the Google API server
      $idToken = $this->token['id_token'];
      if (substr_count($idToken, '.') == 2) {
        $parts = explode('.', $idToken);
        $payload = json_decode(base64_decode($parts[1]), true);
        if ($payload && isset($payload['iat'])) {
          $created = $payload['iat'];
        }
      }
    }

    // If the token is set to expire in the next 30 seconds.
    return ($created + ($this->token['expires_in'] - 30)) < time();
  }

  /**
   * @deprecated See UPGRADING.md for more information
   */
  public function getAuth()
  {
    throw new BadMethodCallException(
        'This function no longer exists. See UPGRADING.md for more information'
    );
  }

  /**
   * @deprecated See UPGRADING.md for more information
   */
  public function setAuth($auth)
  {
    throw new BadMethodCallException(
        'This function no longer exists. See UPGRADING.md for more information'
    );
  }

  /**
   * Set the OAuth 2.0 Client ID.
   * @param string $clientId
   */
  public function setClientId($clientId)
  {
    $this->config['client_id'] = $clientId;
  }

  public function getClientId()
  {
    return $this->config['client_id'];
  }

  /**
   * Set the OAuth 2.0 Client Secret.
   * @param string $clientSecret
   */
  public function setClientSecret($clientSecret)
  {
    $this->config['client_secret'] = $clientSecret;
  }

  public function getClientSecret()
  {
    return $this->config['client_secret'];
  }

  /**
   * Set the OAuth 2.0 Redirect URI.
   * @param string $redirectUri
   */
  public function setRedirectUri($redirectUri)
  {
    $this->config['redirect_uri'] = $redirectUri;
  }

  public function getRedirectUri()
  {
    return $this->config['redirect_uri'];
  }

  /**
   * Set OAuth 2.0 "state" parameter to achieve per-request customization.
   * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-3.1.2.2
   * @param string $state
   */
  public function setState($state)
  {
    $this->config['state'] = $state;
  }

  /**
   * @param string $accessType Possible values for access_type include:
   *  {@code "offline"} to request offline access from the user.
   *  {@code "online"} to request online access from the user.
   */
  public function setAccessType($accessType)
  {
    $this->config['access_type'] = $accessType;
  }

  /**
   * @param string $approvalPrompt Possible values for approval_prompt include:
   *  {@code "force"} to force the approval UI to appear.
   *  {@code "auto"} to request auto-approval when possible. (This is the default value)
   */
  public function setApprovalPrompt($approvalPrompt)
  {
    $this->config['approval_prompt'] = $approvalPrompt;
  }

  /**
   * Set the login hint, email address or sub id.
   * @param string $loginHint
   */
  public function setLoginHint($loginHint)
  {
    $this->config['login_hint'] = $loginHint;
  }

  /**
   * Set the application name, this is included in the User-Agent HTTP header.
   * @param string $applicationName
   */
  public function setApplicationName($applicationName)
  {
    $this->config['application_name'] = $applicationName;
  }

  /**
   * If 'plus.login' is included in the list of requested scopes, you can use
   * this method to define types of app activities that your app will write.
   * You can find a list of available types here:
   * @link https://developers.google.com/+/api/moment-types
   *
   * @param array $requestVisibleActions Array of app activity types
   */
  public function setRequestVisibleActions($requestVisibleActions)
  {
    if (is_array($requestVisibleActions)) {
      $requestVisibleActions = implode(" ", $requestVisibleActions);
    }
    $this->config['request_visible_actions'] = $requestVisibleActions;
  }

  /**
   * Set the developer key to use, these are obtained through the API Console.
   * @see http://code.google.com/apis/console-help/#generatingdevkeys
   * @param string $developerKey
   */
  public function setDeveloperKey($developerKey)
  {
    $this->config['developer_key'] = $developerKey;
  }

  /**
   * Set the hd (hosted domain) parameter streamlines the login process for
   * Google Apps hosted accounts. By including the domain of the user, you
   * restrict sign-in to accounts at that domain.
   * @param $hd string - the domain to use.
   */
  public function setHostedDomain($hd)
  {
    $this->config['hd'] = $hd;
  }

  /**
   * Set the prompt hint. Valid values are none, consent and select_account.
   * If no value is specified and the user has not previously authorized
   * access, then the user is shown a consent screen.
   * @param $prompt string
   *  {@code "none"} Do not display any authentication or consent screens. Must not be specified with other values.
   *  {@code "consent"} Prompt the user for consent.
   *  {@code "select_account"} Prompt the user to select an account.
   */
  public function setPrompt($prompt)
  {
    $this->config['prompt'] = $prompt;
  }

  /**
   * openid.realm is a parameter from the OpenID 2.0 protocol, not from OAuth
   * 2.0. It is used in OpenID 2.0 requests to signify the URL-space for which
   * an authentication request is valid.
   * @param $realm string - the URL-space to use.
   */
  public function setOpenidRealm($realm)
  {
    $this->config['openid.realm'] = $realm;
  }

  /**
   * If this is provided with the value true, and the authorization request is
   * granted, the authorization will include any previous authorizations
   * granted to this user/application combination for other scopes.
   * @param $include boolean - the URL-space to use.
   */
  public function setIncludeGrantedScopes($include)
  {
    $this->config['include_granted_scopes'] = $include;
  }

  /**
   * sets function to be called when an access token is fetched
   * @param callable $tokenCallback - function ($cacheKey, $accessToken)
   */
  public function setTokenCallback(callable $tokenCallback)
  {
    $this->config['token_callback'] = $tokenCallback;
  }

  /**
   * Revoke an OAuth2 access token or refresh token. This method will revoke the current access
   * token, if a token isn't provided.
   *
   * @param string|array|null $token The token (access token or a refresh token) that should be revoked.
   * @return boolean Returns True if the revocation was successful, otherwise False.
   */
  public function revokeToken($token = null)
  {
    $tokenRevoker = new Google_AccessToken_Revoke(
        $this->getHttpClient()
    );

    return $tokenRevoker->revokeToken($token ?: $this->getAccessToken());
  }

  /**
   * Verify an id_token. This method will verify the current id_token, if one
   * isn't provided.
   *
   * @throws LogicException If no token was provided and no token was set using `setAccessToken`.
   * @throws UnexpectedValueException If the token is not a valid JWT.
   * @param string|null $idToken The token (id_token) that should be verified.
   * @return array|false Returns the token payload as an array if the verification was
   * successful, false otherwise.
   */
  public function verifyIdToken($idToken = null)
  {
    $tokenVerifier = new Google_AccessToken_Verify(
        $this->getHttpClient(),
        $this->getCache(),
        $this->config['jwt']
    );

    if (null === $idToken) {
      $token = $this->getAccessToken();
      if (!isset($token['id_token'])) {
        throw new LogicException(
            'id_token must be passed in or set as part of setAccessToken'
        );
      }
      $idToken = $token['id_token'];
    }

    return $tokenVerifier->verifyIdToken(
        $idToken,
        $this->getClientId()
    );
  }

  /**
   * Set the scopes to be requested. Must be called before createAuthUrl().
   * Will remove any previously configured scopes.
   * @param string|array $scope_or_scopes, ie: array('https://www.googleapis.com/auth/plus.login',
   * 'https://www.googleapis.com/auth/moderator')
   */
  public function setScopes($scope_or_scopes)
  {
    $this->requestedScopes = array();
    $this->addScope($scope_or_scopes);
  }

  /**
   * This functions adds a scope to be requested as part of the OAuth2.0 flow.
   * Will append any scopes not previously requested to the scope parameter.
   * A single string will be treated as a scope to request. An array of strings
   * will each be appended.
   * @param $scope_or_scopes string|array e.g. "profile"
   */
  public function addScope($scope_or_scopes)
  {
    if (is_string($scope_or_scopes) && !in_array($scope_or_scopes, $this->requestedScopes)) {
      $this->requestedScopes[] = $scope_or_scopes;
    } else if (is_array($scope_or_scopes)) {
      foreach ($scope_or_scopes as $scope) {
        $this->addScope($scope);
      }
    }
  }

  /**
   * Returns the list of scopes requested by the client
   * @return array the list of scopes
   *
   */
  public function getScopes()
  {
     return $this->requestedScopes;
  }

  /**
   * @return string|null
   * @visible For Testing
   */
  public function prepareScopes()
  {
    if (empty($this->requestedScopes)) {
      return null;
    }

    return implode(' ', $this->requestedScopes);
  }

  /**
   * Helper method to execute deferred HTTP requests.
   *
   * @param $request Psr\Http\Message\RequestInterface|Google_Http_Batch
   * @throws Google_Exception
   * @return object of the type of the expected class or Psr\Http\Message\ResponseInterface.
   */
  public function execute(RequestInterface $request, $expectedClass = null)
  {
    $request = $request->withHeader(
        'User-Agent',
        $this->config['application_name']
        . " " . self::USER_AGENT_SUFFIX
        . $this->getLibraryVersion()
    );

    // call the authorize method
    // this is where most of the grunt work is done
    $http = $this->authorize();

    return Google_Http_REST::execute(
        $http,
        $request,
        $expectedClass,
        $this->config['retry'],
        $this->config['retry_map']
    );
  }

  /**
   * Declare whether batch calls should be used. This may increase throughput
   * by making multiple requests in one connection.
   *
   * @param boolean $useBatch True if the batch support should
   * be enabled. Defaults to False.
   */
  public function setUseBatch($useBatch)
  {
    // This is actually an alias for setDefer.
    $this->setDefer($useBatch);
  }

  /**
   * Are we running in Google AppEngine?
   * return bool
   */
  public function isAppEngine()
  {
    return (isset($_SERVER['SERVER_SOFTWARE']) &&
        strpos($_SERVER['SERVER_SOFTWARE'], 'Google App Engine') !== false);
  }

  public function setConfig($name, $value)
  {
    $this->config[$name] = $value;
  }

  public function getConfig($name, $default = null)
  {
    return isset($this->config[$name]) ? $this->config[$name] : $default;
  }

  /**
   * For backwards compatibility
   * alias for setAuthConfig
   *
   * @param string $file the configuration file
   * @throws Google_Exception
   * @deprecated
   */
  public function setAuthConfigFile($file)
  {
    $this->setAuthConfig($file);
  }

  /**
   * Set the auth config from new or deprecated JSON config.
   * This structure should match the file downloaded from
   * the "Download JSON" button on in the Google Developer
   * Console.
   * @param string|array $config the configuration json
   * @throws Google_Exception
   */
  public function setAuthConfig($config)
  {
    if (is_string($config)) {
      if (!file_exists($config)) {
        throw new InvalidArgumentException(sprintf('file "%s" does not exist', $config));
      }

      $json = file_get_contents($config);

      if (!$config = json_decode($json, true)) {
        throw new LogicException('invalid json for auth config');
      }
    }

    $key = isset($config['installed']) ? 'installed' : 'web';
    if (isset($config['type']) && $config['type'] == 'service_account') {
      // application default credentials
      $this->useApplicationDefaultCredentials();

      // set the information from the config
      $this->setClientId($config['client_id']);
      $this->config['client_email'] = $config['client_email'];
      $this->config['signing_key'] = $config['private_key'];
      $this->config['signing_algorithm'] = 'HS256';
    } elseif (isset($config[$key])) {
      // old-style
      $this->setClientId($config[$key]['client_id']);
      $this->setClientSecret($config[$key]['client_secret']);
      if (isset($config[$key]['redirect_uris'])) {
        $this->setRedirectUri($config[$key]['redirect_uris'][0]);
      }
    } else {
      // new-style
      $this->setClientId($config['client_id']);
      $this->setClientSecret($config['client_secret']);
      if (isset($config['redirect_uris'])) {
        $this->setRedirectUri($config['redirect_uris'][0]);
      }
    }
  }

  /**
   * Use when the service account has been delegated domain wide access.
   *
   * @param string $subject an email address account to impersonate
   */
  public function setSubject($subject)
  {
    $this->config['subject'] = $subject;
  }

  /**
   * Declare whether making API calls should make the call immediately, or
   * return a request which can be called with ->execute();
   *
   * @param boolean $defer True if calls should not be executed right away.
   */
  public function setDefer($defer)
  {
    $this->deferExecution = $defer;
  }

  /**
   * Whether or not to return raw requests
   * @return boolean
   */
  public function shouldDefer()
  {
    return $this->deferExecution;
  }

  /**
   * @return Google\Auth\OAuth2 implementation
   */
  public function getOAuth2Service()
  {
    if (!isset($this->auth)) {
      $this->auth = $this->createOAuth2Service();
    }

    return $this->auth;
  }

  /**
   * create a default google auth object
   */
  protected function createOAuth2Service()
  {
    $auth = new OAuth2(
        [
          'clientId'          => $this->getClientId(),
          'clientSecret'      => $this->getClientSecret(),
          'authorizationUri'   => self::OAUTH2_AUTH_URL,
          'tokenCredentialUri' => self::OAUTH2_TOKEN_URI,
          'redirectUri'       => $this->getRedirectUri(),
          'issuer'            => $this->config['client_id'],
          'signingKey'        => $this->config['signing_key'],
          'signingAlgorithm'  => $this->config['signing_algorithm'],
        ]
    );

    return $auth;
  }

  /**
   * Set the Cache object
   * @param Psr\Cache\CacheItemPoolInterface $cache
   */
  public function setCache(CacheItemPoolInterface $cache)
  {
    $this->cache = $cache;
  }

  /**
   * @return Psr\Cache\CacheItemPoolInterface Cache implementation
   */
  public function getCache()
  {
    if (!$this->cache) {
      $this->cache = $this->createDefaultCache();
    }

    return $this->cache;
  }

  /**
   * @param array $cacheConfig
   */
  public function setCacheConfig(array $cacheConfig)
  {
    $this->config['cache_config'] = $cacheConfig;
  }

  /**
   * Set the Logger object
   * @param Psr\Log\LoggerInterface $logger
   */
  public function setLogger(LoggerInterface $logger)
  {
    $this->logger = $logger;
  }

  /**
   * @return Psr\Log\LoggerInterface implementation
   */
  public function getLogger()
  {
    if (!isset($this->logger)) {
      $this->logger = $this->createDefaultLogger();
    }

    return $this->logger;
  }

  protected function createDefaultLogger()
  {
    $logger = new Logger('google-api-php-client');
    if ($this->isAppEngine()) {
      $handler = new MonologSyslogHandler('app', LOG_USER, Logger::NOTICE);
    } else {
      $handler = new MonologStreamHandler('php://stderr', Logger::NOTICE);
    }
    $logger->pushHandler($handler);

    return $logger;
  }

  protected function createDefaultCache()
  {
    return new MemoryCacheItemPool;
  }

  /**
   * Set the Http Client object
   * @param GuzzleHttp\ClientInterface $http
   */
  public function setHttpClient(ClientInterface $http)
  {
    $this->http = $http;
  }

  /**
   * @return GuzzleHttp\ClientInterface implementation
   */
  public function getHttpClient()
  {
    if (null === $this->http) {
      $this->http = $this->createDefaultHttpClient();
    }

    return $this->http;
  }

  protected function createDefaultHttpClient()
  {
    $options = ['exceptions' => false];

    $version = ClientInterface::VERSION;
    if ('5' === $version[0]) {
      $options = [
        'base_url' => $this->config['base_path'],
        'defaults' => $options,
      ];
      if ($this->isAppEngine()) {
        // set StreamHandler on AppEngine by default
        $options['handler']  = new StreamHandler();
        $options['defaults']['verify'] = '/etc/ca-certificates.crt';
      }
    } else {
      // guzzle 6
      $options['base_uri'] = $this->config['base_path'];
    }

    return new Client($options);
  }

  private function createApplicationDefaultCredentials()
  {
    $scopes = $this->prepareScopes();
    $sub = $this->config['subject'];
    $signingKey = $this->config['signing_key'];

    // create credentials using values supplied in setAuthConfig
    if ($signingKey) {
      $serviceAccountCredentials = array(
        'client_id' => $this->config['client_id'],
        'client_email' => $this->config['client_email'],
        'private_key' => $signingKey,
        'type' => 'service_account',
      );
      $credentials = CredentialsLoader::makeCredentials($scopes, $serviceAccountCredentials);
    } else {
      $credentials = ApplicationDefaultCredentials::getCredentials($scopes);
    }

    // for service account domain-wide authority (impersonating a user)
    // @see https://developers.google.com/identity/protocols/OAuth2ServiceAccount
    if ($sub) {
      if (!$credentials instanceof ServiceAccountCredentials) {
        throw new DomainException('domain-wide authority requires service account credentials');
      }

      $credentials->setSub($sub);
    }

    return $credentials;
  }

  protected function getAuthHandler()
  {
    // Be very careful using the cache, as the underlying auth library's cache
    // implementation is naive, and the cache keys do not account for user
    // sessions.
    //
    // @see https://github.com/google/google-api-php-client/issues/821
    return Google_AuthHandler_AuthHandlerFactory::build(
        $this->getCache(),
        $this->config['cache_config']
    );
  }

  private function createUserRefreshCredentials($scope, $refreshToken)
  {
    $creds = array_filter(
        array(
          'client_id' => $this->getClientId(),
          'client_secret' => $this->getClientSecret(),
          'refresh_token' => $refreshToken,
        )
    );

    return new UserRefreshCredentials($scope, $creds);
  }
}


<?php
/*
 * Copyright 2014 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * A task runner with exponential backoff support.
 *
 * @see https://developers.google.com/drive/web/handle-errors#implementing_exponential_backoff
 */
class Google_Task_Runner
{
  const TASK_RETRY_NEVER = 0;
  const TASK_RETRY_ONCE = 1;
  const TASK_RETRY_ALWAYS = -1;

  /**
   * @var integer $maxDelay The max time (in seconds) to wait before a retry.
   */
  private $maxDelay = 60;
  /**
   * @var integer $delay The previous delay from which the next is calculated.
   */
  private $delay = 1;

  /**
   * @var integer $factor The base number for the exponential back off.
   */
  private $factor = 2;
  /**
   * @var float $jitter A random number between -$jitter and $jitter will be
   * added to $factor on each iteration to allow for a better distribution of
   * retries.
   */
  private $jitter = 0.5;

  /**
   * @var integer $attempts The number of attempts that have been tried so far.
   */
  private $attempts = 0;
  /**
   * @var integer $maxAttempts The max number of attempts allowed.
   */
  private $maxAttempts = 1;

  /**
   * @var callable $action The task to run and possibly retry.
   */
  private $action;
  /**
   * @var array $arguments The task arguments.
   */
  private $arguments;

  /**
   * @var array $retryMap Map of errors with retry counts.
   */
  protected $retryMap = [
    '500' => self::TASK_RETRY_ALWAYS,
    '503' => self::TASK_RETRY_ALWAYS,
    'rateLimitExceeded' => self::TASK_RETRY_ALWAYS,
    'userRateLimitExceeded' => self::TASK_RETRY_ALWAYS,
    6  => self::TASK_RETRY_ALWAYS,  // CURLE_COULDNT_RESOLVE_HOST
    7  => self::TASK_RETRY_ALWAYS,  // CURLE_COULDNT_CONNECT
    28 => self::TASK_RETRY_ALWAYS,  // CURLE_OPERATION_TIMEOUTED
    35 => self::TASK_RETRY_ALWAYS,  // CURLE_SSL_CONNECT_ERROR
    52 => self::TASK_RETRY_ALWAYS   // CURLE_GOT_NOTHING
  ];

  /**
   * Creates a new task runner with exponential backoff support.
   *
   * @param array $config The task runner config
   * @param string $name The name of the current task (used for logging)
   * @param callable $action The task to run and possibly retry
   * @param array $arguments The task arguments
   * @throws Google_Task_Exception when misconfigured
   */
  public function __construct(
      $config,
      $name,
      $action,
      array $arguments = array()
  ) {
    if (isset($config['initial_delay'])) {
      if ($config['initial_delay'] < 0) {
        throw new Google_Task_Exception(
            'Task configuration `initial_delay` must not be negative.'
        );
      }

      $this->delay = $config['initial_delay'];
    }

    if (isset($config['max_delay'])) {
      if ($config['max_delay'] <= 0) {
        throw new Google_Task_Exception(
            'Task configuration `max_delay` must be greater than 0.'
        );
      }

      $this->maxDelay = $config['max_delay'];
    }

    if (isset($config['factor'])) {
      if ($config['factor'] <= 0) {
        throw new Google_Task_Exception(
            'Task configuration `factor` must be greater than 0.'
        );
      }

      $this->factor = $config['factor'];
    }

    if (isset($config['jitter'])) {
      if ($config['jitter'] <= 0) {
        throw new Google_Task_Exception(
            'Task configuration `jitter` must be greater than 0.'
        );
      }

      $this->jitter = $config['jitter'];
    }

    if (isset($config['retries'])) {
      if ($config['retries'] < 0) {
        throw new Google_Task_Exception(
            'Task configuration `retries` must not be negative.'
        );
      }
      $this->maxAttempts += $config['retries'];
    }

    if (!is_callable($action)) {
        throw new Google_Task_Exception(
            'Task argument `$action` must be a valid callable.'
        );
    }

    $this->action = $action;
    $this->arguments = $arguments;
  }

  /**
   * Checks if a retry can be attempted.
   *
   * @return boolean
   */
  public function canAttempt()
  {
    return $this->attempts < $this->maxAttempts;
  }

  /**
   * Runs the task and (if applicable) automatically retries when errors occur.
   *
   * @return mixed
   * @throws Google_Task_Retryable on failure when no retries are available.
   */
  public function run()
  {
    while ($this->attempt()) {
      try {
        return call_user_func_array($this->action, $this->arguments);
      } catch (Google_Service_Exception $exception) {
        $allowedRetries = $this->allowedRetries(
            $exception->getCode(),
            $exception->getErrors()
        );

        if (!$this->canAttempt() || !$allowedRetries) {
          throw $exception;
        }

        if ($allowedRetries > 0) {
          $this->maxAttempts = min(
              $this->maxAttempts,
              $this->attempts + $allowedRetries
          );
        }
      }
    }
  }

  /**
   * Runs a task once, if possible. This is useful for bypassing the `run()`
   * loop.
   *
   * NOTE: If this is not the first attempt, this function will sleep in
   * accordance to the backoff configurations before running the task.
   *
   * @return boolean
   */
  public function attempt()
  {
    if (!$this->canAttempt()) {
      return false;
    }

    if ($this->attempts > 0) {
      $this->backOff();
    }

    $this->attempts++;
    return true;
  }

  /**
   * Sleeps in accordance to the backoff configurations.
   */
  private function backOff()
  {
    $delay = $this->getDelay();

    usleep($delay * 1000000);
  }

  /**
   * Gets the delay (in seconds) for the current backoff period.
   *
   * @return float
   */
  private function getDelay()
  {
    $jitter = $this->getJitter();
    $factor = $this->attempts > 1 ? $this->factor + $jitter : 1 + abs($jitter);

    return $this->delay = min($this->maxDelay, $this->delay * $factor);
  }

  /**
   * Gets the current jitter (random number between -$this->jitter and
   * $this->jitter).
   *
   * @return float
   */
  private function getJitter()
  {
    return $this->jitter * 2 * mt_rand() / mt_getrandmax() - $this->jitter;
  }

  /**
   * Gets the number of times the associated task can be retried.
   *
   * NOTE: -1 is returned if the task can be retried indefinitely
   *
   * @return integer
   */
  public function allowedRetries($code, $errors = array())
  {
    if (isset($this->retryMap[$code])) {
      return $this->retryMap[$code];
    }

    if (
        !empty($errors) &&
        isset($errors[0]['reason'], $this->retryMap[$errors[0]['reason']])
    ) {
      return $this->retryMap[$errors[0]['reason']];
    }

    return 0;
  }

  public function setRetryMap($retryMap)
  {
    $this->retryMap = $retryMap;
  }
}


<?php
/**
 * Copyright 2012 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Uri;
use Psr\Http\Message\RequestInterface;

/**
 * Manage large file uploads, which may be media but can be any type
 * of sizable data.
 */
class Google_Http_MediaFileUpload
{
  const UPLOAD_MEDIA_TYPE = 'media';
  const UPLOAD_MULTIPART_TYPE = 'multipart';
  const UPLOAD_RESUMABLE_TYPE = 'resumable';

  /** @var string $mimeType */
  private $mimeType;

  /** @var string $data */
  private $data;

  /** @var bool $resumable */
  private $resumable;

  /** @var int $chunkSize */
  private $chunkSize;

  /** @var int $size */
  private $size;

  /** @var string $resumeUri */
  private $resumeUri;

  /** @var int $progress */
  private $progress;

  /** @var Google_Client */
  private $client;

  /** @var Psr\Http\Message\RequestInterface */
  private $request;

  /** @var string */
  private $boundary;

  /**
   * Result code from last HTTP call
   * @var int
   */
  private $httpResultCode;

  /**
   * @param $mimeType string
   * @param $data string The bytes you want to upload.
   * @param $resumable bool
   * @param bool $chunkSize File will be uploaded in chunks of this many bytes.
   * only used if resumable=True
   */
  public function __construct(
      Google_Client $client,
      RequestInterface $request,
      $mimeType,
      $data,
      $resumable = false,
      $chunkSize = false
  ) {
    $this->client = $client;
    $this->request = $request;
    $this->mimeType = $mimeType;
    $this->data = $data;
    $this->resumable = $resumable;
    $this->chunkSize = $chunkSize;
    $this->progress = 0;

    $this->process();
  }

  /**
   * Set the size of the file that is being uploaded.
   * @param $size - int file size in bytes
   */
  public function setFileSize($size)
  {
    $this->size = $size;
  }

  /**
   * Return the progress on the upload
   * @return int progress in bytes uploaded.
   */
  public function getProgress()
  {
    return $this->progress;
  }

  /**
   * Send the next part of the file to upload.
   * @param [$chunk] the next set of bytes to send. If false will used $data passed
   * at construct time.
   */
  public function nextChunk($chunk = false)
  {
    $resumeUri = $this->getResumeUri();

    if (false == $chunk) {
      $chunk = substr($this->data, $this->progress, $this->chunkSize);
    }

    $lastBytePos = $this->progress + strlen($chunk) - 1;
    $headers = array(
      'content-range' => "bytes $this->progress-$lastBytePos/$this->size",
      'content-length' => strlen($chunk),
      'expect' => '',
    );

    $request = new Request(
        'PUT',
        $resumeUri,
        $headers,
        Psr7\stream_for($chunk)
    );

    return $this->makePutRequest($request);
  }

  /**
   * Return the HTTP result code from the last call made.
   * @return int code
   */
  public function getHttpResultCode()
  {
    return $this->httpResultCode;
  }

  /**
  * Sends a PUT-Request to google drive and parses the response,
  * setting the appropiate variables from the response()
  *
  * @param Google_Http_Request $httpRequest the Reuqest which will be send
  *
  * @return false|mixed false when the upload is unfinished or the decoded http response
  *
  */
  private function makePutRequest(RequestInterface $request)
  {
    $response = $this->client->execute($request);
    $this->httpResultCode = $response->getStatusCode();

    if (308 == $this->httpResultCode) {
      // Track the amount uploaded.
      $range = $response->getHeaderLine('range');
      if ($range) {
        $range_array = explode('-', $range);
        $this->progress = $range_array[1] + 1;
      }

      // Allow for changing upload URLs.
      $location = $response->getHeaderLine('location');
      if ($location) {
        $this->resumeUri = $location;
      }

      // No problems, but upload not complete.
      return false;
    }

    return Google_Http_REST::decodeHttpResponse($response, $this->request);
  }

  /**
   * Resume a previously unfinished upload
   * @param $resumeUri the resume-URI of the unfinished, resumable upload.
   */
  public function resume($resumeUri)
  {
     $this->resumeUri = $resumeUri;
     $headers = array(
       'content-range' => "bytes */$this->size",
       'content-length' => 0,
     );
     $httpRequest = new Request(
         'PUT',
         $this->resumeUri,
         $headers
     );

     return $this->makePutRequest($httpRequest);
  }

  /**
   * @return Psr\Http\Message\RequestInterface $request
   * @visible for testing
   */
  private function process()
  {
    $this->transformToUploadUrl();
    $request = $this->request;

    $postBody = '';
    $contentType = false;

    $meta = (string) $request->getBody();
    $meta = is_string($meta) ? json_decode($meta, true) : $meta;

    $uploadType = $this->getUploadType($meta);
    $request = $request->withUri(
        Uri::withQueryValue($request->getUri(), 'uploadType', $uploadType)
    );

    $mimeType = $this->mimeType ?: $request->getHeaderLine('content-type');

    if (self::UPLOAD_RESUMABLE_TYPE == $uploadType) {
      $contentType = $mimeType;
      $postBody = is_string($meta) ? $meta : json_encode($meta);
    } else if (self::UPLOAD_MEDIA_TYPE == $uploadType) {
      $contentType = $mimeType;
      $postBody = $this->data;
    } else if (self::UPLOAD_MULTIPART_TYPE == $uploadType) {
      // This is a multipart/related upload.
      $boundary = $this->boundary ?: mt_rand();
      $boundary = str_replace('"', '', $boundary);
      $contentType = 'multipart/related; boundary=' . $boundary;
      $related = "--$boundary\r\n";
      $related .= "Content-Type: application/json; charset=UTF-8\r\n";
      $related .= "\r\n" . json_encode($meta) . "\r\n";
      $related .= "--$boundary\r\n";
      $related .= "Content-Type: $mimeType\r\n";
      $related .= "Content-Transfer-Encoding: base64\r\n";
      $related .= "\r\n" . base64_encode($this->data) . "\r\n";
      $related .= "--$boundary--";
      $postBody = $related;
    }

    $request = $request->withBody(Psr7\stream_for($postBody));

    if (isset($contentType) && $contentType) {
      $request = $request->withHeader('content-type', $contentType);
    }

    return $this->request = $request;
  }

  /**
   * Valid upload types:
   * - resumable (UPLOAD_RESUMABLE_TYPE)
   * - media (UPLOAD_MEDIA_TYPE)
   * - multipart (UPLOAD_MULTIPART_TYPE)
   * @param $meta
   * @return string
   * @visible for testing
   */
  public function getUploadType($meta)
  {
    if ($this->resumable) {
      return self::UPLOAD_RESUMABLE_TYPE;
    }

    if (false == $meta && $this->data) {
      return self::UPLOAD_MEDIA_TYPE;
    }

    return self::UPLOAD_MULTIPART_TYPE;
  }

  public function getResumeUri()
  {
    if (null === $this->resumeUri) {
      $this->resumeUri = $this->fetchResumeUri();
    }

    return $this->resumeUri;
  }

  private function fetchResumeUri()
  {
    $body = $this->request->getBody();
    if ($body) {
      $headers = array(
        'content-type' => 'application/json; charset=UTF-8',
        'content-length' => $body->getSize(),
        'x-upload-content-type' => $this->mimeType,
        'x-upload-content-length' => $this->size,
        'expect' => '',
      );
      foreach ($headers as $key => $value) {
        $this->request = $this->request->withHeader($key, $value);
      }
    }

    $response = $this->client->execute($this->request, false);
    $location = $response->getHeaderLine('location');
    $code = $response->getStatusCode();

    if (200 == $code && true == $location) {
      return $location;
    }

    $message = $code;
    $body = json_decode((string) $this->request->getBody(), true);
    if (isset($body['error']['errors'])) {
      $message .= ': ';
      foreach ($body['error']['errors'] as $error) {
        $message .= "{$error['domain']}, {$error['message']};";
      }
      $message = rtrim($message, ';');
    }

    $error = "Failed to start the resumable upload (HTTP {$message})";
    $this->client->getLogger()->error($error);

    throw new Google_Exception($error);
  }

  private function transformToUploadUrl()
  {
    $parts = parse_url((string) $this->request->getUri());
    if (!isset($parts['path'])) {
      $parts['path'] = '';
    }
    $parts['path'] = '/upload' . $parts['path'];
    $uri = Uri::fromParts($parts);
    $this->request = $this->request->withUri($uri);
  }

  public function setChunkSize($chunkSize)
  {
    $this->chunkSize = $chunkSize;
  }

  public function getRequest()
  {
    return $this->request;
  }
}


<?php
/*
 * Copyright 2012 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

/**
 * Class to handle batched requests to the Google API service.
 *
 * Note that calls to `Google_Http_Batch::execute()` do not clear the queued
 * requests. To start a new batch, be sure to create a new instance of this
 * class.
 */
class Google_Http_Batch
{
  const BATCH_PATH = 'batch';

  private static $CONNECTION_ESTABLISHED_HEADERS = array(
    "HTTP/1.0 200 Connection established\r\n\r\n",
    "HTTP/1.1 200 Connection established\r\n\r\n",
  );

  /** @var string Multipart Boundary. */
  private $boundary;

  /** @var array service requests to be executed. */
  private $requests = array();

  /** @var Google_Client */
  private $client;

  private $rootUrl;

  private $batchPath;

  public function __construct(
      Google_Client $client,
      $boundary = false,
      $rootUrl = null,
      $batchPath = null
  ) {
    $this->client = $client;
    $this->boundary = $boundary ?: mt_rand();
    $this->rootUrl = rtrim($rootUrl ?: $this->client->getConfig('base_path'), '/');
    $this->batchPath = $batchPath ?: self::BATCH_PATH;
  }

  public function add(RequestInterface $request, $key = false)
  {
    if (false == $key) {
      $key = mt_rand();
    }

    $this->requests[$key] = $request;
  }

  public function execute()
  {
    $body = '';
    $classes = array();
    $batchHttpTemplate = <<<EOF
--%s
Content-Type: application/http
Content-Transfer-Encoding: binary
MIME-Version: 1.0
Content-ID: %s

%s
%s%s

EOF;

    /** @var Google_Http_Request $req */
    foreach ($this->requests as $key => $request) {
      $firstLine = sprintf(
          '%s %s HTTP/%s',
          $request->getMethod(),
          $request->getRequestTarget(),
          $request->getProtocolVersion()
      );

      $content = (string) $request->getBody();

      $headers = '';
      foreach ($request->getHeaders() as $name => $values) {
          $headers .= sprintf("%s:%s\r\n", $name, implode(', ', $values));
      }

      $body .= sprintf(
          $batchHttpTemplate,
          $this->boundary,
          $key,
          $firstLine,
          $headers,
          $content ? "\n".$content : ''
      );

      $classes['response-' . $key] = $request->getHeaderLine('X-Php-Expected-Class');
    }

    $body .= "--{$this->boundary}--";
    $body = trim($body);
    $url = $this->rootUrl . '/' . $this->batchPath;
    $headers = array(
      'Content-Type' => sprintf('multipart/mixed; boundary=%s', $this->boundary),
      'Content-Length' => strlen($body),
    );

    $request = new Request(
        'POST',
        $url,
        $headers,
        $body
    );

    $response = $this->client->execute($request);

    return $this->parseResponse($response, $classes);
  }

  public function parseResponse(ResponseInterface $response, $classes = array())
  {
    $contentType = $response->getHeaderLine('content-type');
    $contentType = explode(';', $contentType);
    $boundary = false;
    foreach ($contentType as $part) {
      $part = explode('=', $part, 2);
      if (isset($part[0]) && 'boundary' == trim($part[0])) {
        $boundary = $part[1];
      }
    }

    $body = (string) $response->getBody();
    if (!empty($body)) {
      $body = str_replace("--$boundary--", "--$boundary", $body);
      $parts = explode("--$boundary", $body);
      $responses = array();
      $requests = array_values($this->requests);

      foreach ($parts as $i => $part) {
        $part = trim($part);
        if (!empty($part)) {
          list($rawHeaders, $part) = explode("\r\n\r\n", $part, 2);
          $headers = $this->parseRawHeaders($rawHeaders);

          $status = substr($part, 0, strpos($part, "\n"));
          $status = explode(" ", $status);
          $status = $status[1];

          list($partHeaders, $partBody) = $this->parseHttpResponse($part, false);
          $response = new Response(
              $status,
              $partHeaders,
              Psr7\stream_for($partBody)
          );

          // Need content id.
          $key = $headers['content-id'];

          try {
            $response = Google_Http_REST::decodeHttpResponse($response, $requests[$i-1]);
          } catch (Google_Service_Exception $e) {
            // Store the exception as the response, so successful responses
            // can be processed.
            $response = $e;
          }

          $responses[$key] = $response;
        }
      }

      return $responses;
    }

    return null;
  }

  private function parseRawHeaders($rawHeaders)
  {
    $headers = array();
    $responseHeaderLines = explode("\r\n", $rawHeaders);
    foreach ($responseHeaderLines as $headerLine) {
      if ($headerLine && strpos($headerLine, ':') !== false) {
        list($header, $value) = explode(': ', $headerLine, 2);
        $header = strtolower($header);
        if (isset($headers[$header])) {
          $headers[$header] .= "\n" . $value;
        } else {
          $headers[$header] = $value;
        }
      }
    }
    return $headers;
  }

  /**
   * Used by the IO lib and also the batch processing.
   *
   * @param $respData
   * @param $headerSize
   * @return array
   */
  private function parseHttpResponse($respData, $headerSize)
  {
    // check proxy header
    foreach (self::$CONNECTION_ESTABLISHED_HEADERS as $established_header) {
      if (stripos($respData, $established_header) !== false) {
        // existed, remove it
        $respData = str_ireplace($established_header, '', $respData);
        // Subtract the proxy header size unless the cURL bug prior to 7.30.0
        // is present which prevented the proxy header size from being taken into
        // account.
        // @TODO look into this
        // if (!$this->needsQuirk()) {
        //   $headerSize -= strlen($established_header);
        // }
        break;
      }
    }

    if ($headerSize) {
      $responseBody = substr($respData, $headerSize);
      $responseHeaders = substr($respData, 0, $headerSize);
    } else {
      $responseSegments = explode("\r\n\r\n", $respData, 2);
      $responseHeaders = $responseSegments[0];
      $responseBody = isset($responseSegments[1]) ? $responseSegments[1] :
                                                    null;
    }

    $responseHeaders = $this->parseRawHeaders($responseHeaders);

    return array($responseHeaders, $responseBody);
  }
}


<?php
/*
 * Copyright 2013 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Implementation of levels 1-3 of the URI Template spec.
 * @see http://tools.ietf.org/html/rfc6570
 */
class Google_Utils_UriTemplate
{
  const TYPE_MAP = "1";
  const TYPE_LIST = "2";
  const TYPE_SCALAR = "4";

  /**
   * @var $operators array
   * These are valid at the start of a template block to
   * modify the way in which the variables inside are
   * processed.
   */
  private $operators = array(
      "+" => "reserved",
      "/" => "segments",
      "." => "dotprefix",
      "#" => "fragment",
      ";" => "semicolon",
      "?" => "form",
      "&" => "continuation"
  );

  /**
   * @var reserved array
   * These are the characters which should not be URL encoded in reserved
   * strings.
   */
  private $reserved = array(
      "=", ",", "!", "@", "|", ":", "/", "?", "#",
      "[", "]",'$', "&", "'", "(", ")", "*", "+", ";"
  );
  private $reservedEncoded = array(
    "%3D", "%2C", "%21", "%40", "%7C", "%3A", "%2F", "%3F",
    "%23", "%5B", "%5D", "%24", "%26", "%27", "%28", "%29",
    "%2A", "%2B", "%3B"
  );

  public function parse($string, array $parameters)
  {
    return $this->resolveNextSection($string, $parameters);
  }

  /**
   * This function finds the first matching {...} block and
   * executes the replacement. It then calls itself to find
   * subsequent blocks, if any.
   */
  private function resolveNextSection($string, $parameters)
  {
    $start = strpos($string, "{");
    if ($start === false) {
      return $string;
    }
    $end = strpos($string, "}");
    if ($end === false) {
      return $string;
    }
    $string = $this->replace($string, $start, $end, $parameters);
    return $this->resolveNextSection($string, $parameters);
  }

  private function replace($string, $start, $end, $parameters)
  {
    // We know a data block will have {} round it, so we can strip that.
    $data = substr($string, $start + 1, $end - $start - 1);

    // If the first character is one of the reserved operators, it effects
    // the processing of the stream.
    if (isset($this->operators[$data[0]])) {
      $op = $this->operators[$data[0]];
      $data = substr($data, 1);
      $prefix = "";
      $prefix_on_missing = false;

      switch ($op) {
        case "reserved":
          // Reserved means certain characters should not be URL encoded
          $data = $this->replaceVars($data, $parameters, ",", null, true);
          break;
        case "fragment":
          // Comma separated with fragment prefix. Bare values only.
          $prefix = "#";
          $prefix_on_missing = true;
          $data = $this->replaceVars($data, $parameters, ",", null, true);
          break;
        case "segments":
          // Slash separated data. Bare values only.
          $prefix = "/";
          $data =$this->replaceVars($data, $parameters, "/");
          break;
        case "dotprefix":
          // Dot separated data. Bare values only.
          $prefix = ".";
          $prefix_on_missing = true;
          $data = $this->replaceVars($data, $parameters, ".");
          break;
        case "semicolon":
          // Semicolon prefixed and separated. Uses the key name
          $prefix = ";";
          $data = $this->replaceVars($data, $parameters, ";", "=", false, true, false);
          break;
        case "form":
          // Standard URL format. Uses the key name
          $prefix = "?";
          $data = $this->replaceVars($data, $parameters, "&", "=");
          break;
        case "continuation":
          // Standard URL, but with leading ampersand. Uses key name.
          $prefix = "&";
          $data = $this->replaceVars($data, $parameters, "&", "=");
          break;
      }

      // Add the initial prefix character if data is valid.
      if ($data || ($data !== false && $prefix_on_missing)) {
        $data = $prefix . $data;
      }

    } else {
      // If no operator we replace with the defaults.
      $data = $this->replaceVars($data, $parameters);
    }
    // This is chops out the {...} and replaces with the new section.
    return substr($string, 0, $start) . $data . substr($string, $end + 1);
  }

  private function replaceVars(
      $section,
      $parameters,
      $sep = ",",
      $combine = null,
      $reserved = false,
      $tag_empty = false,
      $combine_on_empty = true
  ) {
    if (strpos($section, ",") === false) {
      // If we only have a single value, we can immediately process.
      return $this->combine(
          $section,
          $parameters,
          $sep,
          $combine,
          $reserved,
          $tag_empty,
          $combine_on_empty
      );
    } else {
      // If we have multiple values, we need to split and loop over them.
      // Each is treated individually, then glued together with the
      // separator character.
      $vars = explode(",", $section);
      return $this->combineList(
          $vars,
          $sep,
          $parameters,
          $combine,
          $reserved,
          false, // Never emit empty strings in multi-param replacements
          $combine_on_empty
      );
    }
  }

  public function combine(
      $key,
      $parameters,
      $sep,
      $combine,
      $reserved,
      $tag_empty,
      $combine_on_empty
  ) {
    $length = false;
    $explode = false;
    $skip_final_combine = false;
    $value = false;

    // Check for length restriction.
    if (strpos($key, ":") !== false) {
      list($key, $length) = explode(":", $key);
    }

    // Check for explode parameter.
    if ($key[strlen($key) - 1] == "*") {
      $explode = true;
      $key = substr($key, 0, -1);
      $skip_final_combine = true;
    }

    // Define the list separator.
    $list_sep = $explode ? $sep : ",";

    if (isset($parameters[$key])) {
      $data_type = $this->getDataType($parameters[$key]);
      switch ($data_type) {
        case self::TYPE_SCALAR:
          $value = $this->getValue($parameters[$key], $length);
          break;
        case self::TYPE_LIST:
          $values = array();
          foreach ($parameters[$key] as $pkey => $pvalue) {
            $pvalue = $this->getValue($pvalue, $length);
            if ($combine && $explode) {
              $values[$pkey] = $key . $combine . $pvalue;
            } else {
              $values[$pkey] = $pvalue;
            }
          }
          $value = implode($list_sep, $values);
          if ($value == '') {
            return '';
          }
          break;
        case self::TYPE_MAP:
          $values = array();
          foreach ($parameters[$key] as $pkey => $pvalue) {
            $pvalue = $this->getValue($pvalue, $length);
            if ($explode) {
              $pkey = $this->getValue($pkey, $length);
              $values[] = $pkey . "=" . $pvalue; // Explode triggers = combine.
            } else {
              $values[] = $pkey;
              $values[] = $pvalue;
            }
          }
          $value = implode($list_sep, $values);
          if ($value == '') {
            return false;
          }
          break;
      }
    } else if ($tag_empty) {
      // If we are just indicating empty values with their key name, return that.
      return $key;
    } else {
      // Otherwise we can skip this variable due to not being defined.
      return false;
    }

    if ($reserved) {
      $value = str_replace($this->reservedEncoded, $this->reserved, $value);
    }

    // If we do not need to include the key name, we just return the raw
    // value.
    if (!$combine || $skip_final_combine) {
      return $value;
    }

    // Else we combine the key name: foo=bar, if value is not the empty string.
    return $key . ($value != '' || $combine_on_empty ? $combine . $value : '');
  }

  /**
   * Return the type of a passed in value
   */
  private function getDataType($data)
  {
    if (is_array($data)) {
      reset($data);
      if (key($data) !== 0) {
        return self::TYPE_MAP;
      }
      return self::TYPE_LIST;
    }
    return self::TYPE_SCALAR;
  }

  /**
   * Utility function that merges multiple combine calls
   * for multi-key templates.
   */
  private function combineList(
      $vars,
      $sep,
      $parameters,
      $combine,
      $reserved,
      $tag_empty,
      $combine_on_empty
  ) {
    $ret = array();
    foreach ($vars as $var) {
      $response = $this->combine(
          $var,
          $parameters,
          $sep,
          $combine,
          $reserved,
          $tag_empty,
          $combine_on_empty
      );
      if ($response === false) {
        continue;
      }
      $ret[] = $response;
    }
    return implode($sep, $ret);
  }

  /**
   * Utility function to encode and trim values
   */
  private function getValue($value, $length)
  {
    if ($length) {
      $value = substr($value, 0, $length);
    }
    $value = rawurlencode($value);
    return $value;
  }
}


<?php
/*
 * Copyright 2010 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use Google\Auth\HttpHandler\HttpHandlerFactory;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

/**
 * This class implements the RESTful transport of apiServiceRequest()'s
 */
class Google_Http_REST
{
  /**
   * Executes a Psr\Http\Message\RequestInterface and (if applicable) automatically retries
   * when errors occur.
   *
   * @param Google_Client $client
   * @param Psr\Http\Message\RequestInterface $req
   * @return array decoded result
   * @throws Google_Service_Exception on server side error (ie: not authenticated,
   *  invalid or malformed post body, invalid url)
   */
  public static function execute(
      ClientInterface $client,
      RequestInterface $request,
      $expectedClass = null,
      $config = array(),
      $retryMap = null
  ) {
    $runner = new Google_Task_Runner(
        $config,
        sprintf('%s %s', $request->getMethod(), (string) $request->getUri()),
        array(get_class(), 'doExecute'),
        array($client, $request, $expectedClass)
    );

    if (null !== $retryMap) {
      $runner->setRetryMap($retryMap);
    }

    return $runner->run();
  }

  /**
   * Executes a Psr\Http\Message\RequestInterface
   *
   * @param Google_Client $client
   * @param Psr\Http\Message\RequestInterface $request
   * @return array decoded result
   * @throws Google_Service_Exception on server side error (ie: not authenticated,
   *  invalid or malformed post body, invalid url)
   */
  public static function doExecute(ClientInterface $client, RequestInterface $request, $expectedClass = null)
  {
    try {
      $httpHandler = HttpHandlerFactory::build($client);
      $response = $httpHandler($request);
    } catch (RequestException $e) {
      // if Guzzle throws an exception, catch it and handle the response
      if (!$e->hasResponse()) {
        throw $e;
      }

      $response = $e->getResponse();
      // specific checking for Guzzle 5: convert to PSR7 response
      if ($response instanceof \GuzzleHttp\Message\ResponseInterface) {
        $response = new Response(
            $response->getStatusCode(),
            $response->getHeaders() ?: [],
            $response->getBody(),
            $response->getProtocolVersion(),
            $response->getReasonPhrase()
        );
      }
    }

    return self::decodeHttpResponse($response, $request, $expectedClass);
  }

  /**
   * Decode an HTTP Response.
   * @static
   * @throws Google_Service_Exception
   * @param Psr\Http\Message\RequestInterface $response The http response to be decoded.
   * @param Psr\Http\Message\ResponseInterface $response
   * @return mixed|null
   */
  public static function decodeHttpResponse(
      ResponseInterface $response,
      RequestInterface $request = null,
      $expectedClass = null
  ) {
    $code = $response->getStatusCode();

    // retry strategy
    if (intVal($code) >= 400) {
      // if we errored out, it should be safe to grab the response body
      $body = (string) $response->getBody();

      // Check if we received errors, and add those to the Exception for convenience
      throw new Google_Service_Exception($body, $code, null, self::getResponseErrors($body));
    }

    // Ensure we only pull the entire body into memory if the request is not
    // of media type
    $body = self::decodeBody($response, $request);

    if ($expectedClass = self::determineExpectedClass($expectedClass, $request)) {
      $json = json_decode($body, true);

      return new $expectedClass($json);
    }

    return $response;
  }

  private static function decodeBody(ResponseInterface $response, RequestInterface $request = null)
  {
    if (self::isAltMedia($request)) {
      // don't decode the body, it's probably a really long string
      return '';
    }

    return (string) $response->getBody();
  }

  private static function determineExpectedClass($expectedClass, RequestInterface $request = null)
  {
    // "false" is used to explicitly prevent an expected class from being returned
    if (false === $expectedClass) {
      return null;
    }

    // if we don't have a request, we just use what's passed in
    if (null === $request) {
      return $expectedClass;
    }

    // return what we have in the request header if one was not supplied
    return $expectedClass ?: $request->getHeaderLine('X-Php-Expected-Class');
  }

  private static function getResponseErrors($body)
  {
    $json = json_decode($body, true);

    if (isset($json['error']['errors'])) {
      return $json['error']['errors'];
    }

    return null;
  }

  private static function isAltMedia(RequestInterface $request = null)
  {
    if ($request && $qs = $request->getUri()->getQuery()) {
      parse_str($qs, $query);
      if (isset($query['alt']) && $query['alt'] == 'media') {
        return true;
      }
    }

    return false;
  }
}


<?php
/*
 * Copyright 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * This class defines attributes, valid values, and usage which is generated
 * from a given json schema.
 * http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5
 *
 */
class Google_Model implements ArrayAccess
{
  /**
   * If you need to specify a NULL JSON value, use Google_Model::NULL_VALUE
   * instead - it will be replaced when converting to JSON with a real null.
   */
  const NULL_VALUE = "{}gapi-php-null";
  protected $internal_gapi_mappings = array();
  protected $modelData = array();
  protected $processed = array();

  /**
   * Polymorphic - accepts a variable number of arguments dependent
   * on the type of the model subclass.
   */
  final public function __construct()
  {
    if (func_num_args() == 1 && is_array(func_get_arg(0))) {
      // Initialize the model with the array's contents.
      $array = func_get_arg(0);
      $this->mapTypes($array);
    }
    $this->gapiInit();
  }

  /**
   * Getter that handles passthrough access to the data array, and lazy object creation.
   * @param string $key Property name.
   * @return mixed The value if any, or null.
   */
  public function __get($key)
  {
    $keyType = $this->keyType($key);
    $keyDataType = $this->dataType($key);
    if ($keyType && !isset($this->processed[$key])) {
      if (isset($this->modelData[$key])) {
        $val = $this->modelData[$key];
      } elseif ($keyDataType == 'array' || $keyDataType == 'map') {
        $val = array();
      } else {
        $val = null;
      }

      if ($this->isAssociativeArray($val)) {
        if ($keyDataType && 'map' == $keyDataType) {
          foreach ($val as $arrayKey => $arrayItem) {
              $this->modelData[$key][$arrayKey] =
                new $keyType($arrayItem);
          }
        } else {
          $this->modelData[$key] = new $keyType($val);
        }
      } else if (is_array($val)) {
        $arrayObject = array();
        foreach ($val as $arrayIndex => $arrayItem) {
          $arrayObject[$arrayIndex] = new $keyType($arrayItem);
        }
        $this->modelData[$key] = $arrayObject;
      }
      $this->processed[$key] = true;
    }

    return isset($this->modelData[$key]) ? $this->modelData[$key] : null;
  }

  /**
   * Initialize this object's properties from an array.
   *
   * @param array $array Used to seed this object's properties.
   * @return void
   */
  protected function mapTypes($array)
  {
    // Hard initialise simple types, lazy load more complex ones.
    foreach ($array as $key => $val) {
      if ($keyType = $this->keyType($key)) {
        $dataType = $this->dataType($key);
        if ($dataType == 'array' || $dataType == 'map') {
          $this->$key = array();
          foreach ($val as $itemKey => $itemVal) {
            if ($itemVal instanceof $keyType) {
              $this->{$key}[$itemKey] = $itemVal;
            } else {
              $this->{$key}[$itemKey] = new $keyType($itemVal);
            }
          }
        } elseif ($val instanceof $keyType) {
          $this->$key = $val;
        } else {
          $this->$key = new $keyType($val);
        }
        unset($array[$key]);
      } elseif (property_exists($this, $key)) {
          $this->$key = $val;
          unset($array[$key]);
      } elseif (property_exists($this, $camelKey = $this->camelCase($key))) {
          // This checks if property exists as camelCase, leaving it in array as snake_case
          // in case of backwards compatibility issues.
          $this->$camelKey = $val;
      }
    }
    $this->modelData = $array;
  }

  /**
   * Blank initialiser to be used in subclasses to do  post-construction initialisation - this
   * avoids the need for subclasses to have to implement the variadics handling in their
   * constructors.
   */
  protected function gapiInit()
  {
    return;
  }

  /**
   * Create a simplified object suitable for straightforward
   * conversion to JSON. This is relatively expensive
   * due to the usage of reflection, but shouldn't be called
   * a whole lot, and is the most straightforward way to filter.
   */
  public function toSimpleObject()
  {
    $object = new stdClass();

    // Process all other data.
    foreach ($this->modelData as $key => $val) {
      $result = $this->getSimpleValue($val);
      if ($result !== null) {
        $object->$key = $this->nullPlaceholderCheck($result);
      }
    }

    // Process all public properties.
    $reflect = new ReflectionObject($this);
    $props = $reflect->getProperties(ReflectionProperty::IS_PUBLIC);
    foreach ($props as $member) {
      $name = $member->getName();
      $result = $this->getSimpleValue($this->$name);
      if ($result !== null) {
        $name = $this->getMappedName($name);
        $object->$name = $this->nullPlaceholderCheck($result);
      }
    }

    return $object;
  }

  /**
   * Handle different types of values, primarily
   * other objects and map and array data types.
   */
  private function getSimpleValue($value)
  {
    if ($value instanceof Google_Model) {
      return $value->toSimpleObject();
    } else if (is_array($value)) {
      $return = array();
      foreach ($value as $key => $a_value) {
        $a_value = $this->getSimpleValue($a_value);
        if ($a_value !== null) {
          $key = $this->getMappedName($key);
          $return[$key] = $this->nullPlaceholderCheck($a_value);
        }
      }
      return $return;
    }
    return $value;
  }

  /**
   * Check whether the value is the null placeholder and return true null.
   */
  private function nullPlaceholderCheck($value)
  {
    if ($value === self::NULL_VALUE) {
      return null;
    }
    return $value;
  }

  /**
   * If there is an internal name mapping, use that.
   */
  private function getMappedName($key)
  {
    if (isset($this->internal_gapi_mappings, $this->internal_gapi_mappings[$key])) {
      $key = $this->internal_gapi_mappings[$key];
    }
    return $key;
  }

  /**
   * Returns true only if the array is associative.
   * @param array $array
   * @return bool True if the array is associative.
   */
  protected function isAssociativeArray($array)
  {
    if (!is_array($array)) {
      return false;
    }
    $keys = array_keys($array);
    foreach ($keys as $key) {
      if (is_string($key)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Verify if $obj is an array.
   * @throws Google_Exception Thrown if $obj isn't an array.
   * @param array $obj Items that should be validated.
   * @param string $method Method expecting an array as an argument.
   */
  public function assertIsArray($obj, $method)
  {
    if ($obj && !is_array($obj)) {
      throw new Google_Exception(
          "Incorrect parameter type passed to $method(). Expected an array."
      );
    }
  }

  public function offsetExists($offset)
  {
    return isset($this->$offset) || isset($this->modelData[$offset]);
  }

  public function offsetGet($offset)
  {
    return isset($this->$offset) ?
        $this->$offset :
        $this->__get($offset);
  }

  public function offsetSet($offset, $value)
  {
    if (property_exists($this, $offset)) {
      $this->$offset = $value;
    } else {
      $this->modelData[$offset] = $value;
      $this->processed[$offset] = true;
    }
  }

  public function offsetUnset($offset)
  {
    unset($this->modelData[$offset]);
  }

  protected function keyType($key)
  {
    $keyType = $key . "Type";

    // ensure keyType is a valid class
    if (property_exists($this, $keyType) && class_exists($this->$keyType)) {
      return $this->$keyType;
    }
  }

  protected function dataType($key)
  {
    $dataType = $key . "DataType";

    if (property_exists($this, $dataType)) {
      return $this->$dataType;
    }
  }

  public function __isset($key)
  {
    return isset($this->modelData[$key]);
  }

  public function __unset($key)
  {
    unset($this->modelData[$key]);
  }

  /**
   * Convert a string to camelCase
   * @param  string $value
   * @return string
   */
  private function camelCase($value)
  {
    $value = ucwords(str_replace(array('-', '_'), ' ', $value));
    $value = str_replace(' ', '', $value);
    $value[0] = strtolower($value[0]);
    return $value;
  }
}


<?php
/**
 * Copyright 2015 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;

class Google_AuthHandler_AuthHandlerFactory
{
  /**
   * Builds out a default http handler for the installed version of guzzle.
   *
   * @return Google_AuthHandler_Guzzle5AuthHandler|Google_AuthHandler_Guzzle6AuthHandler
   * @throws Exception
   */
  public static function build($cache = null, array $cacheConfig = [])
  {
    $version = ClientInterface::VERSION;

    switch ($version[0]) {
      case '5':
        return new Google_AuthHandler_Guzzle5AuthHandler($cache, $cacheConfig);
      case '6':
        return new Google_AuthHandler_Guzzle6AuthHandler($cache, $cacheConfig);
      default:
        throw new Exception('Version not supported');
    }
  }
}


<?php

/**
 * Define the YOURLS config
 */

namespace YOURLS\Config;

use YOURLS\Exceptions\ConfigException;

class Config {

    /**
     * @param string
     */
    protected $root;

    /**
     * @param mixed
     */
    protected $config;

    /**
     * @since  1.7.3
     * @param  mixed $config   Optional user defined config path
     */
    public function __construct($config = false) {
        $this->set_root( $this->fix_win32_path( dirname( dirname( __DIR__ ) ) ) );
        $this->set_config($config);
    }

    /**
     * Convert antislashes to slashes
     *
     * @since  1.7.3
     * @param  string  $path
     * @return string  path with \ converted to /
     */
    public function fix_win32_path($path) {
        return str_replace('\\', '/', $path);
    }

    /**
     * @since  1.7.3
     * @param  string  path to config file
     * @return void
     */
    public function set_config($config) {
        $this->config = $config;
    }

    /**
     * @since  1.7.3
     * @param  string  path to YOURLS root directory
     * @return void
     */
    public function set_root($root) {
        $this->root = $root;
    }

    /**
     * Find config.php, either user defined or from standard location
     *
     * @since  1.7.3
     * @return string         path to found config file
     * @throws ConfigException
     */
    public function find_config() {

        $config = $this->fix_win32_path($this->config);

        if (!empty($config) && is_readable($config)) {
            return $config;
        }

        if (!empty($config) && !is_readable($config)) {
            throw new ConfigException("User defined config not found at '$config'");
        }

        // config.php in /user/
        if (file_exists($this->root . '/user/config.php')) {
            return $this->root . '/user/config.php';
        }

        // config.php in /includes/
        if (file_exists($this->root . '/includes/config.php')) {
            return $this->root . '/includes/config.php';
        }

        // config.php not found :(

        throw new ConfigException('Cannot find config.php. Please read the readme.html to learn how to install YOURLS');
    }

    /**
     * Define core constants that have not been user defined in config.php
     *
     * @since  1.7.3
     * @return void
     * @throws ConfigException
     */
    public function define_core_constants() {
        // Check minimal config job has been properly done
        $must_haves = array('YOURLS_DB_USER', 'YOURLS_DB_PASS', 'YOURLS_DB_NAME', 'YOURLS_DB_HOST', 'YOURLS_DB_PREFIX', 'YOURLS_SITE');
        foreach($must_haves as $must_have) {
            if (!defined($must_have)) {
                throw new ConfigException('Config is incomplete (missing at least '.$must_have.') Check config-sample.php and edit your config accordingly');
            }
        }

        /**
         * The following has an awful CRAP index and it would be much shorter reduced to something like
         * defining an array of ('YOURLS_SOMETHING' => 'default value') and then a simple loop over the
         * array, checking if $current is defined as a constant and otherwise define said constant with
         * its default value. I did not wrote it that way because that would make it difficult for code
         * parsers to identify which constants are defined and where. So, here it is, that long list of
         * if (!defined) define(). Ho and by the way, such beautiful comment, much right aligned, wow !
         */

        // physical path of YOURLS root
        if (!defined( 'YOURLS_ABSPATH' ))
            define('YOURLS_ABSPATH', $this->root);

        // physical path of includes directory
        if (!defined( 'YOURLS_INC' ))
            define('YOURLS_INC', YOURLS_ABSPATH.'/includes');

        // physical path of user directory
        if (!defined( 'YOURLS_USERDIR' ))
            define( 'YOURLS_USERDIR', YOURLS_ABSPATH.'/user' );

        // URL of user directory
        if (!defined( 'YOURLS_USERURL' ))
            define( 'YOURLS_USERURL', YOURLS_SITE.'/user' );

        // physical path of asset directory
        if( !defined( 'YOURLS_ASSETDIR' ) )
            define( 'YOURLS_ASSETDIR', YOURLS_ABSPATH.'/assets' );

        // URL of asset directory
        if( !defined( 'YOURLS_ASSETURL' ) )
            define( 'YOURLS_ASSETURL', YOURLS_SITE.'/assets' );

        // physical path of translations directory
        if (!defined( 'YOURLS_LANG_DIR' ))
            define( 'YOURLS_LANG_DIR', YOURLS_USERDIR.'/languages' );

        // physical path of plugins directory
        if (!defined( 'YOURLS_PLUGINDIR' ))
            define( 'YOURLS_PLUGINDIR', YOURLS_USERDIR.'/plugins' );

        // URL of plugins directory
        if (!defined( 'YOURLS_PLUGINURL' ))
            define( 'YOURLS_PLUGINURL', YOURLS_USERURL.'/plugins' );

        // physical path of themes directory
        if( !defined( 'YOURLS_THEMEDIR' ) )
            define( 'YOURLS_THEMEDIR', YOURLS_USERDIR.'/themes' );

        // URL of themes directory
        if( !defined( 'YOURLS_THEMEURL' ) )
            define( 'YOURLS_THEMEURL', YOURLS_USERURL.'/themes' );

        // physical path of pages directory
        if (!defined( 'YOURLS_PAGEDIR' ))
            define('YOURLS_PAGEDIR', YOURLS_ABSPATH.'/pages' );

        // table to store URLs
        if (!defined( 'YOURLS_DB_TABLE_URL' ))
            define( 'YOURLS_DB_TABLE_URL', YOURLS_DB_PREFIX.'url' );

        // table to store options
        if (!defined( 'YOURLS_DB_TABLE_OPTIONS' ))
            define( 'YOURLS_DB_TABLE_OPTIONS', YOURLS_DB_PREFIX.'options' );

        // table to store hits, for stats
        if (!defined( 'YOURLS_DB_TABLE_LOG' ))
            define( 'YOURLS_DB_TABLE_LOG', YOURLS_DB_PREFIX.'log' );

        // minimum delay in sec before a same IP can add another URL. Note: logged in users are not throttled down.
        if (!defined( 'YOURLS_FLOOD_DELAY_SECONDS' ))
            define( 'YOURLS_FLOOD_DELAY_SECONDS', 15 );

        // comma separated list of IPs that can bypass flood check.
        if (!defined( 'YOURLS_FLOOD_IP_WHITELIST' ))
            define( 'YOURLS_FLOOD_IP_WHITELIST', '' );

        // life span of an auth cookie in seconds (60*60*24*7 = 7 days)
        if (!defined( 'YOURLS_COOKIE_LIFE' ))
            define( 'YOURLS_COOKIE_LIFE', 60*60*24*7 );

        // life span of a nonce in seconds
        if (!defined( 'YOURLS_NONCE_LIFE' ))
            define( 'YOURLS_NONCE_LIFE', 43200 ); // 3600 * 12

        // if set to true, disable stat logging (no use for it, too busy servers, ...)
        if (!defined( 'YOURLS_NOSTATS' ))
            define( 'YOURLS_NOSTATS', false );

        // if set to true, force https:// in the admin area
        if (!defined( 'YOURLS_ADMIN_SSL' ))
            define( 'YOURLS_ADMIN_SSL', false );

        // if set to true, verbose debug infos. Will break things. Don't enable.
        if (!defined( 'YOURLS_DEBUG' ))
            define( 'YOURLS_DEBUG', false );

        // Error reporting
        if (defined( 'YOURLS_DEBUG' ) && YOURLS_DEBUG == true ) {
            error_reporting( -1 );
        } else {
            error_reporting( E_ERROR | E_PARSE );
        }
    }

}