<?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.
 */

if (!defined('ABSPATH')) die('No direct access allowed');

if (!class_exists('UDP_Google_Client')) {
  require_once dirname(__FILE__) . '/../autoload.php';
}

/**
 * Authentication via built-in Compute Engine service accounts.
 * The instance must be pre-configured with a service account
 * and the appropriate scopes.
 * @author Jonathan Parrott <jon.wayne.parrott@gmail.com>
 */
class Google_Auth_ComputeEngine extends Google_Auth_Abstract
{
  const METADATA_AUTH_URL =
      'http://metadata/computeMetadata/v1/instance/service-accounts/default/token';
  private $client;
  private $token;

  public function __construct(UDP_Google_Client $client, $config = null)
  {
    $this->client = $client;
  }

  /**
   * Perform an authenticated / signed apiHttpRequest.
   * This function takes the apiHttpRequest, calls apiAuth->sign on it
   * (which can modify the request in what ever way fits the auth mechanism)
   * and then calls apiCurlIO::makeRequest on the signed request
   *
   * @param Google_Http_Request $request
   * @return Google_Http_Request The resulting HTTP response including the
   * responseHttpCode, responseHeaders and responseBody.
   */
  public function authenticatedRequest(UDP_Google_Http_Request $request)
  {
    $request = $this->sign($request);
    return $this->client->getIo()->makeRequest($request);
  }

  /**
   * @param string $token
   * @throws Google_Auth_Exception
   */
  public function setAccessToken($token)
  {
    $token = json_decode($token, true);
    if ($token == null) {
      throw new Google_Auth_Exception('Could not json decode the token');
    }
    if (! isset($token['access_token'])) {
      throw new Google_Auth_Exception("Invalid token format");
    }
    $token['created'] = time();
    $this->token = $token;
  }

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

  /**
   * Acquires a new access token from the compute engine metadata server.
   * @throws Google_Auth_Exception
   */
  public function acquireAccessToken()
  {
    $request = new UDP_Google_Http_Request(
        self::METADATA_AUTH_URL,
        'GET',
        array(
          'Metadata-Flavor' => 'Google'
        )
    );
    $request->disableGzip();
    $response = $this->client->getIo()->makeRequest($request);

    if ($response->getResponseHttpCode() == 200) {
      $this->setAccessToken($response->getResponseBody());
      $this->token['created'] = time();
      return $this->getAccessToken();
    } else {
      throw new Google_Auth_Exception(
          sprintf(
              "Error fetching service account access token, message: '%s'",
              $response->getResponseBody() // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Error messages should be escaped when caught and printed.
          ),
          $response->getResponseHttpCode() // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- HTTP code should be escaped when caught and printed.
      );
    }
  }

  /**
   * Include an accessToken in a given apiHttpRequest.
   * @param Google_Http_Request $request
   * @return Google_Http_Request
   * @throws Google_Auth_Exception
   */
  public function sign(UDP_Google_Http_Request $request)
  {
    if ($this->isAccessTokenExpired()) {
      $this->acquireAccessToken();
    }

    $this->client->getLogger()->debug('Compute engine service account authentication');

    $request->setRequestHeaders(
        array('Authorization' => 'Bearer ' . $this->token['access_token'])
    );

    return $request;
  }

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

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

    return $expired;
  }
}
