forked from mirrors/gecko-dev
		
	 08997000eb
			
		
	
	
		08997000eb
		
	
	
	
	
		
			
			Backed out changeset 647025383676 (bug1202902) Backed out changeset d70c7fe532c6 (bug1202902)
		
			
				
	
	
		
			1923 lines
		
	
	
	
		
			67 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1923 lines
		
	
	
	
		
			67 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, results: Cr } = Components;
 | |
| 
 | |
| // Invalid auth token as per
 | |
| // https://github.com/mozilla-services/loop-server/blob/45787d34108e2f0d87d74d4ddf4ff0dbab23501c/loop/errno.json#L6
 | |
| const INVALID_AUTH_TOKEN = 110;
 | |
| 
 | |
| const LOOP_SESSION_TYPE = {
 | |
|   GUEST: 1,
 | |
|   FXA: 2
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Values that we segment 2-way media connection length telemetry probes
 | |
|  * into.
 | |
|  *
 | |
|  * @type {{SHORTER_THAN_10S: Number, BETWEEN_10S_AND_30S: Number,
 | |
|  *   BETWEEN_30S_AND_5M: Number, MORE_THAN_5M: Number}}
 | |
|  */
 | |
| const TWO_WAY_MEDIA_CONN_LENGTH = {
 | |
|   SHORTER_THAN_10S: 0,
 | |
|   BETWEEN_10S_AND_30S: 1,
 | |
|   BETWEEN_30S_AND_5M: 2,
 | |
|   MORE_THAN_5M: 3
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Values that we segment sharing state change telemetry probes into.
 | |
|  *
 | |
|  * @type {{WINDOW_ENABLED: Number, WINDOW_DISABLED: Number,
 | |
|  *   BROWSER_ENABLED: Number, BROWSER_DISABLED: Number}}
 | |
|  */
 | |
| const SHARING_STATE_CHANGE = {
 | |
|   WINDOW_ENABLED: 0,
 | |
|   WINDOW_DISABLED: 1,
 | |
|   BROWSER_ENABLED: 2,
 | |
|   BROWSER_DISABLED: 3
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Values that we segment sharing a room URL action telemetry probes into.
 | |
|  *
 | |
|  * @type {{COPY_FROM_PANEL: Number, COPY_FROM_CONVERSATION: Number,
 | |
|  *   EMAIL_FROM_CALLFAILED: Number, EMAIL_FROM_CONVERSATION: Number}}
 | |
|  */
 | |
| const SHARING_ROOM_URL = {
 | |
|   COPY_FROM_PANEL: 0,
 | |
|   COPY_FROM_CONVERSATION: 1,
 | |
|   EMAIL_FROM_CALLFAILED: 2,
 | |
|   EMAIL_FROM_CONVERSATION: 3
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Values that we segment room create action telemetry probes into.
 | |
|  *
 | |
|  * @type {{CREATE_SUCCESS: Number, CREATE_FAIL: Number}}
 | |
|  */
 | |
| const ROOM_CREATE = {
 | |
|   CREATE_SUCCESS: 0,
 | |
|   CREATE_FAIL: 1
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Values that we segment room delete action telemetry probes into.
 | |
|  *
 | |
|  * @type {{DELETE_SUCCESS: Number, DELETE_FAIL: Number}}
 | |
|  */
 | |
| const ROOM_DELETE = {
 | |
|   DELETE_SUCCESS: 0,
 | |
|   DELETE_FAIL: 1
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Values that we segment room context action telemetry probes into.
 | |
|  *
 | |
|  * @type {{ADD_FROM_PANEL: Number, ADD_FROM_CONVERSATION: Number}}
 | |
|  */
 | |
| const ROOM_CONTEXT_ADD = {
 | |
|   ADD_FROM_PANEL: 0,
 | |
|   ADD_FROM_CONVERSATION: 1
 | |
| };
 | |
| 
 | |
| // See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error".
 | |
| const PREF_LOG_LEVEL = "loop.debug.loglevel";
 | |
| 
 | |
| const kChatboxHangupButton = {
 | |
|   id: "loop-hangup",
 | |
|   visibleWhenUndocked: false,
 | |
|   onCommand: function(e, chatbox) {
 | |
|     let window = chatbox.content.contentWindow;
 | |
|     let event = new window.CustomEvent("LoopHangupNow");
 | |
|     window.dispatchEvent(event);
 | |
|   }
 | |
| };
 | |
| 
 | |
| Cu.import("resource://gre/modules/Services.jsm");
 | |
| Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 | |
| Cu.import("resource://gre/modules/Promise.jsm");
 | |
| Cu.import("resource://gre/modules/osfile.jsm", this);
 | |
| Cu.import("resource://gre/modules/Task.jsm");
 | |
| Cu.import("resource://gre/modules/Timer.jsm");
 | |
| Cu.import("resource://gre/modules/FxAccountsOAuthClient.jsm");
 | |
| 
 | |
| Cu.importGlobalProperties(["URL"]);
 | |
| 
 | |
| this.EXPORTED_SYMBOLS = ["MozLoopService", "LOOP_SESSION_TYPE",
 | |
|   "TWO_WAY_MEDIA_CONN_LENGTH", "SHARING_STATE_CHANGE", "SHARING_ROOM_URL",
 | |
|   "ROOM_CREATE", "ROOM_DELETE", "ROOM_CONTEXT_ADD"];
 | |
| 
 | |
| XPCOMUtils.defineConstant(this, "LOOP_SESSION_TYPE", LOOP_SESSION_TYPE);
 | |
| XPCOMUtils.defineConstant(this, "TWO_WAY_MEDIA_CONN_LENGTH", TWO_WAY_MEDIA_CONN_LENGTH);
 | |
| XPCOMUtils.defineConstant(this, "SHARING_STATE_CHANGE", SHARING_STATE_CHANGE);
 | |
| XPCOMUtils.defineConstant(this, "SHARING_ROOM_URL", SHARING_ROOM_URL);
 | |
| XPCOMUtils.defineConstant(this, "ROOM_CREATE", ROOM_CREATE);
 | |
| XPCOMUtils.defineConstant(this, "ROOM_DELETE", ROOM_DELETE);
 | |
| XPCOMUtils.defineConstant(this, "ROOM_CONTEXT_ADD", ROOM_CONTEXT_ADD);
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI",
 | |
|   "resource:///modules/loop/MozLoopAPI.jsm");
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "convertToRTCStatsReport",
 | |
|   "resource://gre/modules/media/RTCStatsReport.jsm");
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "loopUtils",
 | |
|   "resource:///modules/loop/utils.js", "utils");
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "loopCrypto",
 | |
|   "resource:///modules/loop/crypto.js", "LoopCrypto");
 | |
| 
 | |
| 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, "FxAccountsProfileClient",
 | |
|                                   "resource://gre/modules/FxAccountsProfileClient.jsm");
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "HawkClient",
 | |
|                                   "resource://services-common/hawkclient.js");
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "deriveHawkCredentials",
 | |
|                                   "resource://services-common/hawkrequest.js");
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "LoopContacts",
 | |
|                                   "resource:///modules/loop/LoopContacts.jsm");
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
 | |
|                                   "resource:///modules/loop/LoopStorage.jsm");
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "LoopCalls",
 | |
|                                   "resource:///modules/loop/LoopCalls.jsm");
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "LoopRooms",
 | |
|                                   "resource:///modules/loop/LoopRooms.jsm");
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "roomsPushNotification",
 | |
|                                   "resource:///modules/loop/LoopRooms.jsm");
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "MozLoopPushHandler",
 | |
|                                   "resource:///modules/loop/MozLoopPushHandler.jsm");
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "UITour",
 | |
|                                   "resource:///modules/UITour.jsm");
 | |
| 
 | |
| XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
 | |
|                                    "@mozilla.org/uuid-generator;1",
 | |
|                                    "nsIUUIDGenerator");
 | |
| 
 | |
| XPCOMUtils.defineLazyServiceGetter(this, "gDNSService",
 | |
|                                    "@mozilla.org/network/dns-service;1",
 | |
|                                    "nsIDNSService");
 | |
| 
 | |
| XPCOMUtils.defineLazyServiceGetter(this, "gWM",
 | |
|                                    "@mozilla.org/appshell/window-mediator;1",
 | |
|                                    "nsIWindowMediator");
 | |
| 
 | |
| // Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
 | |
| XPCOMUtils.defineLazyGetter(this, "log", () => {
 | |
|   let ConsoleAPI = Cu.import("resource://gre/modules/devtools/shared/Console.jsm", {}).ConsoleAPI;
 | |
|   let consoleOptions = {
 | |
|     maxLogLevelPref: PREF_LOG_LEVEL,
 | |
|     prefix: "Loop"
 | |
|   };
 | |
|   return new ConsoleAPI(consoleOptions);
 | |
| });
 | |
| 
 | |
| function setJSONPref(aName, aValue) {
 | |
|   let value = aValue ? JSON.stringify(aValue) : "";
 | |
|   Services.prefs.setCharPref(aName, value);
 | |
| }
 | |
| 
 | |
| function getJSONPref(aName) {
 | |
|   let value = Services.prefs.getCharPref(aName);
 | |
|   return value ? JSON.parse(value) : null;
 | |
| }
 | |
| 
 | |
| var gHawkClient = null;
 | |
| var gLocalizedStrings = new Map();
 | |
| var gFxAEnabled = true;
 | |
| var gFxAOAuthClientPromise = null;
 | |
| var gFxAOAuthClient = null;
 | |
| var gErrors = new Map();
 | |
| var gLastWindowId = 0;
 | |
| var gConversationWindowData = new Map();
 | |
| 
 | |
| /**
 | |
|  * 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.
 | |
|  */
 | |
