forked from mirrors/gecko-dev
		
	 a22c2d1f9e
			
		
	
	
		a22c2d1f9e
		
	
	
	
	
		
			
			The GMP manager uses a copy of the update service's url formatting code and has since fallen out of sync. We'll also want to use the same formatting code for the system add-on update checks so this just exposes it in a shared API. I've moved the contents of UpdateChannel.jsm to UpdateUtils.jsm and exposed formatUpdateURL there as well as a few properties that the update service still needs access to. UpdateUtils.UpdateChannel is intended to be a lazy getter but isn't for now since tests expect to be able to change the update channel at runtime. --HG-- extra : commitid : KsbH21csjH4 extra : rebase_source : bc7c08de1ec6e802261b8cd294d88ee2c4e75c2d
		
			
				
	
	
		
			1193 lines
		
	
	
	
		
			39 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1193 lines
		
	
	
	
		
			39 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://services-common/utils.js");
 | |
| Cu.import("resource://gre/modules/Services.jsm");
 | |
| Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 | |
| Cu.import("resource:///modules/loop/LoopCalls.jsm");
 | |
| Cu.import("resource:///modules/loop/MozLoopService.jsm");
 | |
| Cu.import("resource:///modules/loop/LoopRooms.jsm");
 | |
| Cu.import("resource:///modules/loop/LoopContacts.jsm");
 | |
| Cu.importGlobalProperties(["Blob"]);
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "LoopContacts",
 | |
|                                         "resource:///modules/loop/LoopContacts.jsm");
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
 | |
|                                         "resource:///modules/loop/LoopStorage.jsm");
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "hookWindowCloseForPanelClose",
 | |
|                                         "resource://gre/modules/MozSocialAPI.jsm");
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "PageMetadata",
 | |
|                                         "resource://gre/modules/PageMetadata.jsm");
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
 | |
|                                         "resource://gre/modules/PluralForm.jsm");
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
 | |
|                                         "resource://gre/modules/UpdateUtils.jsm");
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "UITour",
 | |
|                                         "resource:///modules/UITour.jsm");
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "Social",
 | |
|                                         "resource:///modules/Social.jsm");
 | |
| XPCOMUtils.defineLazyGetter(this, "appInfo", function() {
 | |
|   return Cc["@mozilla.org/xre/app-info;1"]
 | |
|            .getService(Ci.nsIXULAppInfo)
 | |
|            .QueryInterface(Ci.nsIXULRuntime);
 | |
| });
 | |
| XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
 | |
|                                          "@mozilla.org/widget/clipboardhelper;1",
 | |
|                                          "nsIClipboardHelper");
 | |
| XPCOMUtils.defineLazyServiceGetter(this, "extProtocolSvc",
 | |
|                                          "@mozilla.org/uriloader/external-protocol-service;1",
 | |
|                                          "nsIExternalProtocolService");
 | |
| this.EXPORTED_SYMBOLS = ["injectLoopAPI"];
 | |
| 
 | |
| /**
 | |
|  * Trying to clone an Error object into a different container will yield an error.
 | |
|  * We can work around this by copying the properties we care about onto a regular
 | |
|  * object.
 | |
|  *
 | |
|  * @param {Error|nsIException} error        Error object to copy
 | |
|  * @param {nsIDOMWindow}       targetWindow The content window to clone into
 | |
|  */
 | |
