forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			485 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			485 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* 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 } = Components;
 | 
						|
 | 
						|
Cu.import("resource://gre/modules/Services.jsm");
 | 
						|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 | 
						|
Cu.import("resource://gre/modules/Promise.jsm");
 | 
						|
let console = (Cu.import("resource://gre/modules/devtools/Console.jsm", {})).console;
 | 
						|
 | 
						|
this.EXPORTED_SYMBOLS = ["MozLoopService"];
 | 
						|
 | 
						|
XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI",
 | 
						|
  "resource:///modules/loop/MozLoopAPI.jsm");
 | 
						|
 | 
						|
XPCOMUtils.defineLazyModuleGetter(this, "Chat", "resource:///modules/Chat.jsm");
 | 
						|
 | 
						|
XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
 | 
						|
                                  "resource://services-common/utils.js");
 | 
						|
 | 
						|
XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils",
 | 
						|
                                  "resource://services-crypto/utils.js");
 | 
						|
 | 
						|
XPCOMUtils.defineLazyModuleGetter(this, "HawkClient",
 | 
						|
                                  "resource://services-common/hawkclient.js");
 | 
						|
 | 
						|
XPCOMUtils.defineLazyModuleGetter(this, "deriveHawkCredentials",
 | 
						|
                                  "resource://services-common/hawkrequest.js");
 | 
						|
 | 
						|
XPCOMUtils.defineLazyModuleGetter(this, "MozLoopPushHandler",
 | 
						|
                                  "resource:///modules/loop/MozLoopPushHandler.jsm");
 | 
						|
 | 
						|
/**
 | 
						|
 * Internal helper methods and state
 | 
						|
 *
 | 
						|
 * The registration is a two-part process. First we need to connect to
 | 
						|
 * and register with the push server. Then we need to take the result of that
 | 
						|
 * and register with the Loop server.
 | 
						|
 */
 | 
						|
