--- a/browser/components/extensions/ExtensionControlledPopup.jsm
+++ b/browser/components/extensions/ExtensionControlledPopup.jsm
@@ -16,28 +16,31 @@
* view those pages after a change to the setting in each session until they confirm
* the change by triggering the primary action.
*/
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",
"resource://gre/modules/AddonManager.jsm");
ChromeUtils.defineModuleGetter(this, "BrowserUtils",
"resource://gre/modules/BrowserUtils.jsm");
ChromeUtils.defineModuleGetter(this, "CustomizableUI",
"resource:///modules/CustomizableUI.jsm");
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");
});
const PREF_BRANCH_INSTALLED_ADDON = "extensions.installedDistroAddon.";
XPCOMUtils.defineLazyGetter(this, "distributionAddonsList", function() {
--- a/browser/components/extensions/ExtensionPopups.jsm
+++ b/browser/components/extensions/ExtensionPopups.jsm
@@ -13,24 +13,28 @@ ChromeUtils.defineModuleGetter(this, "Cu
ChromeUtils.defineModuleGetter(this, "E10SUtils",
"resource://gre/modules/E10SUtils.jsm");
ChromeUtils.defineModuleGetter(this, "ExtensionParent",
"resource://gre/modules/ExtensionParent.jsm");
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;
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
function promisePopupShown(popup) {
return new Promise(resolve => {
if (popup.state == "open") {
--- a/browser/components/extensions/child/ext-devtools-panels.js
+++ b/browser/components/extensions/child/ext-devtools-panels.js
@@ -13,17 +13,17 @@ var {
* Represents an addon devtools panel in the child process.
*
* @param {DevtoolsExtensionContext}
* A devtools extension context running in a child process.
* @param {object} panelOptions
* @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();
this.context = context;
this.context.callOnClose(this);
this.id = id;
this._panelContext = null;
@@ -139,17 +139,17 @@ class ChildDevToolsPanel extends Extensi
* Represents an addon devtools inspector sidebar in the child process.
*
* @param {DevtoolsExtensionContext}
* A devtools extension context running in a child process.
* @param {object} sidebarOptions
* @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();
this.context = context;
this.context.callOnClose(this);
this.id = id;
--- a/browser/components/extensions/child/ext-menus.js
+++ b/browser/components/extensions/child/ext-menus.js
@@ -1,15 +1,15 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
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
// can use this API runs in the same process, this local variable suffices.
var gNextMenuItemID = 0;
// Map[Extension -> Map[string or id, ContextMenusClickPropHandler]]
var gPropHandlers = new Map();
--- a/browser/components/extensions/parent/ext-browser.js
+++ b/browser/components/extensions/parent/ext-browser.js
@@ -8,36 +8,39 @@
ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
ChromeUtils.defineModuleGetter(this, "BrowserWindowTracker",
"resource:///modules/BrowserWindowTracker.jsm");
var {
ExtensionError,
+} = ExtensionUtils;
+
+var {
defineLazyGetter,
-} = ExtensionUtils;
+} = ExtensionCommon;
const READER_MODE_PREFIX = "about:reader";
let tabTracker;
let windowTracker;
// This function is pretty tightly tied to Extension.jsm.
// Its job is to fill in the |tab| property of the sender.
const getSender = (extension, target, sender) => {
let tabId;
if ("tabId" in sender) {
// The message came from a privileged extension page running in a tab. In
// that case, it should include a tabId property (which is filled in by the
// 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;
}
if (tabId) {
let tab = extension.tabManager.get(tabId, null);
if (tab) {
sender.tab = tab.convert();
}
--- a/browser/components/extensions/parent/ext-history.js
+++ b/browser/components/extensions/parent/ext-history.js
@@ -4,17 +4,17 @@
ChromeUtils.defineModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
ChromeUtils.defineModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
var {
normalizeTime,
-} = ExtensionUtils;
+} = ExtensionCommon;
let nsINavHistoryService = Ci.nsINavHistoryService;
const TRANSITION_TO_TRANSITION_TYPES_MAP = new Map([
["link", nsINavHistoryService.TRANSITION_LINK],
["typed", nsINavHistoryService.TRANSITION_TYPED],
["auto_bookmark", nsINavHistoryService.TRANSITION_BOOKMARK],
["auto_subframe", nsINavHistoryService.TRANSITION_EMBED],
["manual_subframe", nsINavHistoryService.TRANSITION_FRAMED_LINK],
--- a/browser/components/extensions/test/xpcshell/test_ext_history.js
+++ b/browser/components/extensions/test/xpcshell/test_ext_history.js
@@ -1,18 +1,18 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
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");
PromiseTestUtils.whitelistRejectionsGlobally(/Message manager disconnected/);
add_task(async function test_delete() {
function background() {
let historyClearedCount = 0;
@@ -303,17 +303,17 @@ add_task(async function test_add_url() {
];
async function checkUrl(results) {
ok((await PlacesTestUtils.isPageInDB(results.details.url)), `${results.details.url} found in history database`);
ok(PlacesUtils.isValidGuid(results.result.id), "URL was added with a valid id");
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");
}
}
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["history"],
},
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -37,17 +37,16 @@ ChromeUtils.import("resource://gre/modul
XPCOMUtils.defineLazyModuleGetters(this, {
AddonManager: "resource://gre/modules/AddonManager.jsm",
AddonManagerPrivate: "resource://gre/modules/AddonManager.jsm",
AddonSettings: "resource://gre/modules/addons/AddonSettings.jsm",
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",
FileSource: "resource://gre/modules/L10nRegistry.jsm",
L10nRegistry: "resource://gre/modules/L10nRegistry.jsm",
Log: "resource://gre/modules/Log.jsm",
MessageChannel: "resource://gre/modules/MessageChannel.jsm",
NetUtil: "resource://gre/modules/NetUtil.jsm",
@@ -72,16 +71,17 @@ XPCOMUtils.defineLazyGetter(
return obj;
});
XPCOMUtils.defineLazyGetter(
this, "resourceProtocol",
() => 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");
XPCOMUtils.defineLazyServiceGetters(this, {
aomStartup: ["@mozilla.org/addons/addon-manager-startup;1", "amIAddonManagerStartup"],
spellCheck: ["@mozilla.org/spellchecker/engine;1", "mozISpellCheckingEngine"],
uuidGen: ["@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"],
});
@@ -91,22 +91,25 @@ XPCOMUtils.defineLazyPreferenceGetter(th
var {
GlobalManager,
ParentAPIManager,
StartupCache,
apiManager: Management,
} = 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);
// The userContextID reserved for the extension storage (its purpose is ensuring that the IndexedDB
// storage used by the browser.storage.local API is not directly accessible from the extension code).
XPCOMUtils.defineLazyGetter(this, "WEBEXT_STORAGE_USER_CONTEXT_ID", () => {
return ContextualIdentityService.getDefaultPrivateIdentity(
"userContextIdInternal.webextStorageLocal").userContextId;
@@ -1425,17 +1428,17 @@ class Extension extends ExtensionData {
checkLoadURL(url, options = {}) {
// As an optimization, f the URL starts with the extension's base URL,
// don't do any further checks. It's always allowed to load it.
if (url.startsWith(this.baseURL)) {
return true;
}
- return ExtensionUtils.checkLoadURL(url, this.principal, options);
+ return ExtensionCommon.checkLoadURL(url, this.principal, options);
}
async promiseLocales(locale) {
let locales = await StartupCache.locales
.get([this.id, "@@all_locales"], () => this._promiseLocaleMap());
return this._setupLocaleData(locales);
}
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -37,31 +37,31 @@ XPCOMUtils.defineLazyGetter(
() => Cc["@mozilla.org/webextensions/extension-process-script;1"]
.getService().wrappedJSObject);
ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
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;
// Copy an API object from |source| into the scope |dest|.
function injectAPI(source, dest) {
for (let prop in source) {
// Skip names prefixed with '_'.
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -17,48 +17,168 @@ var EXPORTED_SYMBOLS = ["ExtensionCommon
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
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",
SchemaRoot: "resource://gre/modules/Schemas.jsm",
});
XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
"@mozilla.org/content/style-sheet-service;1",
"nsIStyleSheetService");
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",
"extensions.webextensions.background-delayed-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
* spread expression (...args) when passed to a callback.
*/
class SpreadArgs extends Array {
constructor(args) {
super();
@@ -80,22 +200,129 @@ class NoCloneSpreadArgs {
this.unwrappedValues = args;
}
[Symbol.iterator]() {
return this.unwrappedValues[Symbol.iterator]();
}
}
+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();
this.extension = extension;
extension.once("shutdown", () => {
if (this.onShutdown) {
this.onShutdown(extension.shutdownReason);
@@ -241,17 +468,17 @@ class BaseContext {
checkLoadURL(url, options = {}) {
// As an optimization, f the URL starts with the extension's base URL,
// don't do any further checks. It's always allowed to load it.
if (url.startsWith(this.extension.baseURL)) {
return true;
}
- return ExtensionUtils.checkLoadURL(url, this.principal, options);
+ return checkLoadURL(url, this.principal, options);
}
/**
* Safely call JSON.stringify() on an object that comes from an
* extension.
*
* @param {array<any>} args Arguments for JSON.stringify()
* @returns {string} The stringified representation of obj
@@ -2069,20 +2296,29 @@ const stylesheetMap = new DefaultMap(url
});
ExtensionCommon = {
BaseContext,
CanOfAPIs,
EventManager,
ExtensionAPI,
+ EventEmitter,
LocalAPIImplementation,
LocaleData,
NoCloneSpreadArgs,
SchemaAPIInterface,
SchemaAPIManager,
SpreadArgs,
+ checkLoadURL,
+ defineLazyGetter,
+ getConsole,
ignoreEvent,
+ instanceOf,
+ makeWidgetId,
+ normalizeTime,
+ runSafeSyncWithoutClone,
stylesheetMap,
+ withHandlingUserInput,
MultiAPIManager,
LazyAPIManager,
};
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -34,38 +34,38 @@ ChromeUtils.import("resource://gre/modul
ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
XPCOMUtils.defineLazyGlobalGetters(this, ["crypto", "TextEncoder"]);
const {
DefaultMap,
DefaultWeakMap,
- defineLazyGetter,
getInnerWindowID,
getWinUtils,
promiseDocumentIdle,
promiseDocumentLoaded,
promiseDocumentReady,
- runSafeSyncWithoutClone,
} = ExtensionUtils;
const {
BaseContext,
CanOfAPIs,
SchemaAPIManager,
+ defineLazyGetter,
+ runSafeSyncWithoutClone,
} = ExtensionCommon;
const {
BrowserExtensionContent,
ChildAPIManager,
Messenger,
} = ExtensionChild;
-XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
+XPCOMUtils.defineLazyGetter(this, "console", ExtensionCommon.getConsole);
var DocumentManager;
const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
const CONTENT_SCRIPT_INJECTION_HISTOGRAM = "WEBEXT_CONTENT_SCRIPT_INJECTION_MS";
var apiManager = new class extends SchemaAPIManager {
--- a/toolkit/components/extensions/ExtensionPageChild.jsm
+++ b/toolkit/components/extensions/ExtensionPageChild.jsm
@@ -32,25 +32,25 @@ XPCOMUtils.defineLazyGetter(
const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon";
const CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS = "webextension-scripts-devtools";
ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
ChromeUtils.import("resource://gre/modules/ExtensionChild.jsm");
ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
const {
- defineLazyGetter,
getInnerWindowID,
promiseEvent,
} = ExtensionUtils;
const {
BaseContext,
CanOfAPIs,
SchemaAPIManager,
+ defineLazyGetter,
} = ExtensionCommon;
const {
ChildAPIManager,
Messenger,
} = ExtensionChild;
var ExtensionPageChild;
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -20,42 +20,42 @@ ChromeUtils.import("resource://gre/modul
XPCOMUtils.defineLazyModuleGetters(this, {
AppConstants: "resource://gre/modules/AppConstants.jsm",
AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
DeferredTask: "resource://gre/modules/DeferredTask.jsm",
E10SUtils: "resource://gre/modules/E10SUtils.jsm",
ExtensionData: "resource://gre/modules/Extension.jsm",
MessageChannel: "resource://gre/modules/MessageChannel.jsm",
+ MessageManagerProxy: "resource://gre/modules/MessageManagerProxy.jsm",
+ NativeApp: "resource://gre/modules/NativeMessaging.jsm",
OS: "resource://gre/modules/osfile.jsm",
- NativeApp: "resource://gre/modules/NativeMessaging.jsm",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
Schemas: "resource://gre/modules/Schemas.jsm",
});
XPCOMUtils.defineLazyServiceGetters(this, {
aomStartup: ["@mozilla.org/addons/addon-manager-startup;1", "amIAddonManagerStartup"],
});
ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
var {
BaseContext,
CanOfAPIs,
SchemaAPIManager,
SpreadArgs,
+ defineLazyGetter,
} = ExtensionCommon;
var {
DefaultMap,
DefaultWeakMap,
ExtensionError,
- MessageManagerProxy,
- defineLazyGetter,
promiseDocumentLoaded,
promiseEvent,
promiseObserved,
} = ExtensionUtils;
const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
const CATEGORY_EXTENSION_MODULES = "webextension-modules";
const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
--- a/toolkit/components/extensions/ExtensionStorageSync.jsm
+++ b/toolkit/components/extensions/ExtensionStorageSync.jsm
@@ -43,16 +43,17 @@ ChromeUtils.import("resource://gre/modul
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
BulkKeyBundle: "resource://services-sync/keys.js",
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",
FirefoxAdapter: "resource://services-common/kinto-storage-adapter.js",
Observers: "resource://services-common/observers.js",
Utils: "resource://services-sync/util.js",
});
@@ -63,17 +64,16 @@ XPCOMUtils.defineLazyPreferenceGetter(th
KINTO_DEFAULT_SERVER_URL);
XPCOMUtils.defineLazyGetter(this, "WeaveCrypto", function() {
let {WeaveCrypto} = ChromeUtils.import("resource://services-crypto/WeaveCrypto.js", {});
return new WeaveCrypto();
});
const {
DefaultMap,
- runSafeSyncWithoutClone,
} = ExtensionUtils;
// Map of Extensions to Set<Contexts> to track contexts that are still
// "live" and use storage.sync.
const extensionContexts = new DefaultMap(() => new Set());
// Borrow logger from Sync.
const log = Log.repository.getLogger("Sync.Engine.Extension-Storage");
@@ -1244,15 +1244,15 @@ class ExtensionStorageSync {
}
}
notifyListeners(extension, changes) {
Observers.notify("ext.storage.sync-changed");
let listeners = this.listeners.get(extension) || new Set();
if (listeners) {
for (let listener of listeners) {
- runSafeSyncWithoutClone(listener, changes);
+ ExtensionCommon.runSafeSyncWithoutClone(listener, changes);
}
}
}
}
this.ExtensionStorageSync = ExtensionStorageSync;
extensionStorageSync = new ExtensionStorageSync(_fxaService, Services.telemetry);
--- a/toolkit/components/extensions/ExtensionTestCommon.jsm
+++ b/toolkit/components/extensions/ExtensionTestCommon.jsm
@@ -28,28 +28,32 @@ ChromeUtils.defineModuleGetter(this, "Ex
ChromeUtils.defineModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
ChromeUtils.defineModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyGetter(this, "apiManager",
() => ExtensionParent.apiManager);
+ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator");
const {
flushJarCache,
- instanceOf,
} = ExtensionUtils;
-XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
+const {
+ instanceOf,
+} = ExtensionCommon;
+
+XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionCommon.getConsole());
/**
* A skeleton Extension-like object, used for testing, which installs an
* add-on via the add-on manager when startup() is called, and
* uninstalles it on shutdown().
*
* @param {string} id
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -5,30 +5,19 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
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);
// It would be nicer to go through `Services.appinfo`, but some tests need to be
// able to replace that field with a custom implementation before it is first
// called.
// eslint-disable-next-line mozilla/use-services
@@ -57,33 +46,16 @@ function promiseTimeout(delay) {
* to extensions, rather than being interpreted as an unknown error.
*/
class ExtensionError extends Error {}
function filterStack(error) {
return String(error.stack).replace(/(^.*(Task\.jsm|Promise-backend\.js).*\n)+/gm, "<Promise Chain>\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.
*/
class DefaultWeakMap extends WeakMap {
constructor(defaultConstructor = undefined, init = undefined) {
super(init);
if (defaultConstructor) {
@@ -124,132 +96,16 @@ const _winUtils = new DefaultWeakMap(win
.getInterface(Ci.nsIDOMWindowUtils);
});
const getWinUtils = win => _winUtils.get(win);
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.
*
* @param {integer} limit
* The maximum size to trim the set to after it grows too large.
* @param {integer} [slop = limit * .25]
* The number of extra entries to allow in the set after it
@@ -399,308 +255,26 @@ function getMessageManager(target) {
}
return target;
}
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 <browser>.
- *
- * @param {nsIMessageSender|Element} target
- * The target message manager on which to send messages, or the
- * <browser> 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 <browser> 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 <browser>
- * 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 <browser> 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,
};
--- a/toolkit/components/extensions/MessageChannel.jsm
+++ b/toolkit/components/extensions/MessageChannel.jsm
@@ -100,19 +100,29 @@
var EXPORTED_SYMBOLS = ["MessageChannel"];
/* globals MessageChannel */
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;
// Idle callback timeout for low-priority message dispatch.
const LOW_PRIORITY_TIMEOUT_MS = 250;
const MESSAGE_MESSAGES = "MessageChannel:Messages";
const MESSAGE_RESPONSE = "MessageChannel:Response";
@@ -914,28 +924,30 @@ this.MessageChannel = {
Cu.reportError(e.stack ? `${e}\n${e.stack}` : e.message || e);
});
});
data = null;
// Note: Unhandled messages are silently dropped.
return;
}
- let target = new MessageManagerProxy(data.target);
+ let target = getMessageManager(data.target);
let deferred = {
sender: data.sender,
messageManager: target,
channelId: data.channelId,
respondingSide: true,
};
let cleanup = () => {
this.pendingResponses.delete(deferred);
- target.dispose();
+ if (target.dispose) {
+ target.dispose();
+ }
};
this.pendingResponses.add(deferred);
deferred.promise = new Promise((resolve, reject) => {
deferred.reject = reject;
this._callHandlers(handlers, data).then(resolve, reject);
data = null;
@@ -1073,17 +1085,17 @@ this.MessageChannel = {
* The message manager for which to abort brokers.
* @param {object} reason
* An object describing the reason the responses were aborted.
* Will be passed to the promise rejection handler of all aborted
* responses.
*/
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);
}
}
},
observe(subject, topic, data) {
switch (topic) {
new file mode 100644
--- /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 <browser>.
+ *
+ * @param {nsIMessageSender|Element} target
+ * The target message manager on which to send messages, or the
+ * <browser> 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 <browser> 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 <browser>
+ * 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 <browser> 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);
+ }
+ }
+}
--- a/toolkit/components/extensions/ProxyScriptContext.jsm
+++ b/toolkit/components/extensions/ProxyScriptContext.jsm
@@ -33,24 +33,24 @@ const CATEGORY_EXTENSION_SCRIPTS_CONTENT
// DNS is resolved on the SOCKS proxy server.
const {TRANSPARENT_PROXY_RESOLVES_HOST} = Ci.nsIProxyInfo;
// The length of time (seconds) to wait for a proxy to resolve before ignoring it.
const PROXY_TIMEOUT_SEC = 10;
const {
ExtensionError,
- defineLazyGetter,
} = ExtensionUtils;
const {
BaseContext,
CanOfAPIs,
LocalAPIImplementation,
SchemaAPIManager,
+ defineLazyGetter,
} = ExtensionCommon;
const PROXY_TYPES = Object.freeze({
DIRECT: "direct",
HTTPS: "https",
PROXY: "http", // Synonym for PROXY_TYPES.HTTP
HTTP: "http",
SOCKS: "socks", // SOCKS5
--- a/toolkit/components/extensions/extension-process-script.js
+++ b/toolkit/components/extensions/extension-process-script.js
@@ -11,23 +11,24 @@
*/
ChromeUtils.import("resource://gre/modules/MessageChannel.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
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,
getInnerWindowID,
} = ExtensionUtils;
// We need to avoid touching Services.appinfo here in order to prevent
// the wrong version from being cached during xpcshell test startup.
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -21,16 +21,17 @@ EXTRA_JS_MODULES += [
'ExtensionSettingsStore.jsm',
'ExtensionStorage.jsm',
'ExtensionStorageIDB.jsm',
'ExtensionStorageSync.jsm',
'ExtensionUtils.jsm',
'FindContent.jsm',
'LegacyExtensionsUtils.jsm',
'MessageChannel.jsm',
+ 'MessageManagerProxy.jsm',
'NativeManifests.jsm',
'NativeMessaging.jsm',
'ProxyScriptContext.jsm',
'Schemas.jsm',
]
EXTRA_COMPONENTS += [
'extension-process-script.js',
--- a/toolkit/components/extensions/parent/ext-downloads.js
+++ b/toolkit/components/extensions/parent/ext-downloads.js
@@ -8,20 +8,16 @@ ChromeUtils.defineModuleGetter(this, "Do
"resource://gre/modules/DownloadPaths.jsm");
ChromeUtils.defineModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
ChromeUtils.defineModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
var {
EventEmitter,
- normalizeTime,
-} = ExtensionUtils;
-
-var {
ignoreEvent,
} = ExtensionCommon;
const DOWNLOAD_ITEM_FIELDS = ["id", "url", "referrer", "filename", "incognito",
"danger", "mime", "startTime", "endTime",
"estimatedEndTime", "state",
"paused", "canResume", "error",
"bytesReceived", "totalBytes",
@@ -257,17 +253,17 @@ const downloadQuery = query => {
}
}
}
function normalizeDownloadTime(arg, before) {
if (arg == null) {
return before ? Number.MAX_VALUE : 0;
}
- return normalizeTime(arg).getTime();
+ return ExtensionCommon.normalizeTime(arg).getTime();
}
const startedBefore = normalizeDownloadTime(query.startedBefore, true);
const startedAfter = normalizeDownloadTime(query.startedAfter, false);
// const endedBefore = normalizeDownloadTime(query.endedBefore, true);
// const endedAfter = normalizeDownloadTime(query.endedAfter, false);
const totalBytesGreater = query.totalBytesGreater || 0;
--- a/toolkit/components/extensions/parent/ext-idle.js
+++ b/toolkit/components/extensions/parent/ext-idle.js
@@ -27,17 +27,17 @@ const getIdleObserverInfo = (extension,
}
return observerInfo;
};
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);
}
}
}();
idleService.addIdleObserver(observer, detectionInterval);
observerInfo.observer = observer;
--- a/toolkit/components/extensions/parent/ext-management.js
+++ b/toolkit/components/extensions/parent/ext-management.js
@@ -90,17 +90,17 @@ function checkAllowedAddon(addon) {
return false;
}
if (addon.type == "extension" && !addon.isWebExtension) {
return false;
}
return allowedTypes.includes(addon.type);
}
-class AddonListener extends ExtensionUtils.EventEmitter {
+class AddonListener extends ExtensionCommon.EventEmitter {
constructor() {
super();
AddonManager.addAddonListener(this);
}
release() {
AddonManager.removeAddonListener(this);
}
--- a/toolkit/components/extensions/parent/ext-tabs-base.js
+++ b/toolkit/components/extensions/parent/ext-tabs-base.js
@@ -13,20 +13,23 @@ ChromeUtils.defineModuleGetter(this, "Pr
"resource://gre/modules/PrivateBrowsingUtils.jsm");
ChromeUtils.defineModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
var {
DefaultMap,
DefaultWeakMap,
ExtensionError,
- defineLazyGetter,
getWinUtils,
} = ExtensionUtils;
+var {
+ defineLazyGetter,
+} = ExtensionCommon;
+
/**
* The platform-specific type of native tab objects, which are wrapped by
* TabBase instances.
*
* @typedef {Object|XULElement} NativeTab
*/
/**
--- a/toolkit/components/extensions/parent/ext-toolkit.js
+++ b/toolkit/components/extensions/parent/ext-toolkit.js
@@ -14,17 +14,17 @@
ChromeUtils.defineModuleGetter(this, "ContextualIdentityService",
"resource://gre/modules/ContextualIdentityService.jsm");
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 */
global.DEFAULT_STORE = "firefox-default";
global.PRIVATE_STORE = "firefox-private";
global.CONTAINER_STORE = "firefox-container-";