// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview
 * OAuth2 API flow implementations.
 */

'use strict';

/** @suppress {duplicate} */
var remoting = remoting || {};

/** @constructor */
remoting.OAuth2Api = function() {
};

/** @private
 *  @return {string} OAuth2 token URL.
 */
remoting.OAuth2Api.getOAuth2TokenEndpoint_ = function() {
  return remoting.settings.OAUTH2_BASE_URL + '/token';
};

/** @private
 *  @return {string} OAuth token revocation URL.
 */
remoting.OAuth2Api.getOAuth2RevokeTokenEndpoint_ = function() {
  return remoting.settings.OAUTH2_BASE_URL + '/revoke';
};

/** @private
 *  @return {string} OAuth2 userinfo API URL.
 */
remoting.OAuth2Api.getOAuth2ApiUserInfoEndpoint_ = function() {
  return remoting.settings.OAUTH2_API_BASE_URL + '/v1/userinfo';
};


/**
 * Interprets HTTP error responses in authentication XMLHttpRequests.
 *
 * @private
 * @param {number} xhrStatus Status (HTTP response code) of the XMLHttpRequest.
 * @return {remoting.Error} An error code to be raised.
 */
remoting.OAuth2Api.interpretXhrStatus_ =
    function(xhrStatus) {
  // Return AUTHENTICATION_FAILED by default, so that the user can try to
  // recover from an unexpected failure by signing in again.
  /** @type {remoting.Error} */
  var error = remoting.Error.AUTHENTICATION_FAILED;
  if (xhrStatus == 400 || xhrStatus == 401 || xhrStatus == 403) {
    error = remoting.Error.AUTHENTICATION_FAILED;
  } else if (xhrStatus == 502 || xhrStatus == 503) {
    error = remoting.Error.SERVICE_UNAVAILABLE;
  } else if (xhrStatus == 0) {
    error = remoting.Error.NETWORK_FAILURE;
  } else {
    console.warn('Unexpected authentication response code: ' + xhrStatus);
  }
  return error;
};

/**
 * Asynchronously retrieves a new access token from the server.
 *
 * @param {function(string, number): void} onDone Callback to invoke when
 *     the access token and expiration time are successfully fetched.
 * @param {function(remoting.Error):void} onError Callback invoked if an
 *     error occurs.
 * @param {string} clientId OAuth2 client ID.
 * @param {string} clientSecret OAuth2 client secret.
 * @param {string} refreshToken OAuth2 refresh token to be redeemed.
 * @return {void} Nothing.
 */
remoting.OAuth2Api.refreshAccessToken = function(
    onDone, onError, clientId, clientSecret, refreshToken) {
  /** @param {XMLHttpRequest} xhr */
  var onResponse = function(xhr) {
    if (xhr.status == 200) {
      try {
        // Don't use jsonParseSafe here unless you move the definition out of
        // remoting.js, otherwise this won't work from the OAuth trampoline.
        // TODO(jamiewalch): Fix this once we're no longer using the trampoline.
        var tokens = JSON.parse(xhr.responseText);
        onDone(tokens['access_token'], tokens['expires_in']);
      } catch (err) {
        console.error('Invalid "token" response from server:',
                      /** @type {*} */ (err));
        onError(remoting.Error.UNEXPECTED);
      }
    } else {
      console.error('Failed to refresh token. Status: ' + xhr.status +
                    ' response: ' + xhr.responseText);
      onError(remoting.OAuth2Api.interpretXhrStatus_(xhr.status));
    }
  };

  var parameters = {
    'client_id': clientId,
    'client_secret': clientSecret,
    'refresh_token': refreshToken,
    'grant_type': 'refresh_token'
  };

  remoting.xhr.post(remoting.OAuth2Api.getOAuth2TokenEndpoint_(),
                    onResponse, parameters);
};