let MozLoopServiceInternal = {
 | 
						|
  // The uri of the Loop server.
 | 
						|
  loopServerUri: Services.prefs.getCharPref("loop.server"),
 | 
						|
 | 
						|
  // The current deferred for the registration process. This is set if in progress
 | 
						|
  // or the registration was successful. This is null if a registration attempt was
 | 
						|
  // unsuccessful.
 | 
						|
  _registeredDeferred: null,
 | 
						|
 | 
						|
  /**
 | 
						|
   * The initial delay for push registration. This ensures we don't start
 | 
						|
   * kicking off straight after browser startup, just a few seconds later.
 | 
						|
   */
 | 
						|
  get initialRegistrationDelayMilliseconds() {
 | 
						|
    try {
 | 
						|
      // Let a pref override this for developer & testing use.
 | 
						|
      return Services.prefs.getIntPref("loop.initialDelay");
 | 
						|
    } catch (x) {
 | 
						|
      // Default to 5 seconds
 | 
						|
      return 5000;
 | 
						|
    }
 | 
						|
    return initialDelay;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Gets the current latest expiry time for urls.
 | 
						|
   *
 | 
						|
   * In seconds since epoch.
 | 
						|
   */
 | 
						|
  get expiryTimeSeconds() {
 | 
						|
    try {
 | 
						|
      return Services.prefs.getIntPref("loop.urlsExpiryTimeSeconds");
 | 
						|
    } catch (x) {
 | 
						|
      // It is ok for the pref not to exist.
 | 
						|
      return 0;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Sets the expiry time to either the specified time, or keeps it the same
 | 
						|
   * depending on which is latest.
 | 
						|
   */
 | 
						|
  set expiryTimeSeconds(time) {
 | 
						|
    if (time > this.expiryTimeSeconds) {
 | 
						|
      Services.prefs.setIntPref("loop.urlsExpiryTimeSeconds", time);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Returns true if the expiry time is in the future.
 | 
						|
   */
 | 
						|
  urlExpiryTimeIsInFuture: function() {
 | 
						|
    return this.expiryTimeSeconds * 1000 > Date.now();
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Retrieves MozLoopService "do not disturb" pref value.
 | 
						|
   *
 | 
						|
   * @return {Boolean} aFlag
 | 
						|
   */
 | 
						|
  get doNotDisturb() {
 | 
						|
    return Services.prefs.getBoolPref("loop.do_not_disturb");
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Sets MozLoopService "do not disturb" pref value.
 | 
						|
   *
 | 
						|
   * @param {Boolean} aFlag
 | 
						|
   */
 | 
						|
  set doNotDisturb(aFlag) {
 | 
						|
    Services.prefs.setBoolPref("loop.do_not_disturb", Boolean(aFlag));
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Starts registration of Loop with the push server, and then will register
 | 
						|
   * with the Loop server. It will return early if already registered.
 | 
						|
   *
 | 
						|
   * @param {Object} mockPushHandler Optional, test-only mock push handler. Used
 | 
						|
   *                                 to allow mocking of the MozLoopPushHandler.
 | 
						|
   * @returns {Promise} a promise that is resolved with no params on completion, or
 | 
						|
   *          rejected with an error code or string.
 | 
						|
   */
 | 
						|
  promiseRegisteredWithServers: function(mockPushHandler) {
 | 
						|
    if (this._registeredDeferred) {
 | 
						|
      return this._registeredDeferred.promise;
 | 
						|
    }
 | 
						|
 | 
						|
    this._registeredDeferred = Promise.defer();
 | 
						|
    // We grab the promise early in case .initialize or its results sets
 | 
						|
    // it back to null on error.
 | 
						|
    let result = this._registeredDeferred.promise;
 | 
						|
 | 
						|
    this._pushHandler = mockPushHandler || MozLoopPushHandler;
 | 
						|
 | 
						|
    this._pushHandler.initialize(this.onPushRegistered.bind(this),
 | 
						|
      this.onHandleNotification.bind(this));
 | 
						|
 | 
						|
    return result;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Performs a hawk based request to the loop server.
 | 
						|
   *
 | 
						|
   * @param {String} path The path to make the request to.
 | 
						|
   * @param {String} method The request method, e.g. 'POST', 'GET'.
 | 
						|
   * @param {Object} payloadObj An object which is converted to JSON and
 | 
						|
   *                            transmitted with the request.
 | 
						|
   */
 | 
						|
  hawkRequest: function(path, method, payloadObj) {
 | 
						|
    if (!this._hawkClient) {
 | 
						|
      this._hawkClient = new HawkClient(this.loopServerUri);
 | 
						|
    }
 | 
						|
 | 
						|
    let sessionToken;
 | 
						|
    try {
 | 
						|
      sessionToken = Services.prefs.getCharPref("loop.hawk-session-token");
 | 
						|
    } catch (x) {
 | 
						|
      // It is ok for this not to exist, we'll default to sending no-creds
 | 
						|
    }
 | 
						|
 | 
						|
    let credentials;
 | 
						|
    if (sessionToken) {
 | 
						|
      credentials = deriveHawkCredentials(sessionToken, "sessionToken", 2 * 32);
 | 
						|
    }
 | 
						|
 | 
						|
    return this._hawkClient.request(path, method, credentials, payloadObj);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Used to store a session token from a request if it exists in the headers.
 | 
						|
   *
 | 
						|
   * @param {Object} headers The request headers, which may include a
 | 
						|
   *                         "hawk-session-token" to be saved.
 | 
						|
   * @return true on success or no token, false on failure.
 | 
						|
   */
 | 
						|
  storeSessionToken: function(headers) {
 | 
						|
    let sessionToken = headers["hawk-session-token"];
 | 
						|
    if (sessionToken) {
 | 
						|
      // XXX should do more validation here
 | 
						|
      if (sessionToken.length === 64) {
 | 
						|
        Services.prefs.setCharPref("loop.hawk-session-token", sessionToken);
 | 
						|
      } else {
 | 
						|
        // XXX Bubble the precise details up to the UI somehow (bug 1013248).
 | 
						|
        console.warn("Loop server sent an invalid session token");
 | 
						|
        this._registeredDeferred.reject("session-token-wrong-size");
 | 
						|
        this._registeredDeferred = null;
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return true;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Callback from MozLoopPushHandler - The push server has been registered
 | 
						|
   * and has given us a push url.
 | 
						|
   *
 | 
						|
   * @param {String} pushUrl The push url given by the push server.
 | 
						|
   */
 | 
						|
  onPushRegistered: function(err, pushUrl) {
 | 
						|
    if (err) {
 | 
						|
      this._registeredDeferred.reject(err);
 | 
						|
      this._registeredDeferred = null;
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    this.registerWithLoopServer(pushUrl);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Registers with the Loop server.
 | 
						|
   *
 | 
						|
   * @param {String} pushUrl The push url given by the push server.
 | 
						|
   * @param {Boolean} noRetry Optional, don't retry if authentication fails.
 | 
						|
   */
 | 
						|
  registerWithLoopServer: function(pushUrl, noRetry) {
 | 
						|
    this.hawkRequest("/registration", "POST", { simple_push_url: pushUrl})
 | 
						|
      .then((response) => {
 | 
						|
        // If this failed we got an invalid token. storeSessionToken rejects
 | 
						|
        // the _registeredDeferred promise for us, so here we just need to
 | 
						|
        // early return.
 | 
						|
        if (!this.storeSessionToken(response.headers))
 | 
						|
          return;
 | 
						|
 | 
						|
        this.registeredLoopServer = true;
 | 
						|
        this._registeredDeferred.resolve();
 | 
						|
        // No need to clear the promise here, everything was good, so we don't need
 | 
						|
        // to re-register.
 | 
						|
      }, (error) => {
 | 
						|
        if (error.errno == 401) {
 | 
						|
          if (this.urlExpiryTimeIsInFuture()) {
 | 
						|
            // XXX Should this be reported to the user is a visible manner?
 | 
						|
            Cu.reportError("Loop session token is invalid, all previously "
 | 
						|
                           + "generated urls will no longer work.");
 | 
						|
          }
 | 
						|
 | 
						|
          // Authorization failed, invalid token, we need to try again with a new token.
 | 
						|
          Services.prefs.clearUserPref("loop.hawk-session-token");
 | 
						|
          this.registerWithLoopServer(pushUrl, true);
 | 
						|
          return;
 | 
						|
        }
 | 
						|
 | 
						|
        // XXX Bubble the precise details up to the UI somehow (bug 1013248).
 | 
						|
        Cu.reportError("Failed to register with the loop server. error: " + error);
 | 
						|
        this._registeredDeferred.reject(error.errno);
 | 
						|
        this._registeredDeferred = null;
 | 
						|
      }
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Callback from MozLoopPushHandler - A push notification has been received from
 | 
						|
   * the server.
 | 
						|
   *
 | 
						|
   * @param {String} version The version information from the server.
 | 
						|
   */
 | 
						|
  onHandleNotification: function(version) {
 | 
						|
    if (this.doNotDisturb) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    this.openChatWindow(null, "LooP", "about:loopconversation#incoming/" + version);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * A getter to obtain and store the strings for loop. This is structured
 | 
						|
   * for use by l10n.js.
 | 
						|
   *
 | 
						|
   * @returns {Object} a map of element ids with attributes to set.
 | 
						|
   */
 | 
						|
  get localizedStrings() {
 | 
						|
    if (this._localizedStrings)
 | 
						|
      return this._localizedStrings;
 | 
						|
 | 
						|
    var stringBundle =
 | 
						|
      Services.strings.createBundle('chrome://browser/locale/loop/loop.properties');
 | 
						|
 | 
						|
    var map = {};
 | 
						|
    var enumerator = stringBundle.getSimpleEnumeration();
 | 
						|
    while (enumerator.hasMoreElements()) {
 | 
						|
      var string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
 | 
						|
 | 
						|
      // 'textContent' is the default attribute to set if none are specified.
 | 
						|
      var key = string.key, property = 'textContent';
 | 
						|
      var i = key.lastIndexOf('.');
 | 
						|
      if (i >= 0) {
 | 
						|
        property = key.substring(i + 1);
 | 
						|
        key = key.substring(0, i);
 | 
						|
      }
 | 
						|
      if (!(key in map))
 | 
						|
        map[key] = {};
 | 
						|
      map[key][property] = string.value;
 | 
						|
    }
 | 
						|
 | 
						|
    return this._localizedStrings = map;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Opens the chat window
 | 
						|
   *
 | 
						|
   * @param {Object} contentWindow The window to open the chat window in, may
 | 
						|
   *                               be null.
 | 
						|
   * @param {String} title The title of the chat window.
 | 
						|
   * @param {String} url The page to load in the chat window.
 | 
						|
   * @param {String} mode May be "minimized" or undefined.
 | 
						|
   */
 | 
						|
  openChatWindow: function(contentWindow, title, url, mode) {
 | 
						|
    // So I guess the origin is the loop server!?
 | 
						|
    let origin = this.loopServerUri;
 | 
						|
    url = url.spec || url;
 | 
						|
 | 
						|
    let callback = chatbox => {
 | 
						|
      // We need to use DOMContentLoaded as otherwise the injection will happen
 | 
						|
      // in about:blank and then get lost.
 | 
						|
      // Sadly we can't use chatbox.promiseChatLoaded() as promise chaining
 | 
						|
      // involves event loop spins, which means it might be too late.
 | 
						|
      // Have we already done it?
 | 
						|
      if (chatbox.contentWindow.navigator.mozLoop) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      chatbox.addEventListener("DOMContentLoaded", function loaded(event) {
 | 
						|
        if (event.target != chatbox.contentDocument) {
 | 
						|
          return;
 | 
						|
        }
 | 
						|
        chatbox.removeEventListener("DOMContentLoaded", loaded, true);
 | 
						|
        injectLoopAPI(chatbox.contentWindow);
 | 
						|
      }, true);
 | 
						|
    };
 | 
						|
 | 
						|
    Chat.open(contentWindow, origin, title, url, undefined, undefined, callback);
 | 
						|
  }
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Public API
 | 
						|
 */
 | 
						|
this.MozLoopService = {
 | 
						|
  /**
 | 
						|
   * Initialized the loop service, and starts registration with the
 | 
						|
   * push and loop servers.
 | 
						|
   */
 | 
						|
  initialize: function() {
 | 
						|
    // If expiresTime is in the future then kick-off registration.
 | 
						|
    if (MozLoopServiceInternal.urlExpiryTimeIsInFuture()) {
 | 
						|
      this._startInitializeTimer();
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Internal function, exposed for testing purposes only. Used to start the
 | 
						|
   * initialize timer.
 | 
						|
   */
 | 
						|
  _startInitializeTimer: function() {
 | 
						|
    // Kick off the push notification service into registering after a timeout
 | 
						|
    // this ensures we're not doing too much straight after the browser's finished
 | 
						|
    // starting up.
 | 
						|
    this._initializeTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
 | 
						|
    this._initializeTimer.initWithCallback(function() {
 | 
						|
      this.register();
 | 
						|
      this._initializeTimer = null;
 | 
						|
    }.bind(this),
 | 
						|
    MozLoopServiceInternal.initialRegistrationDelayMilliseconds, Ci.nsITimer.TYPE_ONE_SHOT);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Starts registration of Loop with the push server, and then will register
 | 
						|
   * with the Loop server. It will return early if already registered.
 | 
						|
   *
 | 
						|
   * @param {Object} mockPushHandler Optional, test-only mock push handler. Used
 | 
						|
   *                                 to allow mocking of the MozLoopPushHandler.
 | 
						|
   * @returns {Promise} a promise that is resolved with no params on completion, or
 | 
						|
   *          rejected with an error code or string.
 | 
						|
   */
 | 
						|
  register: function(mockPushHandler) {
 | 
						|
    return MozLoopServiceInternal.promiseRegisteredWithServers(mockPushHandler);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Used to note a call url expiry time. If the time is later than the current
 | 
						|
   * latest expiry time, then the stored expiry time is increased. For times
 | 
						|
   * sooner, this function is a no-op; this ensures we always have the latest
 | 
						|
   * expiry time for a url.
 | 
						|
   *
 | 
						|
   * This is used to deterimine whether or not we should be registering with the
 | 
						|
   * push server on start.
 | 
						|
   *
 | 
						|
   * @param {Integer} expiryTimeSeconds The seconds since epoch of the expiry time
 | 
						|
   *                                    of the url.
 | 
						|
   */
 | 
						|
  noteCallUrlExpiry: function(expiryTimeSeconds) {
 | 
						|
    MozLoopServiceInternal.expiryTimeSeconds = expiryTimeSeconds;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Returns the strings for the specified element. Designed for use
 | 
						|
   * with l10n.js.
 | 
						|
   *
 | 
						|
   * @param {key} The element id to get strings for.
 | 
						|
   * @return {String} A JSON string containing the localized
 | 
						|
   *                  attribute/value pairs for the element.
 | 
						|
   */
 | 
						|
  getStrings: function(key) {
 | 
						|
      var stringData = MozLoopServiceInternal.localizedStrings;
 | 
						|
      if (!(key in stringData)) {
 | 
						|
        Cu.reportError('No string for key: ' + key + 'found');
 | 
						|
        return "";
 | 
						|
      }
 | 
						|
 | 
						|
      return JSON.stringify(stringData[key]);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Retrieves MozLoopService "do not disturb" value.
 | 
						|
   *
 | 
						|
   * @return {Boolean}
 | 
						|
   */
 | 
						|
  get doNotDisturb() {
 | 
						|
    return MozLoopServiceInternal.doNotDisturb;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Sets MozLoopService "do not disturb" value.
 | 
						|
   *
 | 
						|
   * @param {Boolean} aFlag
 | 
						|
   */
 | 
						|
  set doNotDisturb(aFlag) {
 | 
						|
    MozLoopServiceInternal.doNotDisturb = aFlag;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Returns the current locale
 | 
						|
   *
 | 
						|
   * @return {String} The code of the current locale.
 | 
						|
   */
 | 
						|
  get locale() {
 | 
						|
    try {
 | 
						|
      return Services.prefs.getComplexValue("general.useragent.locale",
 | 
						|
        Ci.nsISupportsString).data;
 | 
						|
    } catch (ex) {
 | 
						|
      return "en-US";
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Set any character preference under "loop.".
 | 
						|
   *
 | 
						|
   * @param {String} prefName The name of the pref without the preceding "loop."
 | 
						|
   * @param {String} value The value to set.
 | 
						|
   *
 | 
						|
   * Any errors thrown by the Mozilla pref API are logged to the console.
 | 
						|
   */
 | 
						|
  setLoopCharPref: function(prefName, value) {
 | 
						|
    try {
 | 
						|
      Services.prefs.setCharPref("loop." + prefName, value);
 | 
						|
    } catch (ex) {
 | 
						|
      console.log("setLoopCharPref had trouble setting " + prefName +
 | 
						|
        "; exception: " + ex);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Return any preference under "loop." that's coercible to a character
 | 
						|
   * preference.
 | 
						|
   *
 | 
						|
   * @param {String} prefName The name of the pref without the preceding
 | 
						|
   * "loop."
 | 
						|
   *
 | 
						|
   * Any errors thrown by the Mozilla pref API are logged to the console
 | 
						|
   * and cause null to be returned. This includes the case of the preference
 | 
						|
   * not being found.
 | 
						|
   *
 | 
						|
   * @return {String} on success, null on error
 | 
						|
   */
 | 
						|
  getLoopCharPref: function(prefName) {
 | 
						|
    try {
 | 
						|
      return Services.prefs.getCharPref("loop." + prefName);
 | 
						|
    } catch (ex) {
 | 
						|
      console.log("getLoopCharPref had trouble getting " + prefName +
 | 
						|
        "; exception: " + ex);
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
  }
 | 
						|
};
 |