/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;

this.EXPORTED_SYMBOLS = [
  "HAWKAuthenticatedRESTRequest",
];

Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/rest.js");

XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils",
                                  "resource://services-crypto/utils.js");

const Prefs = new Preferences("services.common.rest.");

/**
 * Single-use HAWK-authenticated HTTP requests to RESTish resources.
 *
 * @param uri
 *        (String) URI for the RESTRequest constructor
 *
 * @param credentials
 *        (Object) Optional credentials for computing HAWK authentication
 *        header.
 *
 * @param payloadObj
 *        (Object) Optional object to be converted to JSON payload
 *
 * @param extra
 *        (Object) Optional extra params for HAWK header computation.
 *        Valid properties are:
 *
 *          now:                 <current time in milliseconds>,
 *          localtimeOffsetMsec: <local clock offset vs server>
 *
 * extra.localtimeOffsetMsec is the value in milliseconds that must be added to
 * the local clock to make it agree with the server's clock.  For instance, if
 * the local clock is two minutes ahead of the server, the time offset in
 * milliseconds will be -120000.
 */

this.HAWKAuthenticatedRESTRequest =
 function HawkAuthenticatedRESTRequest(uri, credentials, extra={}) {
  RESTRequest.call(this, uri);

  this.credentials = credentials;
  this.now = extra.now || Date.now();
  this.localtimeOffsetMsec = extra.localtimeOffsetMsec || 0;
  this._log.trace("local time, offset: " + this.now + ", " + (this.localtimeOffsetMsec));

  // Expose for testing
  this._intl = getIntl();
};
HAWKAuthenticatedRESTRequest.prototype = {
  __proto__: RESTRequest.prototype,

  dispatch: function dispatch(method, data, onComplete, onProgress) {
    let contentType = "text/plain";
    if (method == "POST" || method == "PUT") {
      contentType = "application/json";
    }
    if (this.credentials) {
      let options = {
        now: this.now,
        localtimeOffsetMsec: this.localtimeOffsetMsec,
        credentials: this.credentials,
        payload: data && JSON.stringify(data) || "",
        contentType: contentType,
      };
      let header = CryptoUtils.computeHAWK(this.uri, method, options);
      this.setHeader("Authorization", header.field);
      this._log.trace("hawk auth header: " + header.field);
    }

    this.setHeader("Content-Type", contentType);

    this.setHeader("Accept-Language", this._intl.accept_languages);

    return RESTRequest.prototype.dispatch.call(
      this, method, data, onComplete, onProgress
    );
  }
};

// With hawk request, we send the user's accepted-languages with each request.
// To keep the number of times we read this pref at a minimum, maintain the
// preference in a stateful object that notices and updates itself when the
// pref is changed.
this.Intl = function Intl() {
  // We won't actually query the pref until the first time we need it
  this._accepted = "";
  this._everRead = false;
  this._log = Log.repository.getLogger("Services.common.RESTRequest");
  this._log.level = Log.Level[Prefs.get("log.logger.rest.request")];
  this.init();
};

this.Intl.prototype = {
  init: function() {
    Services.prefs.addObserver("intl.accept_languages", this, false);
  },

  uninit: function() {
    Services.prefs.removeObserver("intl.accept_languages", this);
  },

  observe: function(subject, topic, data) {
    this.readPref();
  },

  readPref: function() {
    this._everRead = true;
    try {
      this._accepted = Services.prefs.getComplexValue(
        "intl.accept_languages", Ci.nsIPrefLocalizedString).data;
    } catch (err) {
      this._log.error("Error reading intl.accept_languages pref: " + CommonUtils.exceptionStr(err));
    }
  },

  get accept_languages() {
    if (!this._everRead) {
      this.readPref();
    }
    return this._accepted;
  },
};

// Singleton getter for Intl, creating an instance only when we first need it.
let intl = null;
function getIntl() {
  if (!intl) {
    intl = new Intl();
  }
  return intl;
}