| var MozLoopServiceInternal = {
 | |
|   conversationContexts: new Map(),
 | |
|   pushURLs: new Map(),
 | |
| 
 | |
|   mocks: {
 | |
|     pushHandler: undefined,
 | |
|     isChatWindowOpen: undefined
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * The current deferreds for the registration processes. This is set if in progress
 | |
|    * or the registration was successful. This is null if a registration attempt was
 | |
|    * unsuccessful.
 | |
|    */
 | |
|   deferredRegistrations: new Map(),
 | |
| 
 | |
|   get pushHandler() {
 | |
|     return this.mocks.pushHandler || MozLoopPushHandler;
 | |
|   },
 | |
| 
 | |
|   // The uri of the Loop server.
 | |
|   get loopServerUri() {
 | |
|     return Services.prefs.getCharPref("loop.server");
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Retrieves MozLoopService Firefox Accounts OAuth token.
 | |
|    *
 | |
|    * @return {Object} OAuth token
 | |
|    */
 | |
|   get fxAOAuthTokenData() {
 | |
|     return getJSONPref("loop.fxa_oauth.tokendata");
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Sets MozLoopService Firefox Accounts OAuth token.
 | |
|    * If the tokenData is being cleared, will also clear the
 | |
|    * profile since the profile is dependent on the token data.
 | |
|    *
 | |
|    * @param {Object} aTokenData OAuth token
 | |
|    */
 | |
|   set fxAOAuthTokenData(aTokenData) {
 | |
|     setJSONPref("loop.fxa_oauth.tokendata", aTokenData);
 | |
|     if (!aTokenData) {
 | |
|       this.fxAOAuthProfile = null;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Sets MozLoopService Firefox Accounts Profile data.
 | |
|    *
 | |
|    * @param {Object} aProfileData Profile data
 | |
|    */
 | |
|   set fxAOAuthProfile(aProfileData) {
 | |
|     setJSONPref("loop.fxa_oauth.profile", aProfileData);
 | |
|     this.notifyStatusChanged(aProfileData ? "login" : undefined);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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));
 | |
|     this.notifyStatusChanged();
 | |
|   },
 | |
| 
 | |
|   notifyStatusChanged: function(aReason = null) {
 | |
|     log.debug("notifyStatusChanged with reason:", aReason);
 | |
|     let profile = MozLoopService.userProfile;
 | |
|     LoopStorage.switchDatabase(profile && profile.uid);
 | |
|     LoopRooms.maybeRefresh(profile && profile.uid);
 | |
|     Services.obs.notifyObservers(null, "loop-status-changed", aReason);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Record an error and notify interested UI with the relevant user-facing strings attached.
 | |
|    *
 | |
|    * @param {String} errorType a key to identify the type of error. Only one
 | |
|    *                           error of a type will be saved at a time. This value may be used to
 | |
|    *                           determine user-facing (aka. friendly) strings.
 | |
|    * @param {Object} error     an object describing the error in the format from Hawk errors
 | |
|    * @param {Function} [actionCallback] an object describing the label and callback function for error
 | |
|    *                                    bar's button e.g. to retry.
 | |
|    */
 | |
|   setError: function(errorType, error, actionCallback = null) {
 | |
|     log.debug("setError", errorType, error);
 | |
|     log.trace();
 | |
|     let messageString, detailsString, detailsButtonLabelString, detailsButtonCallback;
 | |
|     const NETWORK_ERRORS = [
 | |
|       Cr.NS_ERROR_CONNECTION_REFUSED,
 | |
|       Cr.NS_ERROR_NET_INTERRUPT,
 | |
|       Cr.NS_ERROR_NET_RESET,
 | |
|       Cr.NS_ERROR_NET_TIMEOUT,
 | |
|       Cr.NS_ERROR_OFFLINE,
 | |
|       Cr.NS_ERROR_PROXY_CONNECTION_REFUSED,
 | |
|       Cr.NS_ERROR_UNKNOWN_HOST,
 | |
|       Cr.NS_ERROR_UNKNOWN_PROXY_HOST
 | |
|     ];
 | |
| 
 | |
|     if (error.code === null && error.errno === null &&
 | |
|         error.error instanceof Ci.nsIException &&
 | |
|         NETWORK_ERRORS.indexOf(error.error.result) != -1) {
 | |
|       // Network error. Override errorType so we can easily clear it on the next succesful request.
 | |
|       errorType = "network";
 | |
|       messageString = "could_not_connect";
 | |
|       detailsString = "check_internet_connection";
 | |
|       detailsButtonLabelString = "retry_button";
 | |
|     } else if (errorType == "profile" && error.code >= 500 && error.code < 600) {
 | |
|       messageString = "problem_accessing_account";
 | |
|     } else if (error.code == 401) {
 | |
|       if (errorType == "login") {
 | |
|         messageString = "could_not_authenticate"; // XXX: Bug 1076377
 | |
|         detailsString = "password_changed_question";
 | |
|         detailsButtonLabelString = "retry_button";
 | |
|         detailsButtonCallback = () => MozLoopService.logInToFxA();
 | |
|       } else {
 | |
|         messageString = "session_expired_error_description";
 | |
|       }
 | |
|     } else if (error.code >= 500 && error.code < 600) {
 | |
|       messageString = "service_not_available";
 | |
|       detailsString = "try_again_later";
 | |
|       detailsButtonLabelString = "retry_button";
 | |
|     } else {
 | |
|       messageString = "generic_failure_message";
 | |
|     }
 | |
| 
 | |
|     error.friendlyMessage = this.localizedStrings.get(messageString);
 | |
| 
 | |
|     // Default to the generic "retry_button" text even though the button won't be shown if
 | |
|     // error.friendlyDetails is null.
 | |
|     error.friendlyDetailsButtonLabel = detailsButtonLabelString ?
 | |
|                                          this.localizedStrings.get(detailsButtonLabelString) :
 | |
|                                          this.localizedStrings.get("retry_button");
 | |
| 
 | |
|     error.friendlyDetailsButtonCallback = actionCallback || detailsButtonCallback || null;
 | |
| 
 | |
|     if (detailsString) {
 | |
|       error.friendlyDetails = this.localizedStrings.get(detailsString);
 | |
|     } else if (error.friendlyDetailsButtonCallback) {
 | |
|       // If we have a retry callback but no details use the generic try again string.
 | |
|       error.friendlyDetails = this.localizedStrings.get("generic_failure_no_reason2");
 | |
|     } else {
 | |
|       error.friendlyDetails = null;
 | |
|     }
 | |
| 
 | |
|     gErrors.set(errorType, error);
 | |
|     this.notifyStatusChanged();
 | |
|   },
 | |
| 
 | |
|   clearError: function(errorType) {
 | |
|     if (gErrors.has(errorType)) {
 | |
|       gErrors.delete(errorType);
 | |
|       this.notifyStatusChanged();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   get errors() {
 | |
|     return gErrors;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Create a notification channel between the LoopServer and this client
 | |
|    * via a PushServer. Once created, any subsequent changes in the pushURL
 | |
|    * assigned by the PushServer will be communicated to the LoopServer.
 | |
|    * with the Loop server. It will return early if already registered.
 | |
|    *
 | |
|    * @param {String} channelID Unique identifier for the notification channel
 | |
|    *                 registered with the PushServer.
 | |
|    * @param {LOOP_SESSION_TYPE} sessionType
 | |
|    * @param {String} serviceType Either 'calls' or 'rooms'.
 | |
|    * @param {Function} onNotification Callback function that will be associated
 | |
|    *                   with this channel from the PushServer.
 | |
|    * @returns {Promise} A promise that is resolved with no params on completion, or
 | |
|    *                    rejected with an error code or string.
 | |
|    */
 | |
|   createNotificationChannel: function(channelID, sessionType, serviceType, onNotification) {
 | |
|     log.debug("createNotificationChannel", channelID, sessionType, serviceType);
 | |
|     // Wrap the push notification registration callback in a Promise.
 | |
|     return new Promise((resolve, reject) => {
 | |
|       let onRegistered = (error, pushURL, chID) => {
 | |
|         log.debug("createNotificationChannel onRegistered:", error, pushURL, chID);
 | |
|         if (error) {
 | |
|           reject(Error(error));
 | |
|         } else {
 | |
|           resolve(this.registerWithLoopServer(sessionType, serviceType, pushURL));
 | |
|         }
 | |
|       };
 | |
| 
 | |
|       this.pushHandler.register(channelID, onRegistered, onNotification);
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Starts registration of Loop with the PushServer and the LoopServer.
 | |
|    * Successful PushServer registration will automatically trigger the registration
 | |
|    * of the PushURL returned by the PushServer with the LoopServer. If the registration
 | |
|    * chain has already been set up, this function will simply resolve.
 | |
|    *
 | |
|    * @param {LOOP_SESSION_TYPE} sessionType
 | |
|    * @returns {Promise} a promise that is resolved with no params on completion, or
 | |
|    *          rejected with an error code or string.
 | |
|    */
 | |
|   promiseRegisteredWithServers: function(sessionType = LOOP_SESSION_TYPE.GUEST) {
 | |
|     if (sessionType !== LOOP_SESSION_TYPE.GUEST && sessionType !== LOOP_SESSION_TYPE.FXA) {
 | |
|       return Promise.reject(new Error("promiseRegisteredWithServers: Invalid sessionType"));
 | |
|     }
 | |
| 
 | |
|     if (this.deferredRegistrations.has(sessionType)) {
 | |
|       log.debug("promiseRegisteredWithServers: registration already completed or in progress:",
 | |
|                 sessionType);
 | |
|       return this.deferredRegistrations.get(sessionType);
 | |
|     }
 | |
| 
 | |
|     let options = this.mocks.webSocket ? { mockWebSocket: this.mocks.webSocket } : {};
 | |
|     this.pushHandler.initialize(options); // This can be called more than once.
 | |
| 
 | |
|     let regPromise;
 | |
|     if (sessionType == LOOP_SESSION_TYPE.GUEST) {
 | |
|       regPromise = this.createNotificationChannel(
 | |
|         MozLoopService.channelIDs.roomsGuest, sessionType, "rooms",
 | |
|         roomsPushNotification);
 | |
|     } else {
 | |
|       regPromise = this.createNotificationChannel(
 | |
|         MozLoopService.channelIDs.callsFxA, sessionType, "calls",
 | |
|         LoopCalls.onNotification).then(() => {
 | |
|           return this.createNotificationChannel(
 | |
|             MozLoopService.channelIDs.roomsFxA, sessionType, "rooms",
 | |
|             roomsPushNotification);
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     log.debug("assigning to deferredRegistrations for sessionType:", sessionType);
 | |
|     this.deferredRegistrations.set(sessionType, regPromise);
 | |
| 
 | |
|     // Do not return the new Promise generated by this catch() invocation.
 | |
|     // This will be called along with any other onReject function attached to regPromise.
 | |
|     regPromise.catch((error) => {
 | |
|       log.error("Failed to register with Loop server with sessionType ", sessionType, error);
 | |
|       this.deferredRegistrations.delete(sessionType);
 | |
|       log.debug("Cleared deferredRegistration for sessionType:", sessionType);
 | |
|     });
 | |
| 
 | |
|     return regPromise;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Registers with the Loop server either as a guest or a FxA user.
 | |
|    *
 | |
|    * @private
 | |
|    * @param {LOOP_SESSION_TYPE} sessionType The type of session e.g. guest or FxA
 | |
|    * @param {String} serviceType: "rooms" or "calls"
 | |
|    * @param {Boolean} [retry=true] Whether to retry if authentication fails.
 | |
|    * @return {Promise} resolves to pushURL or rejects with an Error
 | |
|    */
 | |
|   registerWithLoopServer: function(sessionType, serviceType, pushURL, retry = true) {
 | |
|     log.debug("registerWithLoopServer with sessionType:", sessionType, serviceType, retry);
 | |
|     if (!pushURL || !sessionType || !serviceType) {
 | |
|       return Promise.reject(new Error("Invalid or missing parameters for registerWithLoopServer"));
 | |
|     }
 | |
| 
 | |
|     let pushURLs = this.pushURLs.get(sessionType);
 | |
| 
 | |
|     // Create a blank URL record set if none exists for this sessionType.
 | |
|     if (!pushURLs) {
 | |
|       pushURLs = { calls: undefined, rooms: undefined };
 | |
|       this.pushURLs.set(sessionType, pushURLs);
 | |
|     }
 | |
| 
 | |
|     if (pushURLs[serviceType] == pushURL) {
 | |
|       return Promise.resolve(pushURL);
 | |
|     }
 | |
| 
 | |
|     let newURLs = {calls: pushURLs.calls,
 | |
|                    rooms: pushURLs.rooms};
 | |
|     newURLs[serviceType] = pushURL;
 | |
| 
 | |
|     return this.hawkRequestInternal(sessionType, "/registration", "POST",
 | |
|                                     { simplePushURLs: newURLs }).then(
 | |
|       (response) => {
 | |
|         // If this failed we got an invalid token.
 | |
|         if (!this.storeSessionToken(sessionType, response.headers)) {
 | |
|           throw new Error("session-token-wrong-size");
 | |
|         }
 | |
| 
 | |
|         // Record the new push URL
 | |
|         pushURLs[serviceType] = pushURL;
 | |
|         log.debug("Successfully registered with server for sessionType", sessionType);
 | |
|         this.clearError("registration");
 | |
|         return pushURL;
 | |
|       }, (error) => {
 | |
|         // There's other errors than invalid auth token, but we should only do the reset
 | |
|         // as a last resort.
 | |
|         if (error.code === 401) {
 | |
|           // Authorization failed, invalid token, we need to try again with a new token.
 | |
|           // XXX (pkerr) - Why is there a retry here? This will not clear up a hawk session
 | |
|           // token problem at this level.
 | |
|           if (retry) {
 | |
|             return this.registerWithLoopServer(sessionType, serviceType, pushURL, false);
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         log.error("Failed to register with the loop server. Error: ", error);
 | |
|         throw error;
 | |
|       }
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Unregisters from the Loop server either as a guest or a FxA user.
 | |
|    *
 | |
|    * This is normally only wanted for FxA users as we normally want to keep the
 | |
|    * guest session with the device.
 | |
|    *
 | |
|    * NOTE: It is the responsibiliy of the caller the clear the session token
 | |
|    * after all of the notification classes: calls and rooms, for either
 | |
|    * Guest or FxA have been unregistered with the LoopServer.
 | |
|    *
 | |
|    * @param {LOOP_SESSION_TYPE} sessionType The type of session e.g. guest or FxA
 | |
|    * @return {Promise} resolving when the unregistration request finishes
 | |
|    */
 | |
|   unregisterFromLoopServer: function(sessionType) {
 | |
|     let prefType = Services.prefs.getPrefType(this.getSessionTokenPrefName(sessionType));
 | |
|     if (prefType == Services.prefs.PREF_INVALID) {
 | |
|       log.debug("already unregistered from LoopServer", sessionType);
 | |
|       return Promise.resolve("already unregistered");
 | |
|     }
 | |
| 
 | |
|     let error,
 | |
|         pushURLs = this.pushURLs.get(sessionType),
 | |
|         callsPushURL = pushURLs ? pushURLs.calls : null,
 | |
|         roomsPushURL = pushURLs ? pushURLs.rooms : null;
 | |
|     this.pushURLs.delete(sessionType);
 | |
| 
 | |
|     let unregister = (sessType, pushURL) => {
 | |
|       if (!pushURL) {
 | |
|         return Promise.resolve("no pushURL of this type to unregister");
 | |
|       }
 | |
| 
 | |
|       let unregisterURL = "/registration?simplePushURL=" + encodeURIComponent(pushURL);
 | |
|       return this.hawkRequestInternal(sessType, unregisterURL, "DELETE").then(
 | |
|         () => {
 | |
|           log.debug("Successfully unregistered from server for sessionType = ", sessType);
 | |
|           return "unregistered sessionType " + sessType;
 | |
|         },
 | |
|         err => {
 | |
|           if (err.code === 401) {
 | |
|             // Authorization failed, invalid token. This is fine since it may mean we already logged out.
 | |
|             log.debug("already unregistered - invalid token", sessType);
 | |
|             return "already unregistered, sessionType = " + sessType;
 | |
|           }
 | |
| 
 | |
|           log.error("Failed to unregister with the loop server. Error: ", error);
 | |
|           throw err;
 | |
|         });
 | |
|     };
 | |
| 
 | |
|     return Promise.all([unregister(sessionType, callsPushURL), unregister(sessionType, roomsPushURL)]);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Performs a hawk based request to the loop server - there is no pre-registration
 | |
|    * for this request, if this is required, use hawkRequest.
 | |
|    *
 | |
|    * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request.
 | |
|    *                                        This is one of the LOOP_SESSION_TYPE members.
 | |
|    * @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.
 | |
|    * @param {Boolean} [retryOn401=true] Whether to retry if authentication fails.
 | |
|    * @returns {Promise}
 | |
|    *        Returns a promise that resolves to the response of the API call,
 | |
|    *        or is rejected with an error.  If the server response can be parsed
 | |
|    *        as JSON and contains an 'error' property, the promise will be
 | |
|    *        rejected with this JSON-parsed response.
 | |
|    */
 | |
|   hawkRequestInternal: function(sessionType, path, method, payloadObj, retryOn401 = true) {
 | |
|     log.debug("hawkRequestInternal: ", sessionType, path, method);
 | |
|     if (!gHawkClient) {
 | |
|       gHawkClient = new HawkClient(this.loopServerUri);
 | |
|     }
 | |
| 
 | |
|     let sessionToken, credentials;
 | |
|     try {
 | |
|       sessionToken = Services.prefs.getCharPref(this.getSessionTokenPrefName(sessionType));
 | |
|     } catch (x) {
 | |
|       // It is ok for this not to exist, we'll default to sending no-creds
 | |
|     }
 | |
| 
 | |
|     if (sessionToken) {
 | |
|       // true = use a hex key, as required by the server (see bug 1032738).
 | |
|       credentials = deriveHawkCredentials(sessionToken, "sessionToken",
 | |
|                                           2 * 32, true);
 | |
|     }
 | |
| 
 | |
|     if (payloadObj) {
 | |
|       // Note: we must copy the object rather than mutate it, to avoid
 | |
|       // mutating the values of the object passed in.
 | |
|       let newPayloadObj = {};
 | |
|       for (let property of Object.getOwnPropertyNames(payloadObj)) {
 | |
|         if (typeof payloadObj[property] == "string") {
 | |
|           newPayloadObj[property] = CommonUtils.encodeUTF8(payloadObj[property]);
 | |
|         } else {
 | |
|           newPayloadObj[property] = payloadObj[property];
 | |
|         }
 | |
|       }
 | |
|       payloadObj = newPayloadObj;
 | |
|     }
 | |
| 
 | |
|     let handle401Error = (error) => {
 | |
|       if (sessionType === LOOP_SESSION_TYPE.FXA) {
 | |
|         return MozLoopService.logOutFromFxA().then(() => {
 | |
|           // Set a user-visible error after logOutFromFxA clears existing ones.
 | |
|           this.setError("login", error);
 | |
|           throw error;
 | |
|         });
 | |
|       }
 | |
|       this.setError("registration", error);
 | |
|       throw error;
 | |
|     };
 | |
| 
 | |
|     return gHawkClient.request(path, method, credentials, payloadObj).then(
 | |
|       (result) => {
 | |
|         this.clearError("network");
 | |
|         return result;
 | |
|       },
 | |
|       (error) => {
 | |
|       if (error.code && error.code == 401) {
 | |
|         this.clearSessionToken(sessionType);
 | |
|         if (retryOn401 && sessionType === LOOP_SESSION_TYPE.GUEST) {
 | |
|           log.info("401 and INVALID_AUTH_TOKEN - retry registration");
 | |
|           return this.registerWithLoopServer(sessionType, false).then(
 | |
|             () => {
 | |
|               return this.hawkRequestInternal(sessionType, path, method, payloadObj, false);
 | |
|             },
 | |
|             () => {
 | |
|               // Process the original error that triggered the retry.
 | |
|               return handle401Error(error);
 | |
|             }
 | |
|           );
 | |
|         }
 | |
|         return handle401Error(error);
 | |
|       }
 | |
|       throw error;
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Performs a hawk based request to the loop server, registering if necessary.
 | |
|    *
 | |
|    * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request.
 | |
|    *                                        This is one of the LOOP_SESSION_TYPE members.
 | |
|    * @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.
 | |
|    * @returns {Promise}
 | |
|    *        Returns a promise that resolves to the response of the API call,
 | |
|    *        or is rejected with an error.  If the server response can be parsed
 | |
|    *        as JSON and contains an 'error' property, the promise will be
 | |
|    *        rejected with this JSON-parsed response.
 | |
|    */
 | |
|   hawkRequest: function(sessionType, path, method, payloadObj) {
 | |
|     log.debug("hawkRequest: " + path, sessionType);
 | |
|     return new Promise((resolve, reject) => {
 | |
|       MozLoopService.promiseRegisteredWithServers(sessionType).then(() => {
 | |
|         this.hawkRequestInternal(sessionType, path, method, payloadObj).then(resolve, reject);
 | |
|       }, err => {
 | |
|         reject(err);
 | |
|       }).catch(reject);
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Generic hawkRequest onError handler for the hawkRequest promise.
 | |
|    *
 | |
|    * @param {Object} error - error reporting object
 | |
|    *
 | |
|    */
 | |
| 
 | |
|   _hawkRequestError: function(error) {
 | |
|     log.error("Loop hawkRequest error:", error);
 | |
|     throw error;
 | |
|   },
 | |
| 
 | |
|   getSessionTokenPrefName: function(sessionType) {
 | |
|     let suffix;
 | |
|     switch (sessionType) {
 | |
|       case LOOP_SESSION_TYPE.GUEST:
 | |
|         suffix = "";
 | |
|         break;
 | |
|       case LOOP_SESSION_TYPE.FXA:
 | |
|         suffix = ".fxa";
 | |
|         break;
 | |
|       default:
 | |
|         throw new Error("Unknown LOOP_SESSION_TYPE");
 | |
|     }
 | |
|     return "loop.hawk-session-token" + suffix;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Used to store a session token from a request if it exists in the headers.
 | |
|    *
 | |
|    * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request.
 | |
|    *                                        One of the LOOP_SESSION_TYPE members.
 | |
|    * @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(sessionType, headers) {
 | |
|     let sessionToken = headers["hawk-session-token"];
 | |
|     if (sessionToken) {
 | |
|       // XXX should do more validation here
 | |
|       if (sessionToken.length === 64) {
 | |
|         Services.prefs.setCharPref(this.getSessionTokenPrefName(sessionType), sessionToken);
 | |
|         log.debug("Stored a hawk session token for sessionType", sessionType);
 | |
|       } else {
 | |
|         // XXX Bubble the precise details up to the UI somehow (bug 1013248).
 | |
|         log.warn("Loop server sent an invalid session token");
 | |
|         return false;
 | |
|       }
 | |
|     }
 | |
|     return true;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Clear the loop session token so we don't use it for Hawk Requests anymore.
 | |
|    *
 | |
|    * This should normally be used after unregistering with the server so it can
 | |
|    * clean up session state first.
 | |
|    *
 | |
|    * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request.
 | |
|    *                                        One of the LOOP_SESSION_TYPE members.
 | |
|    */
 | |
|   clearSessionToken: function(sessionType) {
 | |
|     Services.prefs.clearUserPref(this.getSessionTokenPrefName(sessionType));
 | |
|     log.debug("Cleared hawk session token for sessionType", sessionType);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * A getter to obtain and store the strings for loop. This is structured
 | |
|    * for use by l10n.js.
 | |
|    *
 | |
|    * @returns {Map} a map of element ids with localized string values
 | |
|    */
 | |
|   get localizedStrings() {
 | |
|     if (gLocalizedStrings.size) {
 | |
|       return gLocalizedStrings;
 | |
|     }
 | |
| 
 | |
|     let stringBundle =
 | |
|       Services.strings.createBundle("chrome://browser/locale/loop/loop.properties");
 | |
| 
 | |
|     let enumerator = stringBundle.getSimpleEnumeration();
 | |
|     while (enumerator.hasMoreElements()) {
 | |
|       let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
 | |
|       gLocalizedStrings.set(string.key, string.value);
 | |
|     }
 | |
|     // Supply the strings from the branding bundle on a per-need basis.
 | |
|     let brandBundle =
 | |
|       Services.strings.createBundle("chrome://branding/locale/brand.properties");
 | |
|     // Unfortunately the `brandShortName` string is used by Loop with a lowercase 'N'.
 | |
|     gLocalizedStrings.set("brandShortname", brandBundle.GetStringFromName("brandShortName"));
 | |
| 
 | |
|     return gLocalizedStrings;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Saves loop logs to the saved-telemetry-pings folder.
 | |
|    *
 | |
|    * @param {Object} pc The peerConnection in question.
 | |
|    */
 | |
|   stageForTelemetryUpload: function(window, pc) {
 | |
|     window.WebrtcGlobalInformation.getAllStats(allStats => {
 | |
|       let internalFormat = allStats.reports[0]; // filtered on pc.id
 | |
|       window.WebrtcGlobalInformation.getLogging("", logs => {
 | |
|         let report = convertToRTCStatsReport(internalFormat);
 | |
|         let logStr = "";
 | |
|         logs.forEach(s => { logStr += s + "\n"; });
 | |
| 
 | |
|         // We have stats and logs.
 | |
| 
 | |
|         // Create worker job. ping = saved telemetry ping file header + payload
 | |
|         //
 | |
|         // Prepare payload according to https://wiki.mozilla.org/Loop/Telemetry
 | |
| 
 | |
|         let ai = Services.appinfo;
 | |
|         let uuid = uuidgen.generateUUID().toString();
 | |
|         uuid = uuid.substr(1, uuid.length - 2); // remove uuid curly braces
 | |
| 
 | |
|         let directory = OS.Path.join(OS.Constants.Path.profileDir,
 | |
|                                      "saved-telemetry-pings");
 | |
|         let job = {
 | |
|           directory: directory,
 | |
|           filename: uuid + ".json",
 | |
|           ping: {
 | |
|             reason: "loop",
 | |
|             slug: uuid,
 | |
|             payload: {
 | |
|               ver: 1,
 | |
|               info: {
 | |
|                 appUpdateChannel: ai.defaultUpdateChannel,
 | |
|                 appBuildID: ai.appBuildID,
 | |
|                 appName: ai.name,
 | |
|                 appVersion: ai.version,
 | |
|                 reason: "loop",
 | |
|                 OS: ai.OS,
 | |
|                 version: Services.sysinfo.getProperty("version")
 | |
|               },
 | |
|               report: "ice failure",
 | |
|               connectionstate: pc.iceConnectionState,
 | |
|               stats: report,
 | |
|               localSdp: internalFormat.localSdp,
 | |
|               remoteSdp: internalFormat.remoteSdp,
 | |
|               log: logStr
 | |
|             }
 | |
|           }
 | |
|         };
 | |
| 
 | |
|         // Send job to worker to do log sanitation, transcoding and saving to
 | |
|         // disk for pickup by telemetry on next startup, which then uploads it.
 | |
| 
 | |
|         let worker = new ChromeWorker("MozLoopWorker.js");
 | |
|         worker.onmessage = function(e) {
 | |
|           log.info(e.data.ok ?
 | |
|             "Successfully staged loop report for telemetry upload." :
 | |
|             ("Failed to stage loop report. Error: " + e.data.fail));
 | |
|         };
 | |
|         worker.postMessage(job);
 | |
|       });
 | |
|     }, pc.id);
 | |
|   },
 | |
| 
 | |
|   getChatWindowID: function(conversationWindowData) {
 | |
|     // Try getting a window ID that can (re-)identify this conversation, or resort
 | |
|     // to a globally unique one as a last resort.
 | |
|     // XXX We can clean this up once rooms and direct contact calling are the only
 | |
|     //     two modes left.
 | |
|     let windowId = ("contact" in conversationWindowData) ?
 | |
|                    conversationWindowData.contact._guid || gLastWindowId++ :
 | |
|                    conversationWindowData.roomToken || conversationWindowData.callId ||
 | |
|                    gLastWindowId++;
 | |
|     return windowId.toString();
 | |
|   },
 | |
| 
 | |
|   getChatURL: function(chatWindowId) {
 | |
|     return "about:loopconversation#" + chatWindowId;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Determines if a chat window is already open for a given window id.
 | |
|    *
 | |
|    * @param  {String}  chatWindowId The window id.
 | |
|    * @return {Boolean}              True if the window is opened.
 | |
|    */
 | |
|   isChatWindowOpen: function(chatWindowId) {
 | |
|     if (this.mocks.isChatWindowOpen !== undefined) {
 | |
|       return this.mocks.isChatWindowOpen;
 | |
|     }
 | |
| 
 | |
|     let chatUrl = this.getChatURL(chatWindowId);
 | |
| 
 | |
|     return [...Chat.chatboxes].some(chatbox => chatbox.src == chatUrl);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Opens the chat window
 | |
|    *
 | |
|    * @param {Object} conversationWindowData The data to be obtained by the
 | |
|    *                                        window when it opens.
 | |
|    * @returns {Number} The id of the window, null if a window could not
 | |
|    *                   be opened.
 | |
|    */
 | |
|   openChatWindow: function(conversationWindowData) {
 | |
|     // So I guess the origin is the loop server!?
 | |
|     let origin = this.loopServerUri;
 | |
|     let windowId = this.getChatWindowID(conversationWindowData);
 | |
| 
 | |
|     gConversationWindowData.set(windowId, conversationWindowData);
 | |
| 
 | |
|     let url = this.getChatURL(windowId);
 | |
| 
 | |
|     Chat.registerButton(kChatboxHangupButton);
 | |
| 
 | |
|     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;
 | |
|       }
 | |
| 
 | |
|       let loaded = event => {
 | |
|         if (event.target != chatbox.contentDocument) {
 | |
|           return;
 | |
|         }
 | |
|         chatbox.removeEventListener("DOMContentLoaded", loaded, true);
 | |
| 
 | |
|         let chatbar = chatbox.parentNode;
 | |
|         let window = chatbox.contentWindow;
 | |
| 
 | |
|         function socialFrameChanged(eventName) {
 | |
|           UITour.availableTargetsCache.clear();
 | |
|           UITour.notify(eventName);
 | |
| 
 | |
|           if (eventName == "Loop:ChatWindowDetached" || eventName == "Loop:ChatWindowAttached") {
 | |
|             // After detach, re-attach of the chatbox, refresh its reference so
 | |
|             // we can keep using it here.
 | |
|             let ref = chatbar.chatboxForURL.get(chatbox.src);
 | |
|             chatbox = ref && ref.get() || chatbox;
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         window.addEventListener("socialFrameHide", socialFrameChanged.bind(null, "Loop:ChatWindowHidden"));
 | |
|         window.addEventListener("socialFrameShow", socialFrameChanged.bind(null, "Loop:ChatWindowShown"));
 | |
|         window.addEventListener("socialFrameDetached", socialFrameChanged.bind(null, "Loop:ChatWindowDetached"));
 | |
|         window.addEventListener("socialFrameAttached", socialFrameChanged.bind(null, "Loop:ChatWindowAttached"));
 | |
|         window.addEventListener("unload", socialFrameChanged.bind(null, "Loop:ChatWindowClosed"));
 | |
| 
 | |
|         const kSizeMap = {
 | |
|           LoopChatEnabled: "loopChatEnabled",
 | |
|           LoopChatMessageAppended: "loopChatMessageAppended"
 | |
|         };
 | |
| 
 | |
|         function onChatEvent(ev) {
 | |
|           // When the chat box or messages are shown, resize the panel or window
 | |
|           // to be slightly higher to accomodate them.
 | |
|           let customSize = kSizeMap[ev.type];
 | |
|           let currSize = chatbox.getAttribute("customSize");
 | |
|           // If the size is already at the requested one or at the maximum size
 | |
|           // already, don't do anything. Especially don't make it shrink.
 | |
|           if (customSize && currSize != customSize && currSize != "loopChatMessageAppended") {
 | |
|             chatbox.setAttribute("customSize", customSize);
 | |
|             chatbox.parentNode.setAttribute("customSize", customSize);
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         window.addEventListener("LoopChatEnabled", onChatEvent);
 | |
|         window.addEventListener("LoopChatMessageAppended", onChatEvent);
 | |
| 
 | |
|         injectLoopAPI(window);
 | |
| 
 | |
|         let ourID = window.QueryInterface(Ci.nsIInterfaceRequestor)
 | |
|             .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
 | |
| 
 | |
|         let onPCLifecycleChange = (pc, winID, type) => {
 | |
|           if (winID != ourID) {
 | |
|             return;
 | |
|           }
 | |
| 
 | |
|           // Chat Window Id, this is different that the internal winId
 | |
|           let chatWindowId = window.location.hash.slice(1);
 | |
|           var context = this.conversationContexts.get(chatWindowId);
 | |
|           var exists = pc.id.match(/session=(\S+)/);
 | |
|           if (context && !exists) {
 | |
|             // Not ideal but insert our data amidst existing data like this:
 | |
|             // - 000 (id=00 url=http)
 | |
|             // + 000 (session=000 call=000 id=00 url=http)
 | |
|             var pair = pc.id.split("(");
 | |
|             if (pair.length == 2) {
 | |
|               pc.id = pair[0] + "(session=" + context.sessionId +
 | |
|                   (context.callId ? " call=" + context.callId : "") + " " + pair[1];
 | |
|             }
 | |
|           }
 | |
| 
 | |
|           if (type == "iceconnectionstatechange") {
 | |
|             switch(pc.iceConnectionState) {
 | |
|               case "failed":
 | |
|               case "disconnected":
 | |
|                 if (Services.telemetry.canRecordExtended) {
 | |
|                   this.stageForTelemetryUpload(window, pc);
 | |
|                 }
 | |
|                 break;
 | |
|             }
 | |
|           }
 | |
|         };
 | |
| 
 | |
|         let pc_static = new window.RTCPeerConnectionStatic();
 | |
|         pc_static.registerPeerConnectionLifecycleCallback(onPCLifecycleChange);
 | |
| 
 | |
|         UITour.notify("Loop:ChatWindowOpened");
 | |
|       };
 | |
|       chatbox.addEventListener("DOMContentLoaded", loaded, true);
 | |
|     };
 | |
| 
 | |
|     let chatboxInstance = Chat.open(null, origin, "", url, undefined, undefined,
 | |
|                                     callback);
 | |
|     if (!chatboxInstance) {
 | |
|       return null;
 | |
|     // It's common for unit tests to overload Chat.open.
 | |
|     } else if (chatboxInstance.setAttribute) {
 | |
|       // Set properties that influence visual appearance of the chatbox right
 | |
|       // away to circumvent glitches.
 | |
|       chatboxInstance.setAttribute("customSize", "loopDefault");
 | |
|       chatboxInstance.parentNode.setAttribute("customSize", "loopDefault");
 | |
|       Chat.loadButtonSet(chatboxInstance, "minimize,swap," + kChatboxHangupButton.id);
 | |
|     }
 | |
|     return windowId;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Fetch Firefox Accounts (FxA) OAuth parameters from the Loop Server.
 | |
|    *
 | |
|    * @return {Promise} resolved with the body of the hawk request for OAuth parameters.
 | |
|    */
 | |
|   promiseFxAOAuthParameters: function() {
 | |
|     const SESSION_TYPE = LOOP_SESSION_TYPE.FXA;
 | |
|     return this.hawkRequestInternal(SESSION_TYPE, "/fxa-oauth/params", "POST").then(response => {
 | |
|       if (!this.storeSessionToken(SESSION_TYPE, response.headers)) {
 | |
|         throw new Error("Invalid FxA hawk token returned");
 | |
|       }
 | |
|       let prefType = Services.prefs.getPrefType(this.getSessionTokenPrefName(SESSION_TYPE));
 | |
|       if (prefType == Services.prefs.PREF_INVALID) {
 | |
|         throw new Error("No FxA hawk token returned and we don't have one saved");
 | |
|       }
 | |
| 
 | |
|       return JSON.parse(response.body);
 | |
|     },
 | |
|     error => { this._hawkRequestError(error); });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get the OAuth client constructed with Loop OAauth parameters.
 | |
|    *
 | |
|    * @param {Boolean} forceReAuth Set to true to force the user to reauthenticate.
 | |
|    * @return {Promise}
 | |
|    */
 | |
|   promiseFxAOAuthClient: Task.async(function* (forceReAuth) {
 | |
|     // We must make sure to have only a single client otherwise they will have different states and
 | |
|     // multiple channels. This would happen if the user clicks the Login button more than once.
 | |
|     if (gFxAOAuthClientPromise) {
 | |
|       return gFxAOAuthClientPromise;
 | |
|     }
 | |
| 
 | |
|     gFxAOAuthClientPromise = this.promiseFxAOAuthParameters().then(
 | |
|       parameters => {
 | |
|         // Add the fact that we want keys to the parameters.
 | |
|         parameters.keys = true;
 | |
|         if (forceReAuth) {
 | |
|           parameters.action = "force_auth";
 | |
|           parameters.email = MozLoopService.userProfile.email;
 | |
|         }
 | |
| 
 | |
|         try {
 | |
|           gFxAOAuthClient = new FxAccountsOAuthClient({
 | |
|             parameters: parameters
 | |
|           });
 | |
|         } catch (ex) {
 | |
|           gFxAOAuthClientPromise = null;
 | |
|           throw ex;
 | |
|         }
 | |
|         return gFxAOAuthClient;
 | |
|       },
 | |
|       error => {
 | |
|         gFxAOAuthClientPromise = null;
 | |
|         throw error;
 | |
|       }
 | |
|     );
 | |
| 
 | |
|     return gFxAOAuthClientPromise;
 | |
|   }),
 | |
| 
 | |
|   /**
 | |
|    * Get the OAuth client and do the authorization web flow to get an OAuth code.
 | |
|    *
 | |
|    * @param {Boolean} forceReAuth Set to true to force the user to reauthenticate.
 | |
|    * @return {Promise}
 | |
|    */
 | |
|   promiseFxAOAuthAuthorization: function(forceReAuth) {
 | |
|     let deferred = Promise.defer();
 | |
|     this.promiseFxAOAuthClient(forceReAuth).then(
 | |
|       client => {
 | |
|         client.onComplete = this._fxAOAuthComplete.bind(this, deferred);
 | |
|         client.onError = this._fxAOAuthError.bind(this, deferred);
 | |
|         client.launchWebFlow();
 | |
|       },
 | |
|       error => {
 | |
|         log.error(error);
 | |
|         deferred.reject(error);
 | |
|       }
 | |
|     );
 | |
|     return deferred.promise;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get the OAuth token using the OAuth code and state.
 | |
|    *
 | |
|    * The caller should approperiately handle 4xx errors (which should lead to a logout)
 | |
|    * and 5xx or connectivity issues with messaging to try again later.
 | |
|    *
 | |
|    * @param {String} code
 | |
|    * @param {String} state
 | |
|    *
 | |
|    * @return {Promise} resolving with OAuth token data.
 | |
|    */
 | |
|   promiseFxAOAuthToken: function(code, state) {
 | |
|     if (!code || !state) {
 | |
|       throw new Error("promiseFxAOAuthToken: code and state are required.");
 | |
|     }
 | |
| 
 | |
|     let payload = {
 | |
|       code: code,
 | |
|       state: state
 | |
|     };
 | |
|     return this.hawkRequestInternal(LOOP_SESSION_TYPE.FXA, "/fxa-oauth/token", "POST", payload).then(response => {
 | |
|       return JSON.parse(response.body);
 | |
|     },
 | |
|     error => { this._hawkRequestError(error); });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Called once gFxAOAuthClient fires onComplete.
 | |
|    *
 | |
|    * @param {Deferred} deferred used to resolve the gFxAOAuthClientPromise
 | |
|    * @param {Object} result (with code and state)
 | |
|    */
 | |
|   _fxAOAuthComplete: function(deferred, result, keys) {
 | |
|     if (keys.kBr) {
 | |
|       Services.prefs.setCharPref("loop.key.fxa", keys.kBr.k);
 | |
|     }
 | |
|     gFxAOAuthClientPromise = null;
 | |
|     // Note: The state was already verified in FxAccountsOAuthClient.
 | |
|     deferred.resolve(result);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Called if gFxAOAuthClient fires onError.
 | |
|    *
 | |
|    * @param {Deferred} deferred used to reject the gFxAOAuthClientPromise
 | |
|    * @param {Object} error object returned by FxAOAuthClient
 | |
|    */
 | |
|   _fxAOAuthError: function(deferred, err) {
 | |
|     gFxAOAuthClientPromise = null;
 | |
|     deferred.reject(err);
 | |
|   }
 | |
| };
 | |
| Object.freeze(MozLoopServiceInternal);
 | |
| 
 | |
| 
 | |
| var gInitializeTimerFunc = (deferredInitialization) => {
 | |
|   // 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.
 | |
| 
 | |
|   setTimeout(MozLoopService.delayedInitialize.bind(MozLoopService, deferredInitialization),
 | |
|              MozLoopServiceInternal.initialRegistrationDelayMilliseconds);
 | |
| };
 | |
| 
 | |
| var gServiceInitialized = false;
 | |
| 
 | |
| /**
 | |
|  * Public API
 | |
|  */
 | |
| this.MozLoopService = {
 | |
|   _DNSService: gDNSService,
 | |
|   _activeScreenShares: [],
 | |
| 
 | |
|   get channelIDs() {
 | |
|     // Channel ids that will be registered with the PushServer for notifications
 | |
|     return {
 | |
|       callsFxA: "25389583-921f-4169-a426-a4673658944b",
 | |
|       roomsFxA: "6add272a-d316-477c-8335-f00f73dfde71",
 | |
|       roomsGuest: "19d3f799-a8f3-4328-9822-b7cd02765832"
 | |
|     };
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Used to override the initalize timer function for test purposes.
 | |
|    */
 | |
|   set initializeTimerFunc(value) {
 | |
|     gInitializeTimerFunc = value;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Used to reset if the service has been initialized or not - for test
 | |
|    * purposes.
 | |
|    */
 | |
|   resetServiceInitialized: function() {
 | |
|     gServiceInitialized = false;
 | |
|   },
 | |
| 
 | |
|   get roomsParticipantsCount() {
 | |
|     return LoopRooms.participantsCount;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Initialized the loop service, and starts registration with the
 | |
|    * push and loop servers.
 | |
|    *
 | |
|    * Note: this returns a promise for unit test purposes.
 | |
|    *
 | |
|    * @return {Promise}
 | |
|    */
 | |
|   initialize: Task.async(function*() {
 | |
|     // Ensure we don't setup things like listeners more than once.
 | |
|     if (gServiceInitialized) {
 | |
|       return Promise.resolve();
 | |
|     }
 | |
| 
 | |
|     gServiceInitialized = true;
 | |
| 
 | |
|     // Do this here, rather than immediately after definition, so that we can
 | |
|     // stub out API functions for unit testing
 | |
|     Object.freeze(this);
 | |
| 
 | |
|     // Initialise anything that needs it in rooms.
 | |
|     LoopRooms.init();
 | |
| 
 | |
|     // Don't do anything if loop is not enabled.
 | |
|     if (!Services.prefs.getBoolPref("loop.enabled")) {
 | |
|       return Promise.reject(new Error("loop is not enabled"));
 | |
|     }
 | |
| 
 | |
|     if (Services.prefs.getPrefType("loop.fxa.enabled") == Services.prefs.PREF_BOOL) {
 | |
|       gFxAEnabled = Services.prefs.getBoolPref("loop.fxa.enabled");
 | |
|       if (!gFxAEnabled) {
 | |
|         yield this.logOutFromFxA();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // The Loop toolbar button should change icon when the room participant count
 | |
|     // changes from 0 to something.
 | |
|     const onRoomsChange = (e) => {
 | |
|       // Pass the event name as notification reason for better logging.
 | |
|       MozLoopServiceInternal.notifyStatusChanged("room-" + e);
 | |
|     };
 | |
|     LoopRooms.on("add", onRoomsChange);
 | |
|     LoopRooms.on("update", onRoomsChange);
 | |
|     LoopRooms.on("delete", onRoomsChange);
 | |
|     LoopRooms.on("joined", (e, room, participant) => {
 | |
|       // Don't alert if we're in the doNotDisturb mode, or the participant
 | |
|       // is the owner - the content code deals with the rest of the sounds.
 | |
|       if (MozLoopServiceInternal.doNotDisturb || participant.owner) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       let window = gWM.getMostRecentWindow("navigator:browser");
 | |
|       if (window) {
 | |
|         window.LoopUI.showNotification({
 | |
|           sound: "room-joined",
 | |
|           // Fallback to the brand short name if the roomName isn't available.
 | |
|           title: room.roomName || MozLoopServiceInternal.localizedStrings.get("clientShortname2"),
 | |
|           message: MozLoopServiceInternal.localizedStrings.get("rooms_room_joined_label"),
 | |
|           selectTab: "rooms"
 | |
|         });
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     LoopRooms.on("joined", this.maybeResumeTourOnRoomJoined.bind(this));
 | |
| 
 | |
|     // If there's no guest room created and the user hasn't
 | |
|     // previously authenticated then skip registration.
 | |
|     if (!LoopRooms.getGuestCreatedRoom() &&
 | |
|         !MozLoopServiceInternal.fxAOAuthTokenData) {
 | |
|       return Promise.resolve("registration not needed");
 | |
|     }
 | |
| 
 | |
|     let deferredInitialization = Promise.defer();
 | |
|     gInitializeTimerFunc(deferredInitialization);
 | |
| 
 | |
|     return deferredInitialization.promise;
 | |
|   }),
 | |
| 
 | |
|   /**
 | |
|    * Maybe resume the tour (re-opening the tab, if necessary) if someone else joins
 | |
|    * a room of ours and it's currently open.
 | |
|    */
 | |
|   maybeResumeTourOnRoomJoined: function(e, room, participant) {
 | |
|     let isOwnerInRoom = false;
 | |
|     let isOtherInRoom = false;
 | |
| 
 | |
|     if (!this.getLoopPref("gettingStarted.resumeOnFirstJoin")) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (!room.participants) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // The participant that joined isn't necessarily included in room.participants (depending on
 | |
|     // when the broadcast happens) so concatenate.
 | |
|     for (let roomParticipant of room.participants.concat(participant)) {
 | |
|       if (roomParticipant.owner) {
 | |
|         isOwnerInRoom = true;
 | |
|       } else {
 | |
|         isOtherInRoom = true;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (!isOwnerInRoom || !isOtherInRoom) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Check that the room chatbox is still actually open using its URL
 | |
|     let chatboxesForRoom = [...Chat.chatboxes].filter(chatbox => {
 | |
|       return chatbox.src == MozLoopServiceInternal.getChatURL(room.roomToken);
 | |
|     });
 | |
| 
 | |
|     if (!chatboxesForRoom.length) {
 | |
|       log.warn("Tried to resume the tour from a join when the chatbox was closed", room);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this.resumeTour("open");
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * The core of the initialization work that happens once the browser is ready
 | |
|    * (after a timer when called during startup).
 | |
|    *
 | |
|    * Can be called more than once (e.g. if the initial setup fails at some phase).
 | |
|    * @param {Deferred} deferredInitialization
 | |
|    */
 | |
|   delayedInitialize: Task.async(function*(deferredInitialization) {
 | |
|     log.debug("delayedInitialize");
 | |
|     // Set or clear an error depending on how deferredInitialization gets resolved.
 | |
|     // We do this first so that it can handle the early returns below.
 | |
|     let completedPromise = deferredInitialization.promise.then(result => {
 | |
|       MozLoopServiceInternal.clearError("initialization");
 | |
|       return result;
 | |
|     },
 | |
|     error => {
 | |
|       // If we get a non-object then setError was already called for a different error type.
 | |
|       if (typeof error == "object") {
 | |
|         MozLoopServiceInternal.setError("initialization", error, () => MozLoopService.delayedInitialize(Promise.defer()));
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     try {
 | |
|       if (LoopRooms.getGuestCreatedRoom()) {
 | |
|         yield this.promiseRegisteredWithServers(LOOP_SESSION_TYPE.GUEST);
 | |
|       } else {
 | |
|         log.debug("delayedInitialize: Guest Room hasn't been created so not registering as a guest");
 | |
|       }
 | |
|     } catch (ex) {
 | |
|       log.debug("MozLoopService: Failure of guest registration", ex);
 | |
|       deferredInitialization.reject(ex);
 | |
|       yield completedPromise;
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (!MozLoopServiceInternal.fxAOAuthTokenData) {
 | |
|       log.debug("delayedInitialize: Initialized without an already logged-in account");
 | |
|       deferredInitialization.resolve("initialized without FxA status");
 | |
|       yield completedPromise;
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     log.debug("MozLoopService: Initializing with already logged-in account");
 | |
|     MozLoopServiceInternal.promiseRegisteredWithServers(LOOP_SESSION_TYPE.FXA).then(() => {
 | |
|       deferredInitialization.resolve("initialized to logged-in status");
 | |
|     }, error => {
 | |
|       log.debug("MozLoopService: error logging in using cached auth token");
 | |
|       let retryFunc = () => MozLoopServiceInternal.promiseRegisteredWithServers(LOOP_SESSION_TYPE.FXA);
 | |
|       MozLoopServiceInternal.setError("login", error, retryFunc);
 | |
|       deferredInitialization.reject("error logging in using cached auth token");
 | |
|     });
 | |
|     yield completedPromise;
 | |
|   }),
 | |
| 
 | |
|   /**
 | |
|    * Opens the chat window
 | |
|    *
 | |
|    * @param {Object} conversationWindowData The data to be obtained by the
 | |
|    *                                        window when it opens.
 | |
|    * @returns {Number} The id of the window.
 | |
|    */
 | |
|   openChatWindow: function(conversationWindowData) {
 | |
|     return MozLoopServiceInternal.openChatWindow(conversationWindowData);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Determines if a chat window is already open for a given window id.
 | |
|    *
 | |
|    * @param  {String}  chatWindowId The window id.
 | |
|    * @return {Boolean}              True if the window is opened.
 | |
|    */
 | |
|   isChatWindowOpen: function(chatWindowId) {
 | |
|     return MozLoopServiceInternal.isChatWindowOpen(chatWindowId);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * @see MozLoopServiceInternal.promiseRegisteredWithServers
 | |
|    */
 | |
|   promiseRegisteredWithServers: function(sessionType = LOOP_SESSION_TYPE.GUEST) {
 | |
|     return MozLoopServiceInternal.promiseRegisteredWithServers(sessionType);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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 (!stringData.has(key)) {
 | |
|       log.error("No string found for key: ", key);
 | |
|       return "";
 | |
|     }
 | |
| 
 | |
|     return JSON.stringify({ textContent: stringData.get(key) });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Returns a new GUID (UUID) in curly braces format.
 | |
|    */
 | |
|   generateUUID: function() {
 | |
|     return uuidgen.generateUUID().toString();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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;
 | |
|   },
 | |
| 
 | |
|   get fxAEnabled() {
 | |
|     return gFxAEnabled;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Gets the user profile, but only if there is
 | |
|    * tokenData present. Without tokenData, the
 | |
|    * profile is meaningless.
 | |
|    *
 | |
|    * @return {Object}
 | |
|    */
 | |
|   get userProfile() {
 | |
|     return getJSONPref("loop.fxa_oauth.tokendata") &&
 | |
|            getJSONPref("loop.fxa_oauth.profile");
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Gets the encryption key for this profile.
 | |
|    */
 | |
|   promiseProfileEncryptionKey: function() {
 | |
|     return new Promise((resolve, reject) => {
 | |
|       if (this.userProfile) {
 | |
|         // We're an FxA user.
 | |
|         if (Services.prefs.prefHasUserValue("loop.key.fxa")) {
 | |
|           resolve(MozLoopService.getLoopPref("key.fxa"));
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         // This should generally never happen, but its not really possible
 | |
|         // for us to force reauth from here in a sensible way for the user.
 | |
|         // So we'll just have to flag it the best we can.
 | |
|         reject(new Error("No FxA key available"));
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // XXX Temporarily save in preferences until we've got some
 | |
|       // extra storage (bug 1152761).
 | |
|       if (!Services.prefs.prefHasUserValue("loop.key")) {
 | |
|         // Get a new value.
 | |
|         loopCrypto.generateKey().then(key => {
 | |
|           Services.prefs.setCharPref("loop.key", key);
 | |
|           resolve(key);
 | |
|         }).catch(function(error) {
 | |
|           MozLoopService.log.error(error);
 | |
|           reject(error);
 | |
|         });
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       resolve(MozLoopService.getLoopPref("key"));
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Returns true if this profile has an encryption key. For guest profiles
 | |
|    * this is always true, since we can generate a new one if needed. For FxA
 | |
|    * profiles, we need to check the preference.
 | |
|    *
 | |
|    * @return {Boolean} True if the profile has an encryption key.
 | |
|    */
 | |
|   get hasEncryptionKey() {
 | |
|     return !this.userProfile ||
 | |
|       Services.prefs.prefHasUserValue("loop.key.fxa");
 | |
|   },
 | |
| 
 | |
|   get errors() {
 | |
|     return MozLoopServiceInternal.errors;
 | |
|   },
 | |
| 
 | |
|   get log() {
 | |
|     return log;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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 preference under "loop.".
 | |
|    *
 | |
|    * @param {String} prefSuffix The name of the pref without the preceding "loop."
 | |
|    * @param {*} value The value to set.
 | |
|    * @param {Enum} prefType Type of preference, defined at Ci.nsIPrefBranch. Optional.
 | |
|    *
 | |
|    * Any errors thrown by the Mozilla pref API are logged to the console.
 | |
|    */
 | |
|   setLoopPref: function(prefSuffix, value, prefType) {
 | |
|     let prefName = "loop." + prefSuffix;
 | |
|     try {
 | |
|       if (!prefType) {
 | |
|         prefType = Services.prefs.getPrefType(prefName);
 | |
|       }
 | |
|       switch (prefType) {
 | |
|         case Ci.nsIPrefBranch.PREF_STRING:
 | |
|           Services.prefs.setCharPref(prefName, value);
 | |
|           break;
 | |
|         case Ci.nsIPrefBranch.PREF_INT:
 | |
|           Services.prefs.setIntPref(prefName, value);
 | |
|           break;
 | |
|         case Ci.nsIPrefBranch.PREF_BOOL:
 | |
|           Services.prefs.setBoolPref(prefName, value);
 | |
|           break;
 | |
|         default:
 | |
|           log.error("invalid preference type setting " + prefName);
 | |
|           break;
 | |
|       }
 | |
|     } catch (ex) {
 | |
|       log.error("setLoopPref had trouble setting " + prefName +
 | |
|         "; exception: " + ex);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Return any preference under "loop.".
 | |
|    *
 | |
|    * @param {String} prefName The name of the pref without the preceding
 | |
|    * "loop."
 | |
|    * @param {Enum} prefType Type of preference, defined at Ci.nsIPrefBranch. Optional.
 | |
|    *
 | |
|    * 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 {*} on success, null on error
 | |
|    */
 | |
|   getLoopPref: function(prefSuffix, prefType) {
 | |
|     let prefName = "loop." + prefSuffix;
 | |
|     try {
 | |
|       if (!prefType) {
 | |
|         prefType = Services.prefs.getPrefType(prefName);
 | |
|       } else if (prefType != Services.prefs.getPrefType(prefName)) {
 | |
|         log.error("invalid type specified for preference");
 | |
|         return null;
 | |
|       }
 | |
|       switch (prefType) {
 | |
|         case Ci.nsIPrefBranch.PREF_STRING:
 | |
|           return Services.prefs.getCharPref(prefName);
 | |
|         case Ci.nsIPrefBranch.PREF_INT:
 | |
|           return Services.prefs.getIntPref(prefName);
 | |
|         case Ci.nsIPrefBranch.PREF_BOOL:
 | |
|           return Services.prefs.getBoolPref(prefName);
 | |
|         default:
 | |
|           log.error("invalid preference type getting " + prefName);
 | |
|           return null;
 | |
|       }
 | |
|     } catch (ex) {
 | |
|       log.error("getLoopPref had trouble getting " + prefName +
 | |
|         "; exception: " + ex);
 | |
|       return null;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Start the FxA login flow using the OAuth client and params from the Loop server.
 | |
|    *
 | |
|    * The caller should be prepared to handle rejections related to network, server or login errors.
 | |
|    *
 | |
|    * @param {Boolean} forceReAuth Set to true to force the user to reauthenticate.
 | |
|    * @return {Promise} that resolves when the FxA login flow is complete.
 | |
|    */
 | |
|   logInToFxA: function(forceReAuth) {
 | |
|     log.debug("logInToFxA with fxAOAuthTokenData:", !!MozLoopServiceInternal.fxAOAuthTokenData);
 | |
|     if (!forceReAuth && MozLoopServiceInternal.fxAOAuthTokenData) {
 | |
|       return Promise.resolve(MozLoopServiceInternal.fxAOAuthTokenData);
 | |
|     }
 | |
|     return MozLoopServiceInternal.promiseFxAOAuthAuthorization(forceReAuth).then(response => {
 | |
|       return MozLoopServiceInternal.promiseFxAOAuthToken(response.code, response.state);
 | |
|     }).then(tokenData => {
 | |
|       MozLoopServiceInternal.fxAOAuthTokenData = tokenData;
 | |
|       return tokenData;
 | |
|     }).then(tokenData => {
 | |
|       return MozLoopServiceInternal.promiseRegisteredWithServers(LOOP_SESSION_TYPE.FXA).then(() => {
 | |
|         MozLoopServiceInternal.clearError("login");
 | |
|         MozLoopServiceInternal.clearError("profile");
 | |
|         return MozLoopServiceInternal.fxAOAuthTokenData;
 | |
|       });
 | |
|     }).then(Task.async(function* fetchProfile(tokenData) {
 | |
|       yield MozLoopService.fetchFxAProfile(tokenData);
 | |
|       return tokenData;
 | |
|     })).catch(error => {
 | |
|       MozLoopServiceInternal.fxAOAuthTokenData = null;
 | |
|       MozLoopServiceInternal.fxAOAuthProfile = null;
 | |
|       MozLoopServiceInternal.deferredRegistrations.delete(LOOP_SESSION_TYPE.FXA);
 | |
|       throw error;
 | |
|     }).catch((error) => {
 | |
|       MozLoopServiceInternal.setError("login", error,
 | |
|                                       () => MozLoopService.logInToFxA());
 | |
|       // Re-throw for testing
 | |
|       throw error;
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Logs the user out from FxA.
 | |
|    *
 | |
|    * Gracefully handles if the user is already logged out.
 | |
|    *
 | |
|    * @return {Promise} that resolves when the FxA logout flow is complete.
 | |
|    */
 | |
|   logOutFromFxA: Task.async(function*() {
 | |
|     log.debug("logOutFromFxA");
 | |
|     try {
 | |
|       yield MozLoopServiceInternal.unregisterFromLoopServer(LOOP_SESSION_TYPE.FXA);
 | |
|     }
 | |
|     catch (err) {
 | |
|       throw err;
 | |
|     }
 | |
|     finally {
 | |
|       MozLoopServiceInternal.clearSessionToken(LOOP_SESSION_TYPE.FXA);
 | |
|       MozLoopServiceInternal.fxAOAuthTokenData = null;
 | |
|       MozLoopServiceInternal.fxAOAuthProfile = null;
 | |
|       MozLoopServiceInternal.deferredRegistrations.delete(LOOP_SESSION_TYPE.FXA);
 | |
|       // Unregister with PushHandler so these push channels will not get re-registered
 | |
|       // if the connection is re-established by the PushHandler.
 | |
|       MozLoopServiceInternal.pushHandler.unregister(MozLoopService.channelIDs.callsFxA);
 | |
|       MozLoopServiceInternal.pushHandler.unregister(MozLoopService.channelIDs.roomsFxA);
 | |
| 
 | |
|       // Reset the client since the initial promiseFxAOAuthParameters() call is
 | |
|       // what creates a new session.
 | |
|       gFxAOAuthClient = null;
 | |
|       gFxAOAuthClientPromise = null;
 | |
| 
 | |
|       // clearError calls notifyStatusChanged so should be done last when the
 | |
|       // state is clean.
 | |
|       MozLoopServiceInternal.clearError("registration");
 | |
|       MozLoopServiceInternal.clearError("login");
 | |
|       MozLoopServiceInternal.clearError("profile");
 | |
|     }
 | |
|   }),
 | |
| 
 | |
|   /**
 | |
|    * Fetch/update the FxA Profile for the logged in user.
 | |
|    *
 | |
|    * @return {Promise} resolving if the profile information was succesfully retrieved
 | |
|    *                   rejecting if the profile information couldn't be retrieved.
 | |
|    *                   A profile error is registered.
 | |
|    **/
 | |
|   fetchFxAProfile: function() {
 | |
|     log.debug("fetchFxAProfile");
 | |
|     let client = new FxAccountsProfileClient({
 | |
|       serverURL: gFxAOAuthClient.parameters.profile_uri,
 | |
|       token: MozLoopServiceInternal.fxAOAuthTokenData.access_token
 | |
|     });
 | |
|     return client.fetchProfile().then(result => {
 | |
|       MozLoopServiceInternal.fxAOAuthProfile = result;
 | |
|       MozLoopServiceInternal.clearError("profile");
 | |
|     }, error => {
 | |
|       log.error("Failed to retrieve profile", error, this.fetchFxAProfile.bind(this));
 | |
|       MozLoopServiceInternal.setError("profile", error);
 | |
|       MozLoopServiceInternal.fxAOAuthProfile = null;
 | |
|       MozLoopServiceInternal.notifyStatusChanged();
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   openFxASettings: Task.async(function* () {
 | |
|     try {
 | |
|       let fxAOAuthClient = yield MozLoopServiceInternal.promiseFxAOAuthClient();
 | |
|       if (!fxAOAuthClient) {
 | |
|         log.error("Could not get the OAuth client");
 | |
|         return;
 | |
|       }
 | |
|       let url = new URL("/settings", fxAOAuthClient.parameters.content_uri);
 | |
|       let win = Services.wm.getMostRecentWindow("navigator:browser");
 | |
|       win.switchToTabHavingURI(url.toString(), true);
 | |
|     } catch (ex) {
 | |
|       log.error("Error opening FxA settings", ex);
 | |
|     }
 | |
|   }),
 | |
| 
 | |
|   /**
 | |
|    * Gets the tour URL.
 | |
|    *
 | |
|    * @param {String} aSrc A string representing the entry point to begin the tour, optional.
 | |
|    * @param {Object} aAdditionalParams An object with keys used as query parameter names
 | |
|    */
 | |
|   getTourURL: function(aSrc = null, aAdditionalParams = {}) {
 | |
|     let urlStr = this.getLoopPref("gettingStarted.url");
 | |
|     let url = new URL(Services.urlFormatter.formatURL(urlStr));
 | |
|     for (let paramName in aAdditionalParams) {
 | |
|       url.searchParams.set(paramName, aAdditionalParams[paramName]);
 | |
|     }
 | |
|     if (aSrc) {
 | |
|       url.searchParams.set("utm_source", "firefox-browser");
 | |
|       url.searchParams.set("utm_medium", "firefox-browser");
 | |
|       url.searchParams.set("utm_campaign", aSrc);
 | |
|     }
 | |
| 
 | |
|     // Find the most recent pageID that has the Loop prefix.
 | |
|     let mostRecentLoopPageID = {id: null, lastSeen: null};
 | |
|     for (let pageID of UITour.pageIDsForSession) {
 | |
|       if (pageID[0] && pageID[0].startsWith("hello-tour_OpenPanel_") &&
 | |
|           pageID[1] && pageID[1].lastSeen > mostRecentLoopPageID.lastSeen) {
 | |
|         mostRecentLoopPageID.id = pageID[0];
 | |
|         mostRecentLoopPageID.lastSeen = pageID[1].lastSeen;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     const PAGE_ID_EXPIRATION_MS = 60 * 60 * 1000;
 | |
|     if (mostRecentLoopPageID.id &&
 | |
|         mostRecentLoopPageID.lastSeen > Date.now() - PAGE_ID_EXPIRATION_MS) {
 | |
|       url.searchParams.set("utm_content", mostRecentLoopPageID.id);
 | |
|     }
 | |
|     return url;
 | |
|   },
 | |
| 
 | |
|   resumeTour: function(aIncomingConversationState) {
 | |
|     if (!this.getLoopPref("gettingStarted.resumeOnFirstJoin")) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let url = this.getTourURL("resume-with-conversation", {
 | |
|       incomingConversation: aIncomingConversationState
 | |
|     });
 | |
| 
 | |
|     let win = Services.wm.getMostRecentWindow("navigator:browser");
 | |
| 
 | |
|     this.setLoopPref("gettingStarted.resumeOnFirstJoin", false);
 | |
| 
 | |
|     // The query parameters of the url can vary but we always want to re-use a Loop tour tab that's
 | |
|     // already open so we ignore the fragment and query string.
 | |
|     let hadExistingTab = win.switchToTabHavingURI(url, true, {
 | |
|       ignoreFragment: true,
 | |
|       ignoreQueryString: true
 | |
|     });
 | |
| 
 | |
|     // If the tab was already open, send an event instead of using the query
 | |
|     // parameter above (that we don't replace on existing tabs to avoid a reload).
 | |
|     if (hadExistingTab) {
 | |
|       UITour.notify("Loop:IncomingConversation", {
 | |
|         conversationOpen: aIncomingConversationState === "open"
 | |
|       });
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Opens the Getting Started tour in the browser.
 | |
|    *
 | |
|    * @param {String} [aSrc] A string representing the entry point to begin the tour, optional.
 | |
|    */
 | |
|   openGettingStartedTour: Task.async(function(aSrc = null) {
 | |
|     try {
 | |
|       let url = this.getTourURL(aSrc);
 | |
|       let win = Services.wm.getMostRecentWindow("navigator:browser");
 | |
|       win.switchToTabHavingURI(url, true, {
 | |
|         ignoreFragment: true,
 | |
|         replaceQueryString: true
 | |
|       });
 | |
|     } catch (ex) {
 | |
|       log.error("Error opening Getting Started tour", ex);
 | |
|     }
 | |
|   }),
 | |
| 
 | |
|   /**
 | |
|    * Opens a URL in a new tab in the browser.
 | |
|    *
 | |
|    * @param {String} url The new url to open
 | |
|    */
 | |
|   openURL: function(url) {
 | |
|     let win = Services.wm.getMostRecentWindow("navigator:browser");
 | |
|     win.openUILinkIn(url, "tab");
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Performs a hawk based request to the loop server.
 | |
|    *
 | |
|    * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request.
 | |
|    *                                        One of the LOOP_SESSION_TYPE members.
 | |
|    * @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.
 | |
|    * @returns {Promise}
 | |
|    *        Returns a promise that resolves to the response of the API call,
 | |
|    *        or is rejected with an error.  If the server response can be parsed
 | |
|    *        as JSON and contains an 'error' property, the promise will be
 | |
|    *        rejected with this JSON-parsed response.
 | |
|    */
 | |
|   hawkRequest: function(sessionType, path, method, payloadObj) {
 | |
|     return MozLoopServiceInternal.hawkRequest(sessionType, path, method, payloadObj).catch(
 | |
|       error => { MozLoopServiceInternal._hawkRequestError(error); });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Returns the window data for a specific conversation window id.
 | |
|    *
 | |
|    * This data will be relevant to the type of window, e.g. rooms or calls.
 | |
|    * See LoopRooms or LoopCalls for more information.
 | |
|    *
 | |
|    * @param {String} conversationWindowId
 | |
|    * @returns {Object} The window data or null if error.
 | |
|    */
 | |
|   getConversationWindowData: function(conversationWindowId) {
 | |
|     if (gConversationWindowData.has(conversationWindowId)) {
 | |
|       var conversationData = gConversationWindowData.get(conversationWindowId);
 | |
|       gConversationWindowData.delete(conversationWindowId);
 | |
|       return conversationData;
 | |
|     }
 | |
| 
 | |
|     log.error("Window data was already fetched before. Possible race condition!");
 | |
|     return null;
 | |
|   },
 | |
| 
 | |
|   getConversationContext: function(winId) {
 | |
|     return MozLoopServiceInternal.conversationContexts.get(winId);
 | |
|   },
 | |
| 
 | |
|   addConversationContext: function(windowId, context) {
 | |
|     MozLoopServiceInternal.conversationContexts.set(windowId, context);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Used to record the screen sharing state for a window so that it can
 | |
|    * be reflected on the toolbar button.
 | |
|    *
 | |
|    * @param {String} windowId The id of the conversation window the state
 | |
|    *                          is being changed for.
 | |
|    * @param {Boolean} active  Whether or not screen sharing is now active.
 | |
|    */
 | |
|   setScreenShareState: function(windowId, active) {
 | |
|     if (active) {
 | |
|       this._activeScreenShares.push(windowId);
 | |
|     } else {
 | |
|       var index = this._activeScreenShares.indexOf(windowId);
 | |
|       if (index != -1) {
 | |
|         this._activeScreenShares.splice(index, 1);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     MozLoopServiceInternal.notifyStatusChanged();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Returns true if screen sharing is active in at least one window.
 | |
|    */
 | |
|   get screenShareActive() {
 | |
|     return this._activeScreenShares.length > 0;
 | |
|   }
 | |
| };
 |