/**
 * Asynchronously exchanges an authorization code for access and refresh tokens.
 *
 * @param {function(string, string, number): void} onDone Callback to
 *     invoke when the refresh token, access token and access token expiration
 *     time are successfully fetched.
 * @param {function(remoting.Error):void} onError Callback invoked if an
 *     error occurs.
 * @param {string} clientId OAuth2 client ID.
 * @param {string} clientSecret OAuth2 client secret.
 * @param {string} code OAuth2 authorization code.
 * @param {string} redirectUri Redirect URI used to obtain this code.
 * @return {void} Nothing.
 */
remoting.OAuth2Api.exchangeCodeForTokens = function(
    onDone, onError, clientId, clientSecret, code, redirectUri) {
  /** @param {XMLHttpRequest} xhr */
  var onResponse = function(xhr) {
    if (xhr.status == 200) {
      try {
        // Don't use jsonParseSafe here unless you move the definition out of
        // remoting.js, otherwise this won't work from the OAuth trampoline.
        // TODO(jamiewalch): Fix this once we're no longer using the trampoline.
        var tokens = JSON.parse(xhr.responseText);
        onDone(tokens['refresh_token'],
               tokens['access_token'], tokens['expires_in']);
      } catch (err) {
        console.error('Invalid "token" response from server:',
                      /** @type {*} */ (err));
        onError(remoting.Error.UNEXPECTED);
      }
    } else {
      console.error('Failed to exchange code for token. Status: ' + xhr.status +
                    ' response: ' + xhr.responseText);
      onError(remoting.OAuth2Api.interpretXhrStatus_(xhr.status));
    }
  };

  var parameters = {
    'client_id': clientId,
    'client_secret': clientSecret,
    'redirect_uri': redirectUri,
    'code': code,
    'grant_type': 'authorization_code'
  };
  remoting.xhr.post(remoting.OAuth2Api.getOAuth2TokenEndpoint_(),
                    onResponse, parameters);
};

/**
 * Get the user's email address.
 *
 * @param {function(string):void} onDone Callback invoked when the email
 *     address is available.
 * @param {function(remoting.Error):void} onError Callback invoked if an
 *     error occurs.
 * @param {string} token Access token.
 * @return {void} Nothing.
 */
remoting.OAuth2Api.getEmail = function(onDone, onError, token) {
  /** @param {XMLHttpRequest} xhr */
  var onResponse = function(xhr) {
    if (xhr.status == 200) {
      try {
        var result = JSON.parse(xhr.responseText);
        onDone(result['email']);
      } catch (err) {
        console.error('Invalid "userinfo" response from server:',
                      /** @type {*} */ (err));
        onError(remoting.Error.UNEXPECTED);
      }
    } else {
      console.error('Failed to get email. Status: ' + xhr.status +
                    ' response: ' + xhr.responseText);
      onError(remoting.OAuth2Api.interpretXhrStatus_(xhr.status));
    }
  };
  var headers = { 'Authorization': 'OAuth ' + token };
  remoting.xhr.get(remoting.OAuth2Api.getOAuth2ApiUserInfoEndpoint_(),
                   onResponse, '', headers);
};

/**
 * Revokes a refresh or an access token.
 *
 * @param {function():void} onDone Callback invoked when the token is
 *     revoked.
 * @param {function(remoting.Error):void} onError Callback invoked if an
 *     error occurs.
 * @param {string} token An access or refresh token.
 * @return {void} Nothing.
 */
remoting.OAuth2Api.revokeToken = function(onDone, onError, token) {
  /** @param {XMLHttpRequest} xhr */
  var onResponse = function(xhr) {
    if (xhr.status == 200) {
      onDone();
    } else {
      console.error('Failed to revoke token. Status: ' + xhr.status +
                    ' response: ' + xhr.responseText);
      onError(remoting.OAuth2Api.interpretXhrStatus_(xhr.status));
    }
  };

  var parameters = { 'token': token };
  remoting.xhr.post(remoting.OAuth2Api.getOAuth2RevokeTokenEndpoint_(),
                    onResponse, parameters);
};