diff --git a/browser/components/extensions/ExtensionControlledPopup.jsm b/browser/components/extensions/ExtensionControlledPopup.jsm index c1e1fa8447e1..d74f90194cae 100644 --- a/browser/components/extensions/ExtensionControlledPopup.jsm +++ b/browser/components/extensions/ExtensionControlledPopup.jsm @@ -21,6 +21,7 @@ var EXPORTED_SYMBOLS = ["ExtensionControlledPopup"]; ChromeUtils.import("resource://gre/modules/Services.jsm"); ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm"); +ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm"); ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); ChromeUtils.defineModuleGetter(this, "AddonManager", @@ -32,7 +33,9 @@ ChromeUtils.defineModuleGetter(this, "CustomizableUI", ChromeUtils.defineModuleGetter(this, "ExtensionSettingsStore", "resource://gre/modules/ExtensionSettingsStore.jsm"); -let {makeWidgetId} = ExtensionUtils; +let { + makeWidgetId, +} = ExtensionCommon; XPCOMUtils.defineLazyGetter(this, "strBundle", function() { return Services.strings.createBundle("chrome://global/locale/extensions.properties"); diff --git a/browser/components/extensions/ExtensionPopups.jsm b/browser/components/extensions/ExtensionPopups.jsm index 2370c60db6a4..fdce3ab26079 100644 --- a/browser/components/extensions/ExtensionPopups.jsm +++ b/browser/components/extensions/ExtensionPopups.jsm @@ -18,14 +18,18 @@ ChromeUtils.defineModuleGetter(this, "setTimeout", "resource://gre/modules/Timer.jsm"); ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); +ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm"); ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm"); var { DefaultWeakMap, - makeWidgetId, promiseEvent, } = ExtensionUtils; +const { + makeWidgetId, +} = ExtensionCommon; + const POPUP_LOAD_TIMEOUT_MS = 200; diff --git a/browser/components/extensions/child/ext-devtools-panels.js b/browser/components/extensions/child/ext-devtools-panels.js index 82a2ab8dbefc..d9150fd6616b 100644 --- a/browser/components/extensions/child/ext-devtools-panels.js +++ b/browser/components/extensions/child/ext-devtools-panels.js @@ -18,7 +18,7 @@ var { * @param {string} panelOptions.id * The id of the addon devtools panel registered in the main process. */ -class ChildDevToolsPanel extends ExtensionUtils.EventEmitter { +class ChildDevToolsPanel extends ExtensionCommon.EventEmitter { constructor(context, {id}) { super(); @@ -144,7 +144,7 @@ class ChildDevToolsPanel extends ExtensionUtils.EventEmitter { * @param {string} sidebarOptions.id * The id of the addon devtools sidebar registered in the main process. */ -class ChildDevToolsInspectorSidebar extends ExtensionUtils.EventEmitter { +class ChildDevToolsInspectorSidebar extends ExtensionCommon.EventEmitter { constructor(context, {id}) { super(); diff --git a/browser/components/extensions/child/ext-menus.js b/browser/components/extensions/child/ext-menus.js index b1dd7a366352..f252350a7188 100644 --- a/browser/components/extensions/child/ext-menus.js +++ b/browser/components/extensions/child/ext-menus.js @@ -4,7 +4,7 @@ var { withHandlingUserInput, -} = ExtensionUtils; +} = ExtensionCommon; // If id is not specified for an item we use an integer. // This ID need only be unique within a single addon. Since all addon code that diff --git a/browser/components/extensions/parent/ext-browser.js b/browser/components/extensions/parent/ext-browser.js index f7711a5599b0..55ca09c6e54e 100644 --- a/browser/components/extensions/parent/ext-browser.js +++ b/browser/components/extensions/parent/ext-browser.js @@ -13,9 +13,12 @@ ChromeUtils.defineModuleGetter(this, "BrowserWindowTracker", var { ExtensionError, - defineLazyGetter, } = ExtensionUtils; +var { + defineLazyGetter, +} = ExtensionCommon; + const READER_MODE_PREFIX = "about:reader"; let tabTracker; @@ -31,8 +34,8 @@ const getSender = (extension, target, sender) => { // page-open listener below). tabId = sender.tabId; delete sender.tabId; - } else if (ExtensionUtils.instanceOf(target, "XULElement") || - ExtensionUtils.instanceOf(target, "HTMLIFrameElement")) { + } else if (ExtensionCommon.instanceOf(target, "XULElement") || + ExtensionCommon.instanceOf(target, "HTMLIFrameElement")) { tabId = tabTracker.getBrowserData(target).tabId; } diff --git a/browser/components/extensions/parent/ext-history.js b/browser/components/extensions/parent/ext-history.js index b5a995b2a908..997e92d27064 100644 --- a/browser/components/extensions/parent/ext-history.js +++ b/browser/components/extensions/parent/ext-history.js @@ -9,7 +9,7 @@ ChromeUtils.defineModuleGetter(this, "Services", var { normalizeTime, -} = ExtensionUtils; +} = ExtensionCommon; let nsINavHistoryService = Ci.nsINavHistoryService; const TRANSITION_TO_TRANSITION_TYPES_MAP = new Map([ diff --git a/browser/components/extensions/test/xpcshell/test_ext_history.js b/browser/components/extensions/test/xpcshell/test_ext_history.js index e8d38f2baba9..4048a692bcfd 100644 --- a/browser/components/extensions/test/xpcshell/test_ext_history.js +++ b/browser/components/extensions/test/xpcshell/test_ext_history.js @@ -6,8 +6,8 @@ ChromeUtils.defineModuleGetter(this, "PlacesTestUtils", "resource://testing-common/PlacesTestUtils.jsm"); ChromeUtils.defineModuleGetter(this, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm"); -ChromeUtils.defineModuleGetter(this, "ExtensionUtils", - "resource://gre/modules/ExtensionUtils.jsm"); +ChromeUtils.defineModuleGetter(this, "ExtensionCommon", + "resource://gre/modules/ExtensionCommon.jsm"); ChromeUtils.import("resource://testing-common/PromiseTestUtils.jsm"); @@ -308,7 +308,7 @@ add_task(async function test_add_url() { equal(results.result.title, results.details.title, "URL was added with the correct title"); if (results.details.visitTime) { equal(results.result.lastVisitTime, - Number(ExtensionUtils.normalizeTime(results.details.visitTime)), + Number(ExtensionCommon.normalizeTime(results.details.visitTime)), "URL was added with the correct date"); } } diff --git a/mobile/android/components/extensions/ext-utils.js b/mobile/android/components/extensions/ext-utils.js index 60497dd91d5c..4b4f218d44c2 100644 --- a/mobile/android/components/extensions/ext-utils.js +++ b/mobile/android/components/extensions/ext-utils.js @@ -9,14 +9,18 @@ ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils", /* globals EventDispatcher */ ChromeUtils.import("resource://gre/modules/Messaging.jsm"); +ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm"); ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm"); var { DefaultWeakMap, ExtensionError, - defineLazyGetter, } = ExtensionUtils; +var { + defineLazyGetter, +} = ExtensionCommon; + global.GlobalEventDispatcher = EventDispatcher.instance; const BrowserStatusFilter = Components.Constructor( diff --git a/toolkit/components/extensions/Extension.jsm b/toolkit/components/extensions/Extension.jsm index f523ecce4ac8..ac147703ac8e 100644 --- a/toolkit/components/extensions/Extension.jsm +++ b/toolkit/components/extensions/Extension.jsm @@ -42,7 +42,6 @@ XPCOMUtils.defineLazyModuleGetters(this, { AppConstants: "resource://gre/modules/AppConstants.jsm", AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm", ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.jsm", - ExtensionCommon: "resource://gre/modules/ExtensionCommon.jsm", ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.jsm", ExtensionStorage: "resource://gre/modules/ExtensionStorage.jsm", ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.jsm", @@ -77,6 +76,7 @@ XPCOMUtils.defineLazyGetter( () => Services.io.getProtocolHandler("resource") .QueryInterface(Ci.nsIResProtocolHandler)); +ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm"); ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm"); ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm"); @@ -96,12 +96,15 @@ var { } = ExtensionParent; const { - EventEmitter, getUniqueId, promiseTimeout, } = ExtensionUtils; -XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole); +const { + EventEmitter, +} = ExtensionCommon; + +XPCOMUtils.defineLazyGetter(this, "console", ExtensionCommon.getConsole); XPCOMUtils.defineLazyGetter(this, "LocaleData", () => ExtensionCommon.LocaleData); @@ -1430,7 +1433,7 @@ class Extension extends ExtensionData { return true; } - return ExtensionUtils.checkLoadURL(url, this.principal, options); + return ExtensionCommon.checkLoadURL(url, this.principal, options); } async promiseLocales(locale) { diff --git a/toolkit/components/extensions/ExtensionChild.jsm b/toolkit/components/extensions/ExtensionChild.jsm index a921331edc55..33de509f96f7 100644 --- a/toolkit/components/extensions/ExtensionChild.jsm +++ b/toolkit/components/extensions/ExtensionChild.jsm @@ -42,21 +42,21 @@ ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm"); const { DefaultMap, - EventEmitter, LimitedSet, - defineLazyGetter, getMessageManager, getUniqueId, getWinUtils, - withHandlingUserInput, } = ExtensionUtils; const { + EventEmitter, EventManager, LocalAPIImplementation, LocaleData, NoCloneSpreadArgs, SchemaAPIInterface, + defineLazyGetter, + withHandlingUserInput, } = ExtensionCommon; const isContentProcess = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT; diff --git a/toolkit/components/extensions/ExtensionCommon.jsm b/toolkit/components/extensions/ExtensionCommon.jsm index fded102c4a95..489d58c0c435 100644 --- a/toolkit/components/extensions/ExtensionCommon.jsm +++ b/toolkit/components/extensions/ExtensionCommon.jsm @@ -22,6 +22,7 @@ XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]); XPCOMUtils.defineLazyModuleGetters(this, { AppConstants: "resource://gre/modules/AppConstants.jsm", + ConsoleAPI: "resource://gre/modules/Console.jsm", MessageChannel: "resource://gre/modules/MessageChannel.jsm", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm", Schemas: "resource://gre/modules/Schemas.jsm", @@ -37,16 +38,20 @@ ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm"); var { DefaultMap, DefaultWeakMap, - EventEmitter, ExtensionError, - defineLazyGetter, filterStack, - getConsole, getInnerWindowID, getUniqueId, getWinUtils, } = ExtensionUtils; +function getConsole() { + return new ConsoleAPI({ + maxLogLevelPref: "extensions.webextensions.log.level", + prefix: "WebExtensions", + }); +} + XPCOMUtils.defineLazyGetter(this, "console", getConsole); XPCOMUtils.defineLazyPreferenceGetter(this, "DELAYED_BG_STARTUP", @@ -54,6 +59,121 @@ XPCOMUtils.defineLazyPreferenceGetter(this, "DELAYED_BG_STARTUP", var ExtensionCommon; +// Run a function and report exceptions. +function runSafeSyncWithoutClone(f, ...args) { + try { + return f(...args); + } catch (e) { + dump(`Extension error: ${e} ${e.fileName} ${e.lineNumber}\n[[Exception stack\n${filterStack(e)}Current stack\n${filterStack(Error())}]]\n`); + Cu.reportError(e); + } +} + +// Return true if the given value is an instance of the given +// native type. +function instanceOf(value, type) { + return (value && typeof value === "object" && + ChromeUtils.getClassName(value) === type); +} + +/** + * Convert any of several different representations of a date/time to a Date object. + * Accepts several formats: + * a Date object, an ISO8601 string, or a number of milliseconds since the epoch as + * either a number or a string. + * + * @param {Date|string|number} date + * The date to convert. + * @returns {Date} + * A Date object + */ +function normalizeTime(date) { + // Of all the formats we accept the "number of milliseconds since the epoch as a string" + // is an outlier, everything else can just be passed directly to the Date constructor. + return new Date((typeof date == "string" && /^\d+$/.test(date)) + ? parseInt(date, 10) : date); +} + +function withHandlingUserInput(window, callable) { + let handle = getWinUtils(window).setHandlingUserInput(true); + try { + return callable(); + } finally { + handle.destruct(); + } +} + +/** + * Defines a lazy getter for the given property on the given object. The + * first time the property is accessed, the return value of the getter + * is defined on the current `this` object with the given property name. + * Importantly, this means that a lazy getter defined on an object + * prototype will be invoked separately for each object instance that + * it's accessed on. + * + * @param {object} object + * The prototype object on which to define the getter. + * @param {string|Symbol} prop + * The property name for which to define the getter. + * @param {function} getter + * The function to call in order to generate the final property + * value. + */ +function defineLazyGetter(object, prop, getter) { + let redefine = (obj, value) => { + Object.defineProperty(obj, prop, { + enumerable: true, + configurable: true, + writable: true, + value, + }); + return value; + }; + + Object.defineProperty(object, prop, { + enumerable: true, + configurable: true, + + get() { + return redefine(this, getter.call(this)); + }, + + set(value) { + redefine(this, value); + }, + }); +} + +function checkLoadURL(url, principal, options) { + let ssm = Services.scriptSecurityManager; + + let flags = ssm.STANDARD; + if (!options.allowScript) { + flags |= ssm.DISALLOW_SCRIPT; + } + if (!options.allowInheritsPrincipal) { + flags |= ssm.DISALLOW_INHERIT_PRINCIPAL; + } + if (options.dontReportErrors) { + flags |= ssm.DONT_REPORT_ERRORS; + } + + try { + ssm.checkLoadURIWithPrincipal(principal, + Services.io.newURI(url), + flags); + } catch (e) { + return false; + } + return true; +} + +function makeWidgetId(id) { + id = id.toLowerCase(); + // FIXME: This allows for collisions. + return id.replace(/[^a-z0-9_-]/g, "_"); +} + /** * A sentinel class to indicate that an array of values should be * treated as an array when used as a promise resolution value, but as a @@ -85,12 +205,119 @@ class NoCloneSpreadArgs { } } +const LISTENERS = Symbol("listeners"); +const ONCE_MAP = Symbol("onceMap"); + +class EventEmitter { + constructor() { + this[LISTENERS] = new Map(); + this[ONCE_MAP] = new WeakMap(); + } + + /** + * Adds the given function as a listener for the given event. + * + * The listener function may optionally return a Promise which + * resolves when it has completed all operations which event + * dispatchers may need to block on. + * + * @param {string} event + * The name of the event to listen for. + * @param {function(string, ...any)} listener + * The listener to call when events are emitted. + */ + on(event, listener) { + let listeners = this[LISTENERS].get(event); + if (!listeners) { + listeners = new Set(); + this[LISTENERS].set(event, listeners); + } + + listeners.add(listener); + } + + /** + * Removes the given function as a listener for the given event. + * + * @param {string} event + * The name of the event to stop listening for. + * @param {function(string, ...any)} listener + * The listener function to remove. + */ + off(event, listener) { + let set = this[LISTENERS].get(event); + if (set) { + set.delete(listener); + set.delete(this[ONCE_MAP].get(listener)); + if (!set.size) { + this[LISTENERS].delete(event); + } + } + } + + /** + * Adds the given function as a listener for the given event once. + * + * @param {string} event + * The name of the event to listen for. + * @param {function(string, ...any)} listener + * The listener to call when events are emitted. + */ + once(event, listener) { + let wrapper = (...args) => { + this.off(event, wrapper); + this[ONCE_MAP].delete(listener); + + return listener(...args); + }; + this[ONCE_MAP].set(listener, wrapper); + + this.on(event, wrapper); + } + + + /** + * Triggers all listeners for the given event. If any listeners return + * a value, returns a promise which resolves when all returned + * promises have resolved. Otherwise, returns undefined. + * + * @param {string} event + * The name of the event to emit. + * @param {any} args + * Arbitrary arguments to pass to the listener functions, after + * the event name. + * @returns {Promise?} + */ + emit(event, ...args) { + let listeners = this[LISTENERS].get(event); + + if (listeners) { + let promises = []; + + for (let listener of listeners) { + try { + let result = listener(event, ...args); + if (result !== undefined) { + promises.push(result); + } + } catch (e) { + Cu.reportError(e); + } + } + + if (promises.length) { + return Promise.all(promises); + } + } + } +} + /** * Base class for WebExtension APIs. Each API creates a new class * that inherits from this class, the derived class is instantiated * once for each extension that uses the API. */ -class ExtensionAPI extends ExtensionUtils.EventEmitter { +class ExtensionAPI extends EventEmitter { constructor(extension) { super(); @@ -246,7 +473,7 @@ class BaseContext { return true; } - return ExtensionUtils.checkLoadURL(url, this.principal, options); + return checkLoadURL(url, this.principal, options); } /** @@ -2074,14 +2301,23 @@ ExtensionCommon = { CanOfAPIs, EventManager, ExtensionAPI, + EventEmitter, LocalAPIImplementation, LocaleData, NoCloneSpreadArgs, SchemaAPIInterface, SchemaAPIManager, SpreadArgs, + checkLoadURL, + defineLazyGetter, + getConsole, ignoreEvent, + instanceOf, + makeWidgetId, + normalizeTime, + runSafeSyncWithoutClone, stylesheetMap, + withHandlingUserInput, MultiAPIManager, LazyAPIManager, diff --git a/toolkit/components/extensions/ExtensionContent.jsm b/toolkit/components/extensions/ExtensionContent.jsm index 8320621bf6e8..a249136f4ebe 100644 --- a/toolkit/components/extensions/ExtensionContent.jsm +++ b/toolkit/components/extensions/ExtensionContent.jsm @@ -39,19 +39,19 @@ XPCOMUtils.defineLazyGlobalGetters(this, ["crypto", "TextEncoder"]); const { DefaultMap, DefaultWeakMap, - defineLazyGetter, getInnerWindowID, getWinUtils, promiseDocumentIdle, promiseDocumentLoaded, promiseDocumentReady, - runSafeSyncWithoutClone, } = ExtensionUtils; const { BaseContext, CanOfAPIs, SchemaAPIManager, + defineLazyGetter, + runSafeSyncWithoutClone, } = ExtensionCommon; const { @@ -60,7 +60,7 @@ const { Messenger, } = ExtensionChild; -XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole); +XPCOMUtils.defineLazyGetter(this, "console", ExtensionCommon.getConsole); var DocumentManager; diff --git a/toolkit/components/extensions/ExtensionPageChild.jsm b/toolkit/components/extensions/ExtensionPageChild.jsm index a45eac9a6ee5..2493f525e135 100644 --- a/toolkit/components/extensions/ExtensionPageChild.jsm +++ b/toolkit/components/extensions/ExtensionPageChild.jsm @@ -37,7 +37,6 @@ ChromeUtils.import("resource://gre/modules/ExtensionChild.jsm"); ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm"); const { - defineLazyGetter, getInnerWindowID, promiseEvent, } = ExtensionUtils; @@ -46,6 +45,7 @@ const { BaseContext, CanOfAPIs, SchemaAPIManager, + defineLazyGetter, } = ExtensionCommon; const { diff --git a/toolkit/components/extensions/ExtensionParent.jsm b/toolkit/components/extensions/ExtensionParent.jsm index 0d0bb575f756..1a3647b9e2a4 100644 --- a/toolkit/components/extensions/ExtensionParent.jsm +++ b/toolkit/components/extensions/ExtensionParent.jsm @@ -25,8 +25,9 @@ XPCOMUtils.defineLazyModuleGetters(this, { E10SUtils: "resource://gre/modules/E10SUtils.jsm", ExtensionData: "resource://gre/modules/Extension.jsm", MessageChannel: "resource://gre/modules/MessageChannel.jsm", - OS: "resource://gre/modules/osfile.jsm", + MessageManagerProxy: "resource://gre/modules/MessageManagerProxy.jsm", NativeApp: "resource://gre/modules/NativeMessaging.jsm", + OS: "resource://gre/modules/osfile.jsm", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm", Schemas: "resource://gre/modules/Schemas.jsm", }); @@ -43,14 +44,13 @@ var { CanOfAPIs, SchemaAPIManager, SpreadArgs, + defineLazyGetter, } = ExtensionCommon; var { DefaultMap, DefaultWeakMap, ExtensionError, - MessageManagerProxy, - defineLazyGetter, promiseDocumentLoaded, promiseEvent, promiseObserved, diff --git a/toolkit/components/extensions/ExtensionStorageSync.jsm b/toolkit/components/extensions/ExtensionStorageSync.jsm index d7404ab94b84..02c8c05e1874 100644 --- a/toolkit/components/extensions/ExtensionStorageSync.jsm +++ b/toolkit/components/extensions/ExtensionStorageSync.jsm @@ -48,6 +48,7 @@ XPCOMUtils.defineLazyModuleGetters(this, { CollectionKeyManager: "resource://services-sync/record.js", CommonUtils: "resource://services-common/utils.js", CryptoUtils: "resource://services-crypto/utils.js", + ExtensionCommon: "resource://gre/modules/ExtensionCommon.jsm", fxAccounts: "resource://gre/modules/FxAccounts.jsm", KintoHttpClient: "resource://services-common/kinto-http-client.js", Kinto: "resource://services-common/kinto-offline-client.js", @@ -68,7 +69,6 @@ XPCOMUtils.defineLazyGetter(this, "WeaveCrypto", function() { const { DefaultMap, - runSafeSyncWithoutClone, } = ExtensionUtils; // Map of Extensions to Set to track contexts that are still @@ -1249,7 +1249,7 @@ class ExtensionStorageSync { let listeners = this.listeners.get(extension) || new Set(); if (listeners) { for (let listener of listeners) { - runSafeSyncWithoutClone(listener, changes); + ExtensionCommon.runSafeSyncWithoutClone(listener, changes); } } } diff --git a/toolkit/components/extensions/ExtensionTestCommon.jsm b/toolkit/components/extensions/ExtensionTestCommon.jsm index 770c585cc692..8810d36c5070 100644 --- a/toolkit/components/extensions/ExtensionTestCommon.jsm +++ b/toolkit/components/extensions/ExtensionTestCommon.jsm @@ -33,6 +33,7 @@ ChromeUtils.defineModuleGetter(this, "OS", XPCOMUtils.defineLazyGetter(this, "apiManager", () => ExtensionParent.apiManager); +ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm"); ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "uuidGen", @@ -41,10 +42,13 @@ XPCOMUtils.defineLazyServiceGetter(this, "uuidGen", const { flushJarCache, - instanceOf, } = ExtensionUtils; -XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole); +const { + instanceOf, +} = ExtensionCommon; + +XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionCommon.getConsole()); /** diff --git a/toolkit/components/extensions/ExtensionUtils.jsm b/toolkit/components/extensions/ExtensionUtils.jsm index 9ad1776175b7..3d00a4490052 100644 --- a/toolkit/components/extensions/ExtensionUtils.jsm +++ b/toolkit/components/extensions/ExtensionUtils.jsm @@ -10,20 +10,9 @@ var EXPORTED_SYMBOLS = ["ExtensionUtils"]; ChromeUtils.import("resource://gre/modules/Services.jsm"); ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); -ChromeUtils.defineModuleGetter(this, "ConsoleAPI", - "resource://gre/modules/Console.jsm"); ChromeUtils.defineModuleGetter(this, "setTimeout", "resource://gre/modules/Timer.jsm"); -function getConsole() { - return new ConsoleAPI({ - maxLogLevelPref: "extensions.webextensions.log.level", - prefix: "WebExtensions", - }); -} - -XPCOMUtils.defineLazyGetter(this, "console", getConsole); - // xpcshell doesn't handle idle callbacks well. XPCOMUtils.defineLazyGetter(this, "idleTimeout", () => Services.appinfo.name === "XPCShell" ? 500 : undefined); @@ -62,23 +51,6 @@ function filterStack(error) { return String(error.stack).replace(/(^.*(Task\.jsm|Promise-backend\.js).*\n)+/gm, "\n"); } -// Run a function and report exceptions. -function runSafeSyncWithoutClone(f, ...args) { - try { - return f(...args); - } catch (e) { - dump(`Extension error: ${e} ${e.fileName} ${e.lineNumber}\n[[Exception stack\n${filterStack(e)}Current stack\n${filterStack(Error())}]]\n`); - Cu.reportError(e); - } -} - -// Return true if the given value is an instance of the given -// native type. -function instanceOf(value, type) { - return (value && typeof value === "object" && - ChromeUtils.getClassName(value) === type); -} - /** * Similar to a WeakMap, but creates a new key with the given * constructor if one is not present. @@ -129,122 +101,6 @@ function getInnerWindowID(window) { return getWinUtils(window).currentInnerWindowID; } -function withHandlingUserInput(window, callable) { - let handle = getWinUtils(window).setHandlingUserInput(true); - try { - return callable(); - } finally { - handle.destruct(); - } -} - -const LISTENERS = Symbol("listeners"); -const ONCE_MAP = Symbol("onceMap"); - -class EventEmitter { - constructor() { - this[LISTENERS] = new Map(); - this[ONCE_MAP] = new WeakMap(); - } - - /** - * Adds the given function as a listener for the given event. - * - * The listener function may optionally return a Promise which - * resolves when it has completed all operations which event - * dispatchers may need to block on. - * - * @param {string} event - * The name of the event to listen for. - * @param {function(string, ...any)} listener - * The listener to call when events are emitted. - */ - on(event, listener) { - let listeners = this[LISTENERS].get(event); - if (!listeners) { - listeners = new Set(); - this[LISTENERS].set(event, listeners); - } - - listeners.add(listener); - } - - /** - * Removes the given function as a listener for the given event. - * - * @param {string} event - * The name of the event to stop listening for. - * @param {function(string, ...any)} listener - * The listener function to remove. - */ - off(event, listener) { - let set = this[LISTENERS].get(event); - if (set) { - set.delete(listener); - set.delete(this[ONCE_MAP].get(listener)); - if (!set.size) { - this[LISTENERS].delete(event); - } - } - } - - /** - * Adds the given function as a listener for the given event once. - * - * @param {string} event - * The name of the event to listen for. - * @param {function(string, ...any)} listener - * The listener to call when events are emitted. - */ - once(event, listener) { - let wrapper = (...args) => { - this.off(event, wrapper); - this[ONCE_MAP].delete(listener); - - return listener(...args); - }; - this[ONCE_MAP].set(listener, wrapper); - - this.on(event, wrapper); - } - - - /** - * Triggers all listeners for the given event. If any listeners return - * a value, returns a promise which resolves when all returned - * promises have resolved. Otherwise, returns undefined. - * - * @param {string} event - * The name of the event to emit. - * @param {any} args - * Arbitrary arguments to pass to the listener functions, after - * the event name. - * @returns {Promise?} - */ - emit(event, ...args) { - let listeners = this[LISTENERS].get(event); - - if (listeners) { - let promises = []; - - for (let listener of listeners) { - try { - let result = listener(event, ...args); - if (result !== undefined) { - promises.push(result); - } - } catch (e) { - Cu.reportError(e); - } - } - - if (promises.length) { - return Promise.all(promises); - } - } - } -} - /** * A set with a limited number of slots, which flushes older entries as * newer ones are added. @@ -404,303 +260,21 @@ function flushJarCache(jarPath) { Services.obs.notifyObservers(null, "flush-cache-entry", jarPath); } -/** - * Convert any of several different representations of a date/time to a Date object. - * Accepts several formats: - * a Date object, an ISO8601 string, or a number of milliseconds since the epoch as - * either a number or a string. - * - * @param {Date|string|number} date - * The date to convert. - * @returns {Date} - * A Date object - */ -function normalizeTime(date) { - // Of all the formats we accept the "number of milliseconds since the epoch as a string" - // is an outlier, everything else can just be passed directly to the Date constructor. - return new Date((typeof date == "string" && /^\d+$/.test(date)) - ? parseInt(date, 10) : date); -} - -/** - * Defines a lazy getter for the given property on the given object. The - * first time the property is accessed, the return value of the getter - * is defined on the current `this` object with the given property name. - * Importantly, this means that a lazy getter defined on an object - * prototype will be invoked separately for each object instance that - * it's accessed on. - * - * @param {object} object - * The prototype object on which to define the getter. - * @param {string|Symbol} prop - * The property name for which to define the getter. - * @param {function} getter - * The function to call in order to generate the final property - * value. - */ -function defineLazyGetter(object, prop, getter) { - let redefine = (obj, value) => { - Object.defineProperty(obj, prop, { - enumerable: true, - configurable: true, - writable: true, - value, - }); - return value; - }; - - Object.defineProperty(object, prop, { - enumerable: true, - configurable: true, - - get() { - return redefine(this, getter.call(this)); - }, - - set(value) { - redefine(this, value); - }, - }); -} - -/** - * Acts as a proxy for a message manager or message manager owner, and - * tracks docShell swaps so that messages are always sent to the same - * receiver, even if it is moved to a different . - * - * @param {nsIMessageSender|Element} target - * The target message manager on which to send messages, or the - * element which owns it. - */ -class MessageManagerProxy { - constructor(target) { - this.listeners = new DefaultMap(() => new Map()); - this.closed = false; - - if (target instanceof Ci.nsIMessageSender) { - this.messageManager = target; - } else { - this.addListeners(target); - } - - Services.obs.addObserver(this, "message-manager-close"); - } - - /** - * Disposes of the proxy object, removes event listeners, and drops - * all references to the underlying message manager. - * - * Must be called before the last reference to the proxy is dropped, - * unless the underlying message manager or is also being - * destroyed. - */ - dispose() { - if (this.eventTarget) { - this.removeListeners(this.eventTarget); - this.eventTarget = null; - } - this.messageManager = null; - - Services.obs.removeObserver(this, "message-manager-close"); - } - - observe(subject, topic, data) { - if (topic === "message-manager-close") { - if (subject === this.messageManager) { - this.closed = true; - } - } - } - - /** - * Returns true if the given target is the same as, or owns, the given - * message manager. - * - * @param {nsIMessageSender|MessageManagerProxy|Element} target - * The message manager, MessageManagerProxy, or - * element against which to match. - * @param {nsIMessageSender} messageManager - * The message manager against which to match `target`. - * - * @returns {boolean} - * True if `messageManager` is the same object as `target`, or - * `target` is a MessageManagerProxy or element that - * is tied to it. - */ - static matches(target, messageManager) { - return target === messageManager || target.messageManager === messageManager; - } - - /** - * @property {nsIMessageSender|null} messageManager - * The message manager that is currently being proxied. This - * may change during the life of the proxy object, so should - * not be stored elsewhere. - */ - - /** - * Sends a message on the proxied message manager. - * - * @param {array} args - * Arguments to be passed verbatim to the underlying - * sendAsyncMessage method. - * @returns {undefined} - */ - sendAsyncMessage(...args) { - if (this.messageManager) { - return this.messageManager.sendAsyncMessage(...args); - } - - Cu.reportError(`Cannot send message: Other side disconnected: ${uneval(args)}`); - } - - get isDisconnected() { - return this.closed || !this.messageManager; - } - - /** - * Adds a message listener to the current message manager, and - * transfers it to the new message manager after a docShell swap. - * - * @param {string} message - * The name of the message to listen for. - * @param {nsIMessageListener} listener - * The listener to add. - * @param {boolean} [listenWhenClosed = false] - * If true, the listener will receive messages which were sent - * after the remote side of the listener began closing. - */ - addMessageListener(message, listener, listenWhenClosed = false) { - this.messageManager.addMessageListener(message, listener, listenWhenClosed); - this.listeners.get(message).set(listener, listenWhenClosed); - } - - /** - * Adds a message listener from the current message manager. - * - * @param {string} message - * The name of the message to stop listening for. - * @param {nsIMessageListener} listener - * The listener to remove. - */ - removeMessageListener(message, listener) { - this.messageManager.removeMessageListener(message, listener); - - let listeners = this.listeners.get(message); - listeners.delete(listener); - if (!listeners.size) { - this.listeners.delete(message); - } - } - - /** - * @private - * Iterates over all of the currently registered message listeners. - */ - * iterListeners() { - for (let [message, listeners] of this.listeners) { - for (let [listener, listenWhenClosed] of listeners) { - yield {message, listener, listenWhenClosed}; - } - } - } - - /** - * @private - * Adds docShell swap listeners to the message manager owner. - * - * @param {Element} target - * The target element. - */ - addListeners(target) { - target.addEventListener("SwapDocShells", this); - - this.eventTarget = target; - this.messageManager = target.messageManager; - - for (let {message, listener, listenWhenClosed} of this.iterListeners()) { - this.messageManager.addMessageListener(message, listener, listenWhenClosed); - } - } - - /** - * @private - * Removes docShell swap listeners to the message manager owner. - * - * @param {Element} target - * The target element. - */ - removeListeners(target) { - target.removeEventListener("SwapDocShells", this); - - for (let {message, listener} of this.iterListeners()) { - this.messageManager.removeMessageListener(message, listener); - } - } - - handleEvent(event) { - if (event.type == "SwapDocShells") { - this.removeListeners(this.eventTarget); - this.addListeners(event.detail); - } - } -} - -function checkLoadURL(url, principal, options) { - let ssm = Services.scriptSecurityManager; - - let flags = ssm.STANDARD; - if (!options.allowScript) { - flags |= ssm.DISALLOW_SCRIPT; - } - if (!options.allowInheritsPrincipal) { - flags |= ssm.DISALLOW_INHERIT_PRINCIPAL; - } - if (options.dontReportErrors) { - flags |= ssm.DONT_REPORT_ERRORS; - } - - try { - ssm.checkLoadURIWithPrincipal(principal, - Services.io.newURI(url), - flags); - } catch (e) { - return false; - } - return true; -} - -function makeWidgetId(id) { - id = id.toLowerCase(); - // FIXME: This allows for collisions. - return id.replace(/[^a-z0-9_-]/g, "_"); -} - var ExtensionUtils = { - checkLoadURL, - defineLazyGetter, flushJarCache, - getConsole, getInnerWindowID, getMessageManager, getUniqueId, filterStack, getWinUtils, - instanceOf, - makeWidgetId, - normalizeTime, promiseDocumentIdle, promiseDocumentLoaded, promiseDocumentReady, promiseEvent, promiseObserved, promiseTimeout, - runSafeSyncWithoutClone, - withHandlingUserInput, DefaultMap, DefaultWeakMap, - EventEmitter, ExtensionError, LimitedSet, - MessageManagerProxy, }; diff --git a/toolkit/components/extensions/MessageChannel.jsm b/toolkit/components/extensions/MessageChannel.jsm index f6bdb9413c67..49e1ce63329e 100644 --- a/toolkit/components/extensions/MessageChannel.jsm +++ b/toolkit/components/extensions/MessageChannel.jsm @@ -105,9 +105,19 @@ ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm"); ChromeUtils.import("resource://gre/modules/Services.jsm"); -const { - MessageManagerProxy, -} = ExtensionUtils; +ChromeUtils.defineModuleGetter(this, "MessageManagerProxy", + "resource://gre/modules/MessageManagerProxy.jsm"); + +function getMessageManager(target) { + if (typeof target.sendAsyncMessage === "function") { + return target; + } + return new MessageManagerProxy(target); +} + +function matches(target, messageManager) { + return target === messageManager || target.messageManager === messageManager; +} const {DEBUG} = AppConstants; @@ -919,7 +929,7 @@ this.MessageChannel = { return; } - let target = new MessageManagerProxy(data.target); + let target = getMessageManager(data.target); let deferred = { sender: data.sender, @@ -930,7 +940,9 @@ this.MessageChannel = { let cleanup = () => { this.pendingResponses.delete(deferred); - target.dispose(); + if (target.dispose) { + target.dispose(); + } }; this.pendingResponses.add(deferred); @@ -1078,7 +1090,7 @@ this.MessageChannel = { */ abortMessageManager(target, reason) { for (let response of this.pendingResponses) { - if (MessageManagerProxy.matches(response.messageManager, target)) { + if (matches(response.messageManager, target)) { this.abortedResponses.add(response.channelId); response.reject(reason); } diff --git a/toolkit/components/extensions/MessageManagerProxy.jsm b/toolkit/components/extensions/MessageManagerProxy.jsm new file mode 100644 index 000000000000..e35b96b64746 --- /dev/null +++ b/toolkit/components/extensions/MessageManagerProxy.jsm @@ -0,0 +1,198 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +/* 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"; + +var EXPORTED_SYMBOLS = ["MessageManagerProxy"]; + +ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm"); +ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const { + DefaultMap, +} = ExtensionUtils; + +/** + * Acts as a proxy for a message manager or message manager owner, and + * tracks docShell swaps so that messages are always sent to the same + * receiver, even if it is moved to a different . + * + * @param {nsIMessageSender|Element} target + * The target message manager on which to send messages, or the + * element which owns it. + */ +class MessageManagerProxy { + constructor(target) { + this.listeners = new DefaultMap(() => new Map()); + this.closed = false; + + if (target instanceof Ci.nsIMessageSender) { + this.messageManager = target; + } else { + this.addListeners(target); + } + + Services.obs.addObserver(this, "message-manager-close"); + } + + /** + * Disposes of the proxy object, removes event listeners, and drops + * all references to the underlying message manager. + * + * Must be called before the last reference to the proxy is dropped, + * unless the underlying message manager or is also being + * destroyed. + */ + dispose() { + if (this.eventTarget) { + this.removeListeners(this.eventTarget); + this.eventTarget = null; + } + this.messageManager = null; + + Services.obs.removeObserver(this, "message-manager-close"); + } + + observe(subject, topic, data) { + if (topic === "message-manager-close") { + if (subject === this.messageManager) { + this.closed = true; + } + } + } + + /** + * Returns true if the given target is the same as, or owns, the given + * message manager. + * + * @param {nsIMessageSender|MessageManagerProxy|Element} target + * The message manager, MessageManagerProxy, or + * element against which to match. + * @param {nsIMessageSender} messageManager + * The message manager against which to match `target`. + * + * @returns {boolean} + * True if `messageManager` is the same object as `target`, or + * `target` is a MessageManagerProxy or element that + * is tied to it. + */ + static matches(target, messageManager) { + return target === messageManager || target.messageManager === messageManager; + } + + /** + * @property {nsIMessageSender|null} messageManager + * The message manager that is currently being proxied. This + * may change during the life of the proxy object, so should + * not be stored elsewhere. + */ + + /** + * Sends a message on the proxied message manager. + * + * @param {array} args + * Arguments to be passed verbatim to the underlying + * sendAsyncMessage method. + * @returns {undefined} + */ + sendAsyncMessage(...args) { + if (this.messageManager) { + return this.messageManager.sendAsyncMessage(...args); + } + + Cu.reportError(`Cannot send message: Other side disconnected: ${uneval(args)}`); + } + + get isDisconnected() { + return this.closed || !this.messageManager; + } + + /** + * Adds a message listener to the current message manager, and + * transfers it to the new message manager after a docShell swap. + * + * @param {string} message + * The name of the message to listen for. + * @param {nsIMessageListener} listener + * The listener to add. + * @param {boolean} [listenWhenClosed = false] + * If true, the listener will receive messages which were sent + * after the remote side of the listener began closing. + */ + addMessageListener(message, listener, listenWhenClosed = false) { + this.messageManager.addMessageListener(message, listener, listenWhenClosed); + this.listeners.get(message).set(listener, listenWhenClosed); + } + + /** + * Adds a message listener from the current message manager. + * + * @param {string} message + * The name of the message to stop listening for. + * @param {nsIMessageListener} listener + * The listener to remove. + */ + removeMessageListener(message, listener) { + this.messageManager.removeMessageListener(message, listener); + + let listeners = this.listeners.get(message); + listeners.delete(listener); + if (!listeners.size) { + this.listeners.delete(message); + } + } + + /** + * @private + * Iterates over all of the currently registered message listeners. + */ + * iterListeners() { + for (let [message, listeners] of this.listeners) { + for (let [listener, listenWhenClosed] of listeners) { + yield {message, listener, listenWhenClosed}; + } + } + } + + /** + * @private + * Adds docShell swap listeners to the message manager owner. + * + * @param {Element} target + * The target element. + */ + addListeners(target) { + target.addEventListener("SwapDocShells", this); + + this.eventTarget = target; + this.messageManager = target.messageManager; + + for (let {message, listener, listenWhenClosed} of this.iterListeners()) { + this.messageManager.addMessageListener(message, listener, listenWhenClosed); + } + } + + /** + * @private + * Removes docShell swap listeners to the message manager owner. + * + * @param {Element} target + * The target element. + */ + removeListeners(target) { + target.removeEventListener("SwapDocShells", this); + + for (let {message, listener} of this.iterListeners()) { + this.messageManager.removeMessageListener(message, listener); + } + } + + handleEvent(event) { + if (event.type == "SwapDocShells") { + this.removeListeners(this.eventTarget); + this.addListeners(event.detail); + } + } +} diff --git a/toolkit/components/extensions/ProxyScriptContext.jsm b/toolkit/components/extensions/ProxyScriptContext.jsm index 8a483c148856..748870528251 100644 --- a/toolkit/components/extensions/ProxyScriptContext.jsm +++ b/toolkit/components/extensions/ProxyScriptContext.jsm @@ -38,7 +38,6 @@ const PROXY_TIMEOUT_SEC = 10; const { ExtensionError, - defineLazyGetter, } = ExtensionUtils; const { @@ -46,6 +45,7 @@ const { CanOfAPIs, LocalAPIImplementation, SchemaAPIManager, + defineLazyGetter, } = ExtensionCommon; const PROXY_TYPES = Object.freeze({ diff --git a/toolkit/components/extensions/extension-process-script.js b/toolkit/components/extensions/extension-process-script.js index 8fc3309f9873..53d1e44e0eeb 100644 --- a/toolkit/components/extensions/extension-process-script.js +++ b/toolkit/components/extensions/extension-process-script.js @@ -16,13 +16,14 @@ ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetters(this, { ExtensionChild: "resource://gre/modules/ExtensionChild.jsm", + ExtensionCommon: "resource://gre/modules/ExtensionCommon.jsm", ExtensionContent: "resource://gre/modules/ExtensionContent.jsm", ExtensionPageChild: "resource://gre/modules/ExtensionPageChild.jsm", }); ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm"); -XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionUtils.getConsole()); +XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionCommon.getConsole()); const { DefaultWeakMap, diff --git a/toolkit/components/extensions/moz.build b/toolkit/components/extensions/moz.build index 692821ab0e73..ccdfb4c206b7 100644 --- a/toolkit/components/extensions/moz.build +++ b/toolkit/components/extensions/moz.build @@ -26,6 +26,7 @@ EXTRA_JS_MODULES += [ 'FindContent.jsm', 'LegacyExtensionsUtils.jsm', 'MessageChannel.jsm', + 'MessageManagerProxy.jsm', 'NativeManifests.jsm', 'NativeMessaging.jsm', 'ProxyScriptContext.jsm', diff --git a/toolkit/components/extensions/parent/ext-downloads.js b/toolkit/components/extensions/parent/ext-downloads.js index 650c9effdc00..5d800eac5799 100644 --- a/toolkit/components/extensions/parent/ext-downloads.js +++ b/toolkit/components/extensions/parent/ext-downloads.js @@ -13,10 +13,6 @@ ChromeUtils.defineModuleGetter(this, "FileUtils", var { EventEmitter, - normalizeTime, -} = ExtensionUtils; - -var { ignoreEvent, } = ExtensionCommon; @@ -262,7 +258,7 @@ const downloadQuery = query => { if (arg == null) { return before ? Number.MAX_VALUE : 0; } - return normalizeTime(arg).getTime(); + return ExtensionCommon.normalizeTime(arg).getTime(); } const startedBefore = normalizeDownloadTime(query.startedBefore, true); diff --git a/toolkit/components/extensions/parent/ext-idle.js b/toolkit/components/extensions/parent/ext-idle.js index 5dcb2aaee507..b28f0a02fb9b 100644 --- a/toolkit/components/extensions/parent/ext-idle.js +++ b/toolkit/components/extensions/parent/ext-idle.js @@ -32,7 +32,7 @@ const getIdleObserver = (extension, context) => { let observerInfo = getIdleObserverInfo(extension, context); let {observer, detectionInterval} = observerInfo; if (!observer) { - observer = new class extends ExtensionUtils.EventEmitter { + observer = new class extends ExtensionCommon.EventEmitter { observe(subject, topic, data) { if (topic == "idle" || topic == "active") { this.emit("stateChanged", topic); diff --git a/toolkit/components/extensions/parent/ext-management.js b/toolkit/components/extensions/parent/ext-management.js index 620ab036956d..420240b6baf6 100644 --- a/toolkit/components/extensions/parent/ext-management.js +++ b/toolkit/components/extensions/parent/ext-management.js @@ -95,7 +95,7 @@ function checkAllowedAddon(addon) { return allowedTypes.includes(addon.type); } -class AddonListener extends ExtensionUtils.EventEmitter { +class AddonListener extends ExtensionCommon.EventEmitter { constructor() { super(); AddonManager.addAddonListener(this); diff --git a/toolkit/components/extensions/parent/ext-tabs-base.js b/toolkit/components/extensions/parent/ext-tabs-base.js index 6891b8299be5..e632a92b29c5 100644 --- a/toolkit/components/extensions/parent/ext-tabs-base.js +++ b/toolkit/components/extensions/parent/ext-tabs-base.js @@ -18,10 +18,13 @@ var { DefaultMap, DefaultWeakMap, ExtensionError, - defineLazyGetter, getWinUtils, } = ExtensionUtils; +var { + defineLazyGetter, +} = ExtensionCommon; + /** * The platform-specific type of native tab objects, which are wrapped by * TabBase instances. diff --git a/toolkit/components/extensions/parent/ext-toolkit.js b/toolkit/components/extensions/parent/ext-toolkit.js index 9b1494f5ad8c..5a4c943f9c40 100644 --- a/toolkit/components/extensions/parent/ext-toolkit.js +++ b/toolkit/components/extensions/parent/ext-toolkit.js @@ -19,7 +19,7 @@ XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]); ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm"); -global.EventEmitter = ExtensionUtils.EventEmitter; +global.EventEmitter = ExtensionCommon.EventEmitter; global.EventManager = ExtensionCommon.EventManager; /* globals DEFAULT_STORE, PRIVATE_STORE, CONTAINER_STORE */