| const cloneErrorObject = function(error, targetWindow) {
 | |
|   let obj = new targetWindow.Error();
 | |
|   let props = Object.getOwnPropertyNames(error);
 | |
|   // nsIException properties are not enumerable, so we'll try to copy the most
 | |
|   // common and useful ones.
 | |
|   if (!props.length) {
 | |
|     props.push("message", "filename", "lineNumber", "columnNumber", "stack");
 | |
|   }
 | |
|   for (let prop of props) {
 | |
|     let value = error[prop];
 | |
|     // for nsIException objects, the property may not be defined.
 | |
|     if (typeof value == "undefined") {
 | |
|       continue;
 | |
|     }
 | |
|     if (typeof value != "string" && typeof value != "number") {
 | |
|       value = String(value);
 | |
|     }
 | |
| 
 | |
|     Object.defineProperty(Cu.waiveXrays(obj), prop, {
 | |
|       configurable: false,
 | |
|       enumerable: true,
 | |
|       value: value,
 | |
|       writable: false
 | |
|     });
 | |
|   }
 | |
|   return obj;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Makes an object or value available to an unprivileged target window.
 | |
|  *
 | |
|  * Primitives are returned as they are, while objects are cloned into the
 | |
|  * specified target.  Error objects are also handled correctly.
 | |
|  *
 | |
|  * @param {any}          value        Value or object to copy
 | |
|  * @param {nsIDOMWindow} targetWindow The content window to copy to
 | |
|  */
 | |
| const cloneValueInto = function(value, targetWindow) {
 | |
|   if (!value || typeof value != "object") {
 | |
|     return value;
 | |
|   }
 | |
| 
 | |
|   // HAWK request errors contain an nsIException object inside `value`.
 | |
|   if (("error" in value) && (value.error instanceof Ci.nsIException)) {
 | |
|     value = value.error;
 | |
|   }
 | |
| 
 | |
|   // Strip Function properties, since they can not be cloned across boundaries
 | |
|   // like this.
 | |
|   for (let prop of Object.getOwnPropertyNames(value)) {
 | |
|     if (typeof value[prop] == "function") {
 | |
|       delete value[prop];
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Inspect for an error this way, because the Error object is special.
 | |
|   if (value.constructor.name == "Error" || value instanceof Ci.nsIException) {
 | |
|     return cloneErrorObject(value, targetWindow);
 | |
|   }
 | |
| 
 | |
|   let clone;
 | |
|   try {
 | |
|     clone = Cu.cloneInto(value, targetWindow);
 | |
|   } catch (ex) {
 | |
|     MozLoopService.log.debug("Failed to clone value:", value);
 | |
|     throw ex;
 | |
|   }
 | |
| 
 | |
|   return clone;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Guarded callback invocation that reports when a callback function doesn't
 | |
|  * exist anymore.
 | |
|  *
 | |
|  * @param {Function} callback Callback function to be invoked.
 | |
|  * @param {...mixed} args     Rest param of callback function arguments.
 | |
|  */
 | |
| const invokeCallback = function(callback, ...args) {
 | |
|   if (typeof callback != "function") {
 | |
|     // We log an error, because it will have a stack trace attached which will
 | |
|     // be helpful whilst debugging.
 | |
|     MozLoopService.log.error.apply(MozLoopService.log,
 | |
|       [new Error("Callback function was lost!"), ...args]);
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   return callback.apply(null, args);
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Get the two-digit hexadecimal code for a byte
 | |
|  *
 | |
|  * @param {byte} charCode
 | |
|  */
 | |
| const toHexString = function(charCode) {
 | |
|   return ("0" + charCode.toString(16)).slice(-2);
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Inject any API containing _only_ function properties into the given window.
 | |
|  *
 | |
|  * @param {Object}       api          Object containing functions that need to
 | |
|  *                                    be exposed to content
 | |
|  * @param {nsIDOMWindow} targetWindow The content window to attach the API
 | |
|  */
 | |
| const injectObjectAPI = function(api, targetWindow) {
 | |
|   let injectedAPI = {};
 | |
|   // Wrap all the methods in `api` to help results passed to callbacks get
 | |
|   // through the priv => unpriv barrier with `Cu.cloneInto()`.
 | |
|   Object.keys(api).forEach(func => {
 | |
|     injectedAPI[func] = function(...params) {
 | |
|       let lastParam = params.pop();
 | |
|       let callbackIsFunction = (typeof lastParam == "function");
 | |
|       // Clone params coming from content to the current scope.
 | |
|       params = [cloneValueInto(p, api) for (p of params)];
 | |
| 
 | |
|       // If the last parameter is a function, assume its a callback
 | |
|       // and wrap it differently.
 | |
|       if (callbackIsFunction) {
 | |
|         api[func](...params, function callback(...results) {
 | |
|           // When the function was garbage collected due to async events, like
 | |
|           // closing a window, we want to circumvent a JS error.
 | |
|           if (callbackIsFunction && typeof lastParam != "function") {
 | |
|             MozLoopService.log.debug(func + ": callback function was lost.");
 | |
|             // Clean up event listeners.
 | |
|             if (func == "on" && api.off) {
 | |
|               api.off(results[0], callback);
 | |
|               return;
 | |
|             }
 | |
|             // Assume the presence of a first result argument to be an error.
 | |
|             if (results[0]) {
 | |
|               MozLoopService.log.error(func + " error:", results[0]);
 | |
|             }
 | |
|             return;
 | |
|           }
 | |
|           lastParam(...[cloneValueInto(r, targetWindow) for (r of results)]);
 | |
|         });
 | |
|       } else {
 | |
|         try {
 | |
|           lastParam = cloneValueInto(lastParam, api);
 | |
|           return cloneValueInto(api[func](...params, lastParam), targetWindow);
 | |
|         } catch (ex) {
 | |
|           MozLoopService.log.error(func + " error: ", ex, params, lastParam);
 | |
|           return cloneValueInto(ex, targetWindow);
 | |
|         }
 | |
|       }
 | |
|     };
 | |
|   });
 | |
| 
 | |
|   let contentObj = Cu.cloneInto(injectedAPI, targetWindow, {cloneFunctions: true});
 | |
|   // Since we deny preventExtensions on XrayWrappers, because Xray semantics make
 | |
|   // it difficult to act like an object has actually been frozen, we try to seal
 | |
|   // the `contentObj` without Xrays.
 | |
|   try {
 | |
|     Object.seal(Cu.waiveXrays(contentObj));
 | |
|   } catch (ex) {}
 | |
|   return contentObj;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Inject the loop API into the given window.  The caller must be sure the
 | |
|  * window is a loop content window (eg, a panel, chatwindow, or similar).
 | |
|  *
 | |
|  * See the documentation on the individual functions for details of the API.
 | |
|  *
 | |
|  * @param {nsIDOMWindow} targetWindow The content window to attach the API.
 | |
|  */
 | |
| function injectLoopAPI(targetWindow) {
 | |
|   let ringer;
 | |
|   let ringerStopper;
 | |
|   let appVersionInfo;
 | |
|   let contactsAPI;
 | |
|   let roomsAPI;
 | |
|   let callsAPI;
 | |
|   let savedWindowListeners = new Map();
 | |
|   let socialProviders;
 | |
|   const kShareWidgetId = "social-share-button";
 | |
|   let socialShareButtonListenersAdded = false;
 | |
| 
 | |
| 
 | |
|   let api = {
 | |
|     /**
 | |
|      * Gets an object with data that represents the currently
 | |
|      * authenticated user's identity.
 | |
|      *
 | |
|      * @return null if user not logged in; profile object otherwise
 | |
|      */
 | |
|     userProfile: {
 | |
|       enumerable: true,
 | |
|       get: function() {
 | |
|         if (!MozLoopService.userProfile)
 | |
|           return null;
 | |
|         let userProfile = Cu.cloneInto({
 | |
|           email: MozLoopService.userProfile.email,
 | |
|           uid: MozLoopService.userProfile.uid
 | |
|         }, targetWindow);
 | |
|         return userProfile;
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Sets and gets the "do not disturb" mode activation flag.
 | |
|      */
 | |
|     doNotDisturb: {
 | |
|       enumerable: true,
 | |
|       get: function() {
 | |
|         return MozLoopService.doNotDisturb;
 | |
|       },
 | |
|       set: function(aFlag) {
 | |
|         MozLoopService.doNotDisturb = aFlag;
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     errors: {
 | |
|       enumerable: true,
 | |
|       get: function() {
 | |
|         let errors = {};
 | |
|         for (let [type, error] of MozLoopService.errors) {
 | |
|           // if error.error is an nsIException, just delete it since it's hard
 | |
|           // to clone across the boundary.
 | |
|           if (error.error instanceof Ci.nsIException) {
 | |
|             MozLoopService.log.debug("Warning: Some errors were omitted from MozLoopAPI.errors " +
 | |
|                                      "due to issues copying nsIException across boundaries.",
 | |
|                                      error.error);
 | |
|             delete error.error;
 | |
|           }
 | |
| 
 | |
|           // We have to clone the error property since it may be an Error object.
 | |
|           if (error.hasOwnProperty("toString")) {
 | |
|             delete error.toString;
 | |
|           }
 | |
|           errors[type] = Cu.waiveXrays(Cu.cloneInto(error, targetWindow, { cloneFunctions: true }));
 | |
|         }
 | |
|         return Cu.cloneInto(errors, targetWindow, { cloneFunctions: true });
 | |
|       },
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Returns the current locale of the browser.
 | |
|      *
 | |
|      * @returns {String} The locale string
 | |
|      */
 | |
|     locale: {
 | |
|       enumerable: true,
 | |
|       get: function() {
 | |
|         return MozLoopService.locale;
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Adds a listener to the most recent window for browser/tab sharing. The
 | |
|      * listener will be notified straight away of the current tab id, then every
 | |
|      * time there is a change of tab.
 | |
|      *
 | |
|      * Listener parameters:
 | |
|      * - {Object}  err      If there is a error this will be defined, null otherwise.
 | |
|      * - {Number} windowId The new windowId after a change of tab.
 | |
|      *
 | |
|      * @param {Function} listener The listener to handle the windowId changes.
 | |
|      */
 | |
|     addBrowserSharingListener: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function(listener) {
 | |
|         let win = Services.wm.getMostRecentWindow("navigator:browser");
 | |
|         let browser = win && win.gBrowser.selectedBrowser;
 | |
|         if (!win || !browser) {
 | |
|           // This may happen when an undocked conversation window is the only
 | |
|           // window left.
 | |
|           let err = new Error("No tabs available to share.");
 | |
|           MozLoopService.log.error(err);
 | |
|           listener(cloneValueInto(err, targetWindow));
 | |
|           return;
 | |
|         }
 | |
|         if (browser.getAttribute("remote") == "true") {
 | |
|           // Tab sharing is not supported yet for e10s-enabled browsers. This will
 | |
|           // be fixed in bug 1137634.
 | |
|           let err = new Error("Tab sharing is not supported for e10s-enabled browsers");
 | |
|           MozLoopService.log.error(err);
 | |
|           listener(cloneValueInto(err, targetWindow));
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         win.LoopUI.addBrowserSharingListener(listener);
 | |
| 
 | |
|         savedWindowListeners.set(listener, Cu.getWeakReference(win));
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Removes a listener that was previously added.
 | |
|      *
 | |
|      * @param {Function} listener The listener to handle the windowId changes.
 | |
|      */
 | |
|     removeBrowserSharingListener: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function(listener) {
 | |
|         if (!savedWindowListeners.has(listener)) {
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         let win = savedWindowListeners.get(listener).get();
 | |
| 
 | |
|         // Remove the element, regardless of if the window exists or not so
 | |
|         // that we clean the map.
 | |
|         savedWindowListeners.delete(listener);
 | |
| 
 | |
|         if (!win) {
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         win.LoopUI.removeBrowserSharingListener(listener);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * 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: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function(conversationWindowId) {
 | |
|         return cloneValueInto(MozLoopService.getConversationWindowData(conversationWindowId),
 | |
|           targetWindow);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Returns the contacts API.
 | |
|      *
 | |
|      * @returns {Object} The contacts API object
 | |
|      */
 | |
|     contacts: {
 | |
|       enumerable: true,
 | |
|       get: function() {
 | |
|         if (contactsAPI) {
 | |
|           return contactsAPI;
 | |
|         }
 | |
| 
 | |
|         // Make a database switch when a userProfile is active already.
 | |
|         let profile = MozLoopService.userProfile;
 | |
|         if (profile) {
 | |
|           LoopStorage.switchDatabase(profile.uid);
 | |
|         }
 | |
|         return contactsAPI = injectObjectAPI(LoopContacts, targetWindow);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Returns the rooms API.
 | |
|      *
 | |
|      * @returns {Object} The rooms API object
 | |
|      */
 | |
|     rooms: {
 | |
|       enumerable: true,
 | |
|       get: function() {
 | |
|         if (roomsAPI) {
 | |
|           return roomsAPI;
 | |
|         }
 | |
|         return roomsAPI = injectObjectAPI(LoopRooms, targetWindow);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Returns the calls API.
 | |
|      *
 | |
|      * @returns {Object} The rooms API object
 | |
|      */
 | |
|     calls: {
 | |
|       enumerable: true,
 | |
|       get: function() {
 | |
|         if (callsAPI) {
 | |
|           return callsAPI;
 | |
|         }
 | |
| 
 | |
|         return callsAPI = injectObjectAPI(LoopCalls, targetWindow);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Import a list of (new) contacts from an external data source.
 | |
|      *
 | |
|      * @param {Object}   options  Property bag of options for the importer
 | |
|      * @param {Function} callback Function that will be invoked once the operation
 | |
|      *                            finished. The first argument passed will be an
 | |
|      *                            `Error` object or `null`. The second argument will
 | |
|      *                            be the result of the operation, if successfull.
 | |
|      */
 | |
|     startImport: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function(options, callback) {
 | |
|         LoopContacts.startImport(options, getChromeWindow(targetWindow), function(...results) {
 | |
|           invokeCallback(callback, ...[cloneValueInto(r, targetWindow) for (r of results)]);
 | |
|         });
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Returns translated strings associated with an element. Designed
 | |
|      * for use with l10n.js
 | |
|      *
 | |
|      * @param {String} key The element id
 | |
|      * @returns {Object} A JSON string containing the localized
 | |
|      *                   attribute/value pairs for the element.
 | |
|      */
 | |
|     getStrings: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function(key) {
 | |
|         return MozLoopService.getStrings(key);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Returns the correct form of a semi-colon separated string
 | |
|      * based on the value of the `num` argument and the current locale.
 | |
|      *
 | |
|      * @param {Integer} num The number used to find the plural form.
 | |
|      * @param {String} str The semi-colon separated string of word forms.
 | |
|      * @returns {String} The correct word form based on the value of the number
 | |
|      *                   and the current locale.
 | |
|      */
 | |
|     getPluralForm: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function(num, str) {
 | |
|         return PluralForm.get(num, str);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Displays a confirmation dialog using the specified strings.
 | |
|      *
 | |
|      * @param {Object}   options  Confirm dialog options
 | |
|      * @param {Function} callback Function that will be invoked once the operation
 | |
|      *                            finished. The first argument passed will be an
 | |
|      *                            `Error` object or `null`. The second argument
 | |
|      *                            will be the result of the operation, TRUE if
 | |
|      *                            the user chose the OK button.
 | |
|      */
 | |
|     confirm: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function(options, callback) {
 | |
|         let buttonFlags;
 | |
|         if (options.okButton && options.cancelButton) {
 | |
|           buttonFlags =
 | |
|             (Ci.nsIPrompt.BUTTON_POS_0 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING) +
 | |
|             (Ci.nsIPrompt.BUTTON_POS_1 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING);
 | |
|         } else if (!options.okButton && !options.cancelButton) {
 | |
|           buttonFlags = Services.prompt.STD_YES_NO_BUTTONS;
 | |
|         } else {
 | |
|           invokeCallback(callback, cloneValueInto(new Error("confirm: missing button options"), targetWindow));
 | |
|         }
 | |
| 
 | |
|         try {
 | |
|           let chosenButton = Services.prompt.confirmEx(null, "",
 | |
|             options.message, buttonFlags, options.okButton, options.cancelButton,
 | |
|             null, null, {});
 | |
| 
 | |
|           invokeCallback(callback, null, chosenButton == 0);
 | |
|         } catch (ex) {
 | |
|           invokeCallback(callback, cloneValueInto(ex, targetWindow));
 | |
|         }
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Set any preference under "loop."
 | |
|      *
 | |
|      * @param {String} prefName 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
 | |
|      * and cause false to be returned.
 | |
|      */
 | |
|     setLoopPref: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function(prefName, value, prefType) {
 | |
|         MozLoopService.setLoopPref(prefName, value, prefType);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * 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: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function(prefName, prefType) {
 | |
|         return MozLoopService.getLoopPref(prefName);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Starts alerting the user about an incoming call
 | |
|      */
 | |
|     startAlerting: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function() {
 | |
|         let chromeWindow = getChromeWindow(targetWindow);
 | |
|         chromeWindow.getAttention();
 | |
|         ringer = new chromeWindow.Audio();
 | |
|         ringer.src = Services.prefs.getCharPref("loop.ringtone");
 | |
|         ringer.loop = true;
 | |
|         ringer.load();
 | |
|         ringer.play();
 | |
|         targetWindow.document.addEventListener("visibilitychange",
 | |
|           ringerStopper = function(event) {
 | |
|             if (event.currentTarget.hidden) {
 | |
|               api.stopAlerting.value();
 | |
|             }
 | |
|           });
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Stops alerting the user about an incoming call
 | |
|      */
 | |
|     stopAlerting: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function() {
 | |
|         if (ringerStopper) {
 | |
|           targetWindow.document.removeEventListener("visibilitychange",
 | |
|                                                     ringerStopper);
 | |
|           ringerStopper = null;
 | |
|         }
 | |
|         if (ringer) {
 | |
|           ringer.pause();
 | |
|           ringer = null;
 | |
|         }
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Performs a hawk based request to the loop server.
 | |
|      *
 | |
|      * Callback parameters:
 | |
|      *  - {Object|null} null if success. Otherwise an object:
 | |
|      *    {
 | |
|      *      code: 401,
 | |
|      *      errno: 401,
 | |
|      *      error: "Request failed",
 | |
|      *      message: "invalid token"
 | |
|      *    }
 | |
|      *  - {String} The body of the response.
 | |
|      *
 | |
|      * @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 {Function} callback Called when the request completes.
 | |
|      */
 | |
|     hawkRequest: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function(sessionType, path, method, payloadObj, callback) {
 | |
|         // XXX Should really return a DOM promise here.
 | |
|         MozLoopService.hawkRequest(sessionType, path, method, payloadObj).then((response) => {
 | |
|           invokeCallback(callback, null, response.body);
 | |
|         }, hawkError => {
 | |
|           // The hawkError.error property, while usually a string representing
 | |
|           // an HTTP response status message, may also incorrectly be a native
 | |
|           // error object that will cause the cloning function to fail.
 | |
|           invokeCallback(callback, Cu.cloneInto({
 | |
|             error: (hawkError.error && typeof hawkError.error == "string")
 | |
|                    ? hawkError.error : "Unexpected exception",
 | |
|             message: hawkError.message,
 | |
|             code: hawkError.code,
 | |
|             errno: hawkError.errno,
 | |
|           }, targetWindow));
 | |
|         }).catch(Cu.reportError);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     LOOP_SESSION_TYPE: {
 | |
|       enumerable: true,
 | |
|       get: function() {
 | |
|         return Cu.cloneInto(LOOP_SESSION_TYPE, targetWindow);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     TWO_WAY_MEDIA_CONN_LENGTH: {
 | |
|       enumerable: true,
 | |
|       get: function() {
 | |
|         return Cu.cloneInto(TWO_WAY_MEDIA_CONN_LENGTH, targetWindow);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     SHARING_STATE_CHANGE: {
 | |
|       enumerable: true,
 | |
|       get: function() {
 | |
|         return Cu.cloneInto(SHARING_STATE_CHANGE, targetWindow);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     SHARING_ROOM_URL: {
 | |
|       enumerable: true,
 | |
|       get: function() {
 | |
|         return Cu.cloneInto(SHARING_ROOM_URL, targetWindow);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     ROOM_CREATE: {
 | |
|       enumerable: true,
 | |
|       get: function() {
 | |
|         return Cu.cloneInto(ROOM_CREATE, targetWindow);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     ROOM_DELETE: {
 | |
|       enumerable: true,
 | |
|       get: function() {
 | |
|         return Cu.cloneInto(ROOM_DELETE, targetWindow);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     ROOM_CONTEXT_ADD: {
 | |
|       enumerable: true,
 | |
|       get: function() {
 | |
|         return Cu.cloneInto(ROOM_CONTEXT_ADD, targetWindow);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     fxAEnabled: {
 | |
|       enumerable: true,
 | |
|       get: function() {
 | |
|         return MozLoopService.fxAEnabled;
 | |
|       },
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Start the FxA login flow using the OAuth client and params from the Loop
 | |
|      * server.
 | |
|      *
 | |
|      * @param {Boolean} forceReAuth Set to true to force FxA into a re-auth even
 | |
|      *                              if the user is already logged in.
 | |
|      * @return {Promise} Returns a promise that is resolved on successful
 | |
|      *                   completion, or rejected otherwise.
 | |
|      */
 | |
|     logInToFxA: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function(forceReAuth) {
 | |
|         return MozLoopService.logInToFxA(forceReAuth);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     logOutFromFxA: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function() {
 | |
|         return MozLoopService.logOutFromFxA();
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     openFxASettings: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function() {
 | |
|         return MozLoopService.openFxASettings();
 | |
|       },
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Returns true if this profile has an encryption key.
 | |
|      *
 | |
|      * @return {Boolean} True if the profile has an encryption key.
 | |
|      */
 | |
|     hasEncryptionKey: {
 | |
|       enumerable: true,
 | |
|       get: function() {
 | |
|         return MozLoopService.hasEncryptionKey;
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Opens the Getting Started tour in the browser.
 | |
|      *
 | |
|      * @param {String} aSrc
 | |
|      *   - The UI element that the user used to begin the tour, optional.
 | |
|      */
 | |
|     openGettingStartedTour: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function(aSrc) {
 | |
|         return MozLoopService.openGettingStartedTour(aSrc);
 | |
|       },
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Opens a URL in a new tab in the browser.
 | |
|      *
 | |
|      * @param {String} url The new url to open
 | |
|      */
 | |
|     openURL: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function(url) {
 | |
|         return MozLoopService.openURL(url);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Copies passed string onto the system clipboard.
 | |
|      *
 | |
|      * @param {String} str The string to copy
 | |
|      */
 | |
|     copyString: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function(str) {
 | |
|         clipboardHelper.copyString(str);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Returns the app version information for use during feedback.
 | |
|      *
 | |
|      * @return {Object} An object containing:
 | |
|      *   - channel: The update channel the application is on
 | |
|      *   - version: The application version
 | |
|      *   - OS: The operating system the application is running on
 | |
|      */
 | |
|     appVersionInfo: {
 | |
|       enumerable: true,
 | |
|       get: function() {
 | |
|         if (!appVersionInfo) {
 | |
|           // If the lazy getter explodes, we're probably loaded in xpcshell,
 | |
|           // which doesn't have what we need, so log an error.
 | |
|           try {
 | |
|             appVersionInfo = Cu.cloneInto({
 | |
|               channel: UpdateUtils.UpdateChannel,
 | |
|               version: appInfo.version,
 | |
|               OS: appInfo.OS
 | |
|             }, targetWindow);
 | |
|           } catch (ex) {
 | |
|             // only log outside of xpcshell to avoid extra message noise
 | |
|             if (typeof targetWindow !== 'undefined' && "console" in targetWindow) {
 | |
|               MozLoopService.log.error("Failed to construct appVersionInfo; if this isn't " +
 | |
|                                        "an xpcshell unit test, something is wrong", ex);
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|         return appVersionInfo;
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Composes an email via the external protocol service.
 | |
|      *
 | |
|      * @param {String} subject   Subject of the email to send
 | |
|      * @param {String} body      Body message of the email to send
 | |
|      * @param {String} recipient Recipient email address (optional)
 | |
|      */
 | |
|     composeEmail: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function(subject, body, recipient) {
 | |
|         recipient = recipient || "";
 | |
|         let mailtoURL = "mailto:" + encodeURIComponent(recipient) +
 | |
|                         "?subject=" + encodeURIComponent(subject) +
 | |
|                         "&body=" + encodeURIComponent(body);
 | |
|         extProtocolSvc.loadURI(CommonUtils.makeURI(mailtoURL));
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Adds a value to a telemetry histogram.
 | |
|      *
 | |
|      * @param  {String}  histogramId Name of the telemetry histogram to update.
 | |
|      * @param  {String}  value       Label of bucket to increment in the histogram.
 | |
|      */
 | |
|     telemetryAddValue: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function(histogramId, value) {
 | |
|         Services.telemetry.getHistogramById(histogramId).add(value);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Returns a new GUID (UUID) in curly braces format.
 | |
|      */
 | |
|     generateUUID: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function() {
 | |
|         return MozLoopService.generateUUID();
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     getAudioBlob: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function(name, callback) {
 | |
|         let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
 | |
|                         .createInstance(Ci.nsIXMLHttpRequest);
 | |
|         let url = `chrome://browser/content/loop/shared/sounds/${name}.ogg`;
 | |
| 
 | |
|         request.open("GET", url, true);
 | |
|         request.responseType = "arraybuffer";
 | |
|         request.onload = () => {
 | |
|           if (request.status < 200 || request.status >= 300) {
 | |
|             let error = new Error(request.status + " " + request.statusText);
 | |
|             invokeCallback(callback, cloneValueInto(error, targetWindow));
 | |
|             return;
 | |
|           }
 | |
| 
 | |
|           let blob = new Blob([request.response], {type: "audio/ogg"});
 | |
|           invokeCallback(callback, null, cloneValueInto(blob, targetWindow));
 | |
|         };
 | |
| 
 | |
|         request.send();
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Compose a URL pointing to the location of an avatar by email address.
 | |
|      * At the moment we use the Gravatar service to match email addresses with
 | |
|      * avatars. If no email address is found we return null.
 | |
|      *
 | |
|      * @param {String} emailAddress Users' email address
 | |
|      * @param {Number} size         Size of the avatar image to return in pixels.
 | |
|      *                              Optional. Default value: 40.
 | |
|      * @return the URL pointing to an avatar matching the provided email address
 | |
|      *         or null if this is not available.
 | |
|      */
 | |
|     getUserAvatar: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function(emailAddress, size = 40) {
 | |
|         if (!emailAddress || !MozLoopService.getLoopPref("contacts.gravatars.show")) {
 | |
|           return null;
 | |
|         }
 | |
| 
 | |
|         // Do the MD5 dance.
 | |
|         let hasher = Cc["@mozilla.org/security/hash;1"]
 | |
|                        .createInstance(Ci.nsICryptoHash);
 | |
|         hasher.init(Ci.nsICryptoHash.MD5);
 | |
|         let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]
 | |
|                              .createInstance(Ci.nsIStringInputStream);
 | |
|         stringStream.data = emailAddress.trim().toLowerCase();
 | |
|         hasher.updateFromStream(stringStream, -1);
 | |
|         let hash = hasher.finish(false);
 | |
|         // Convert the binary hash data to a hex string.
 | |
|         let md5Email = [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
 | |
| 
 | |
|         // Compose the Gravatar URL.
 | |
|         return "https://www.gravatar.com/avatar/" + md5Email + ".jpg?default=blank&s=" + size;
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Gets the metadata related to the currently selected tab in
 | |
|      * the most recent window.
 | |
|      *
 | |
|      * @param {Function} A callback that is passed the metadata.
 | |
|      */
 | |
|     getSelectedTabMetadata: {
 | |
|       value: function(callback) {
 | |
|         let win = Services.wm.getMostRecentWindow("navigator:browser");
 | |
|         win.messageManager.addMessageListener("PageMetadata:PageDataResult", function onPageDataResult(msg) {
 | |
|           win.messageManager.removeMessageListener("PageMetadata:PageDataResult", onPageDataResult);
 | |
|           let pageData = msg.json;
 | |
|           win.LoopUI.getFavicon(function(err, favicon) {
 | |
|             if (err) {
 | |
|               MozLoopService.log.error("Error occurred whilst fetching favicon", err);
 | |
|               // We don't return here intentionally to make sure the callback is
 | |
|               // invoked at all times. We just report the error here.
 | |
|             }
 | |
|             pageData.favicon = favicon || null;
 | |
| 
 | |
|             invokeCallback(callback, cloneValueInto(pageData, targetWindow));
 | |
|           });
 | |
|         });
 | |
|         win.gBrowser.selectedBrowser.messageManager.sendAsyncMessage("PageMetadata:GetPageData");
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Associates a session-id and a call-id with a window for debugging.
 | |
|      *
 | |
|      * @param  {string}  windowId  The window id.
 | |
|      * @param  {string}  sessionId OT session id.
 | |
|      * @param  {string}  callId    The callId on the server.
 | |
|      */
 | |
|     addConversationContext: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function(windowId, sessionId, callid) {
 | |
|         MozLoopService.addConversationContext(windowId, {
 | |
|           sessionId: sessionId,
 | |
|           callId: callid
 | |
|         });
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Notifies the UITour module that an event occurred that it might be
 | |
|      * interested in.
 | |
|      *
 | |
|      * @param {String} subject  Subject of the notification
 | |
|      * @param {mixed}  [params] Optional parameters, providing more details to
 | |
|      *                          the notification subject
 | |
|      */
 | |
|     notifyUITour: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function(subject, params) {
 | |
|         UITour.notify(subject, params);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * 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: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function(windowId, active) {
 | |
|         MozLoopService.setScreenShareState(windowId, active);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Activates the Social Share panel with the Social Provider panel opened
 | |
|      * when the popup open.
 | |
|      */
 | |
|     addSocialShareProvider: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function() {
 | |
|         let win = Services.wm.getMostRecentWindow("navigator:browser");
 | |
|         if (!win || !win.SocialShare) {
 | |
|           return;
 | |
|         }
 | |
|         win.SocialShare.showDirectory(win.LoopUI.toolbarButton.anchor);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Returns a sorted list of Social Providers that can share URLs. See
 | |
|      * `updateSocialProvidersCache()` for more information.
 | |
|      *
 | |
|      * @return {Array} Sorted list of share-capable Social Providers.
 | |
|      */
 | |
|     getSocialShareProviders: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function() {
 | |
|         if (socialProviders) {
 | |
|           return socialProviders;
 | |
|         }
 | |
|         return updateSocialProvidersCache();
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Share a room URL through a Social Provider with the provided title message.
 | |
|      * This action will open the share panel, which is anchored to the Social
 | |
|      * Share widget.
 | |
|      *
 | |
|      * @param {String} providerOrigin Identifier of the targeted Social Provider
 | |
|      * @param {String} roomURL        URL that points to the standalone client
 | |
|      * @param {String} title          Message that augments the URL inside the
 | |
|      *                                share message
 | |
|      * @param {String} [body]         Optional longer message to be displayed
 | |
|      *                                similar to the body of an email 
 | |
|      */
 | |
|     socialShareRoom: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: function(providerOrigin, roomURL, title, body = null) {
 | |
|         let win = Services.wm.getMostRecentWindow("navigator:browser");
 | |
|         if (!win || !win.SocialShare) {
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         let graphData = {
 | |
|           url: roomURL,
 | |
|           title: title
 | |
|         };
 | |
|         if (body) {
 | |
|           graphData.body = body;
 | |
|         }
 | |
|         win.SocialShare.sharePage(providerOrigin, graphData, null,
 | |
|           win.LoopUI.toolbarButton.anchor);
 | |
|       }
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   /**
 | |
|    * Send an event to the content window to indicate that the state on the chrome
 | |
|    * side was updated.
 | |
|    *
 | |
|    * @param  {name} name Name of the event, defaults to 'LoopStatusChanged'
 | |
|    */
 | |
|   function sendEvent(name = "LoopStatusChanged") {
 | |
|     if (typeof targetWindow.CustomEvent != "function") {
 | |
|       MozLoopService.log.debug("Could not send event to content document, " +
 | |
|         "because it's being destroyed or we're in a unit test where " +
 | |
|         "`targetWindow` is mocked.");
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let event = new targetWindow.CustomEvent(name);
 | |
|     targetWindow.dispatchEvent(event);
 | |
|   }
 | |
| 
 | |
|   function onStatusChanged(aSubject, aTopic, aData) {
 | |
|     sendEvent();
 | |
|   }
 | |
| 
 | |
|   function onDOMWindowDestroyed(aSubject, aTopic, aData) {
 | |
|     if (targetWindow && aSubject != targetWindow)
 | |
|       return;
 | |
|     Services.obs.removeObserver(onDOMWindowDestroyed, "dom-window-destroyed");
 | |
|     Services.obs.removeObserver(onStatusChanged, "loop-status-changed");
 | |
|     // Stop listening for changes in the social provider list, if necessary.
 | |
|     if (socialProviders)
 | |
|       Services.obs.removeObserver(updateSocialProvidersCache, "social:providers-changed");
 | |
|     if (socialShareButtonListenersAdded) {
 | |
|       let eventName = "social:" + kShareWidgetId;
 | |
|       Services.obs.removeObserver(onShareWidgetChanged, eventName + "-added");
 | |
|       Services.obs.removeObserver(onShareWidgetChanged, eventName + "-removed");
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function onShareWidgetChanged(aSubject, aTopic, aData) {
 | |
|     sendEvent("LoopShareWidgetChanged");
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Retrieves a list of Social Providers from the Social API that are explicitly
 | |
|    * capable of sharing URLs.
 | |
|    * It also adds a listener that is fired whenever a new Provider is added or
 | |
|    * removed.
 | |
|    *
 | |
|    * @return {Array} Sorted list of share-capable Social Providers.
 | |
|    */
 | |
|   function updateSocialProvidersCache() {
 | |
|     let providers = [];
 | |
| 
 | |
|     for (let provider of Social.providers) {
 | |
|       if (!provider.shareURL) {
 | |
|         continue;
 | |
|       }
 | |
| 
 | |
|       // Only pass the relevant data on to content.
 | |
|       providers.push({
 | |
|         iconURL: provider.iconURL,
 | |
|         name: provider.name,
 | |
|         origin: provider.origin
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     let providersWasSet = !!socialProviders;
 | |
|     // Replace old with new.
 | |
|     socialProviders = cloneValueInto(providers.sort((a, b) =>
 | |
|       a.name.toLowerCase().localeCompare(b.name.toLowerCase())), targetWindow);
 | |
| 
 | |
|     // Start listening for changes in the social provider list, if we're not
 | |
|     // doing that yet.
 | |
|     if (!providersWasSet) {
 | |
|       Services.obs.addObserver(updateSocialProvidersCache, "social:providers-changed", false);
 | |
|     } else {
 | |
|       // Dispatch an event to content to let stores freshen-up.
 | |
|       sendEvent("LoopSocialProvidersChanged");
 | |
|     }
 | |
| 
 | |
|     return socialProviders;
 | |
|   }
 | |
| 
 | |
|   let contentObj = Cu.createObjectIn(targetWindow);
 | |
|   Object.defineProperties(contentObj, api);
 | |
|   Object.seal(contentObj);
 | |
|   Cu.makeObjectPropsNormal(contentObj);
 | |
|   Services.obs.addObserver(onStatusChanged, "loop-status-changed", false);
 | |
|   Services.obs.addObserver(onDOMWindowDestroyed, "dom-window-destroyed", false);
 | |
| 
 | |
|   if ("navigator" in targetWindow) {
 | |
|     targetWindow.navigator.wrappedJSObject.__defineGetter__("mozLoop", function () {
 | |
|       // We do this in a getter, so that we create these objects
 | |
|       // only on demand (this is a potential concern, since
 | |
|       // otherwise we might add one per iframe, and keep them
 | |
|       // alive for as long as the window is alive).
 | |
|       delete targetWindow.navigator.wrappedJSObject.mozLoop;
 | |
|       return targetWindow.navigator.wrappedJSObject.mozLoop = contentObj;
 | |
|     });
 | |
| 
 | |
|     // Handle window.close correctly on the panel and chatbox.
 | |
|     hookWindowCloseForPanelClose(targetWindow);
 | |
|   } else {
 | |
|     // This isn't a window; but it should be a JS scope; used for testing
 | |
|     return targetWindow.mozLoop = contentObj;
 | |
|   }
 | |
| 
 | |
| }
 | |
| 
 | |
| function getChromeWindow(contentWin) {
 | |
|   return contentWin.QueryInterface(Ci.nsIInterfaceRequestor)
 | |
|                    .getInterface(Ci.nsIWebNavigation)
 | |
|                    .QueryInterface(Ci.nsIDocShellTreeItem)
 | |
|                    .rootTreeItem
 | |
|                    .QueryInterface(Ci.nsIInterfaceRequestor)
 | |
|                    .getInterface(Ci.nsIDOMWindow);
 | |
| }
 |