--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -73,35 +73,28 @@ XPCOMUtils.defineLazyGetter(this, "stand
stylesheets.push("chrome://browser/content/extension-mac-panel.css");
}
if (AppConstants.platform === "win") {
stylesheets.push("chrome://browser/content/extension-win-panel.css");
}
return stylesheets;
});
-/* eslint-disable mozilla/balanced-listeners */
-extensions.on("page-shutdown", (type, context) => {
- if (context.viewType == "popup" && context.active) {
- // TODO(robwu): This is not webext-oop compatible.
- context.xulBrowser.contentWindow.close();
- }
-});
-/* eslint-enable mozilla/balanced-listeners */
-
class BasePopup {
constructor(extension, viewNode, popupURL, browserStyle, fixedWidth = false) {
this.extension = extension;
this.popupURL = popupURL;
this.viewNode = viewNode;
this.browserStyle = browserStyle;
this.window = viewNode.ownerGlobal;
this.destroyed = false;
this.fixedWidth = fixedWidth;
+ extension.callOnClose(this);
+
this.contentReady = new Promise(resolve => {
this._resolveContentReady = resolve;
});
this.viewNode.addEventListener(this.DESTROY_EVENT, this);
let doc = viewNode.ownerDocument;
let arrowContent = doc.getAnonymousElementByAttribute(this.panel, "class", "panel-arrowcontent");
@@ -115,17 +108,23 @@ class BasePopup {
BasePopup.instances.get(this.window).set(extension, this);
}
static for(extension, window) {
return BasePopup.instances.get(window).get(extension);
}
+ close() {
+ this.closePopup();
+ }
+
destroy() {
+ this.extension.forgetOnClose(this);
+
this.destroyed = true;
this.browserLoadedDeferred.reject(new Error("Popup destroyed"));
return this.browserReady.then(() => {
this.destroyBrowser(this.browser);
this.browser.remove();
this.viewNode.removeEventListener(this.DESTROY_EVENT, this);
this.viewNode.style.maxHeight = "";
--- a/browser/components/extensions/test/browser/browser_ext_popup_shutdown.js
+++ b/browser/components/extensions/test/browser/browser_ext_popup_shutdown.js
@@ -69,12 +69,10 @@ add_task(function* testPageAction() {
yield extension.awaitMessage("pageAction ready");
clickPageAction(extension);
let browser = yield awaitExtensionPanel(extension);
let panel = getPanelForNode(browser);
yield extension.unload();
- yield new Promise(resolve => setTimeout(resolve, 0));
-
is(panel.parentNode, null, "Panel should be removed from the document");
});
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -78,16 +78,17 @@ let schemaURLs = new Set();
if (!AppConstants.RELEASE_OR_BETA) {
schemaURLs.add("chrome://extensions/content/schemas/experiments.json");
}
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
BaseContext,
+ defineLazyGetter,
EventEmitter,
SchemaAPIManager,
LocaleData,
instanceOf,
LocalAPIImplementation,
flushJarCache,
} = ExtensionUtils;
@@ -284,32 +285,26 @@ class ProxyContext extends BaseContext {
// This message manager is used by ParentAPIManager to send messages and to
// close the ProxyContext if the underlying message manager closes. This
// message manager object may change when `xulBrowser` swaps docshells, e.g.
// when a tab is moved to a different window.
this.currentMessageManager = xulBrowser.messageManager;
this._docShellTracker = new BrowserDocshellFollower(xulBrowser,
this.onBrowserChange.bind(this));
- this.principal_ = principal;
- this.apiObj = {};
- GlobalManager.injectInObject(this, false, this.apiObj);
+ Object.defineProperty(this, "principal", {
+ value: principal, enumerable: true, configurable: true,
+ });
this.listenerProxies = new Map();
- this.sandbox = Cu.Sandbox(principal, {});
-
Management.emit("proxy-context-load", this);
}
- get principal() {
- return this.principal_;
- }
-
get cloneScope() {
return this.sandbox;
}
onBrowserChange(browser) {
// Make sure that the message manager is set. Otherwise the ProxyContext may
// never be destroyed because the ParentAPIManager would fail to detect that
// the message manager is closed.
@@ -329,16 +324,26 @@ class ProxyContext extends BaseContext {
return;
}
this._docShellTracker.destroy();
super.unload();
Management.emit("proxy-context-unload", this);
}
}
+defineLazyGetter(ProxyContext.prototype, "apiObj", function() {
+ let obj = {};
+ GlobalManager.injectInObject(this, false, obj);
+ return obj;
+});
+
+defineLazyGetter(ProxyContext.prototype, "sandbox", function() {
+ return Cu.Sandbox(this.principal);
+});
+
// The parent ProxyContext of an ExtensionContext in ExtensionChild.jsm.
class ExtensionChildProxyContext extends ProxyContext {
constructor(envType, extension, params, xulBrowser) {
super(envType, extension, params, xulBrowser, extension.principal);
this.viewType = params.viewType;
// WARNING: The xulBrowser may change when docShells are swapped, e.g. when
// the tab moves to a different window.
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -29,16 +29,17 @@ XPCOMUtils.defineLazyModuleGetter(this,
const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon";
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
getInnerWindowID,
BaseContext,
ChildAPIManager,
+ defineLazyGetter,
LocalAPIImplementation,
Messenger,
SchemaAPIManager,
} = ExtensionUtils;
// There is a circular dependency between Extension.jsm and us.
// Long-term this file should not reference Extension.jsm (because they would
// live in different processes), but for now use lazy getters.
@@ -164,43 +165,32 @@ class ExtensionContext extends BaseConte
let sender = {id: extension.uuid};
if (viewType == "tab") {
sender.tabId = tabId;
this.tabId = tabId;
}
if (uri) {
sender.url = uri.spec;
}
+ this.sender = sender;
- let filter = {extensionId: extension.id};
- let optionalFilter = {};
- // Addon-generated messages (not necessarily from the same process as the
- // addon itself) are sent to the main process, which forwards them via the
- // parent process message manager. Specific replies can be sent to the frame
- // message manager.
- this.messenger = new Messenger(this, [Services.cpmm, this.messageManager], sender, filter, optionalFilter);
+ Schemas.exportLazyGetter(contentWindow, "browser", () => {
+ let browserObj = Cu.createObjectIn(contentWindow);
+ Schemas.inject(browserObj, this.childManager);
+ return browserObj;
+ });
- let localApis = {};
- apiManager.generateAPIs(this, localApis);
- this.childManager = new WannabeChildAPIManager(this, this.messageManager, localApis, {
- envType: "addon_parent",
- viewType,
- url: uri.spec,
+ Schemas.exportLazyGetter(contentWindow, "chrome", () => {
+ let chromeApiWrapper = Object.create(this.childManager);
+ chromeApiWrapper.isChromeCompat = true;
+
+ let chromeObj = Cu.createObjectIn(contentWindow);
+ Schemas.inject(chromeObj, chromeApiWrapper);
+ return chromeObj;
});
- let chromeApiWrapper = Object.create(this.childManager);
- chromeApiWrapper.isChromeCompat = true;
-
- let browserObj = Cu.createObjectIn(contentWindow, {defineAs: "browser"});
- let chromeObj = Cu.createObjectIn(contentWindow, {defineAs: "chrome"});
- Schemas.inject(browserObj, this.childManager);
- Schemas.inject(chromeObj, chromeApiWrapper);
-
- if (viewType == "background") {
- apiManager.global.initializeBackgroundPage(contentWindow);
- }
this.extension.views.add(this);
}
get cloneScope() {
return this.contentWindow;
}
@@ -225,22 +215,55 @@ class ExtensionContext extends BaseConte
unload() {
// Note that without this guard, we end up running unload code
// multiple times for tab pages closed by the "page-unload" handlers
// triggered below.
if (this.unloaded) {
return;
}
+ if (this.contentWindow) {
+ this.contentWindow.close();
+ }
+
super.unload();
- this.childManager.close();
this.extension.views.delete(this);
}
}
+defineLazyGetter(ExtensionContext.prototype, "messenger", function() {
+ let filter = {extensionId: this.extension.id};
+ let optionalFilter = {};
+ // Addon-generated messages (not necessarily from the same process as the
+ // addon itself) are sent to the main process, which forwards them via the
+ // parent process message manager. Specific replies can be sent to the frame
+ // message manager.
+ return new Messenger(this, [Services.cpmm, this.messageManager], this.sender,
+ filter, optionalFilter);
+});
+
+defineLazyGetter(ExtensionContext.prototype, "childManager", function() {
+ let localApis = {};
+ apiManager.generateAPIs(this, localApis);
+
+ if (this.viewType == "background") {
+ apiManager.global.initializeBackgroundPage(this.contentWindow);
+ }
+
+ let childManager = new WannabeChildAPIManager(this, this.messageManager, localApis, {
+ envType: "addon_parent",
+ viewType: this.viewType,
+ url: this.uri.spec,
+ });
+
+ this.callOnClose(childManager);
+
+ return childManager;
+});
+
// All subframes in a tab, background page, popup, etc. have the same view type.
// This class keeps track of such global state.
// Note that this is created even for non-extension tabs because at present we
// do not have a way to distinguish regular tabs from extension tabs at the
// initialization of a frame script.
class ContentGlobal {
/**
* @param {nsIContentFrameMessageManager} global The frame script's global.
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -44,16 +44,17 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
"resource://gre/modules/WebNavigationFrames.jsm");
Cu.import("resource://gre/modules/ExtensionChild.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
runSafeSyncWithoutClone,
+ defineLazyGetter,
BaseContext,
LocaleData,
Messenger,
flushJarCache,
getInnerWindowID,
promiseDocumentReady,
ChildAPIManager,
SchemaAPIManager,
@@ -264,32 +265,32 @@ class ExtensionContext extends BaseConte
this.setContentWindow(contentWindow);
let frameId = WebNavigationFrames.getFrameId(contentWindow);
this.frameId = frameId;
this.scripts = [];
- let prin;
let contentPrincipal = contentWindow.document.nodePrincipal;
let ssm = Services.scriptSecurityManager;
// copy origin attributes from the content window origin attributes to
// preserve the user context id. overwrite the addonId.
let attrs = contentPrincipal.originAttributes;
attrs.addonId = this.extension.id;
let extensionPrincipal = ssm.createCodebasePrincipal(this.extension.baseURI, attrs);
+ let principal;
if (ssm.isSystemPrincipal(contentPrincipal)) {
// Make sure we don't hand out the system principal by accident.
// also make sure that the null principal has the right origin attributes
- prin = ssm.createNullPrincipal(attrs);
+ principal = ssm.createNullPrincipal(attrs);
} else {
- prin = [contentPrincipal, extensionPrincipal];
+ principal = [contentPrincipal, extensionPrincipal];
}
if (isExtensionPage) {
if (ExtensionManagement.getAddonIdForWindow(this.contentWindow) != this.extension.id) {
throw new Error("Invalid target window for this extension context");
}
// This is an iframe with content script API enabled and its principal should be the
// contentWindow itself. (we create a sandbox with the contentWindow as principal and with X-rays disabled
@@ -304,17 +305,17 @@ class ExtensionContext extends BaseConte
// This metadata is required by the Developer Tools, in order for
// the content script to be associated with both the extension and
// the tab holding the content page.
let metadata = {
"inner-window-id": this.innerWindowID,
addonId: attrs.addonId,
};
- this.sandbox = Cu.Sandbox(prin, {
+ this.sandbox = Cu.Sandbox(principal, {
metadata,
sandboxPrototype: contentWindow,
wantXrays: true,
isWebExtensionContentScript: true,
wantExportHelpers: true,
wantGlobalProperties: ["XMLHttpRequest", "fetch"],
originAttributes: attrs,
});
@@ -327,42 +328,34 @@ class ExtensionContext extends BaseConte
}
Object.defineProperty(this, "principal", {
value: Cu.getObjectPrincipal(this.sandbox),
enumerable: true,
configurable: true,
});
- let url = contentWindow.location.href;
- // The |sender| parameter is passed directly to the extension.
- let sender = {id: this.extension.uuid, frameId, url};
- let filter = {extensionId: this.extension.id};
- let optionalFilter = {frameId};
- this.messenger = new Messenger(this, [this.messageManager], sender, filter, optionalFilter);
-
- this.chromeObj = Cu.createObjectIn(this.sandbox, {defineAs: "browser"});
+ this.url = contentWindow.location.href;
- // Sandboxes don't get Xrays for some weird compatibility
- // reason. However, we waive here anyway in case that changes.
- Cu.waiveXrays(this.sandbox).chrome = this.chromeObj;
+ defineLazyGetter(this, "chromeObj", () => {
+ let chromeObj = Cu.createObjectIn(this.sandbox);
- let localApis = {};
- apiManager.generateAPIs(this, localApis);
- this.childManager = new ChildAPIManager(this, this.messageManager, localApis, {
- envType: "content_parent",
- url,
+ Schemas.inject(chromeObj, this.childManager);
+ return chromeObj;
});
- Schemas.inject(this.chromeObj, this.childManager);
+ Schemas.exportLazyGetter(this.sandbox, "browser", () => this.chromeObj);
+ Schemas.exportLazyGetter(this.sandbox, "chrome", () => this.chromeObj);
- // This is an iframe with content script API enabled. (See Bug 1214658 for rationale)
+ // This is an iframe with content script API enabled (bug 1214658)
if (isExtensionPage) {
- Cu.waiveXrays(this.contentWindow).chrome = this.chromeObj;
- Cu.waiveXrays(this.contentWindow).browser = this.chromeObj;
+ Schemas.exportLazyGetter(this.contentWindow,
+ "browser", () => this.chromeObj);
+ Schemas.exportLazyGetter(this.contentWindow,
+ "chrome", () => this.chromeObj);
}
}
get cloneScope() {
return this.sandbox;
}
execute(script, shouldRun) {
@@ -388,18 +381,16 @@ class ExtensionContext extends BaseConte
// Don't bother saving scripts after document_idle.
this.scripts = this.scripts.filter(script => script.requiresCleanup);
}
}
close() {
super.unload();
- this.childManager.close();
-
if (this.contentWindow) {
for (let script of this.scripts) {
if (script.requiresCleanup) {
script.cleanup(this.contentWindow);
}
}
// Overwrite the content script APIs with an empty object if the APIs objects are still
@@ -409,16 +400,39 @@ class ExtensionContext extends BaseConte
Cu.createObjectIn(this.contentWindow, {defineAs: "chrome"});
}
}
Cu.nukeSandbox(this.sandbox);
this.sandbox = null;
}
}
+defineLazyGetter(ExtensionContext.prototype, "messenger", function() {
+ // The |sender| parameter is passed directly to the extension.
+ let sender = {id: this.extension.uuid, frameId: this.frameId, url: this.url};
+ let filter = {extensionId: this.extension.id};
+ let optionalFilter = {frameId: this.frameId};
+
+ return new Messenger(this, [this.messageManager], sender, filter, optionalFilter);
+});
+
+defineLazyGetter(ExtensionContext.prototype, "childManager", function() {
+ let localApis = {};
+ apiManager.generateAPIs(this, localApis);
+
+ let childManager = new ChildAPIManager(this, this.messageManager, localApis, {
+ envType: "content_parent",
+ url: this.url,
+ });
+
+ this.callOnClose(childManager);
+
+ return childManager;
+});
+
// Responsible for creating ExtensionContexts and injecting content
// scripts into them when new documents are created.
DocumentManager = {
extensionCount: 0,
// Map[windowId -> Map[extensionId -> ExtensionContext]]
contentScriptWindows: new Map(),
@@ -662,31 +676,29 @@ DocumentManager = {
this.extensionCount--;
if (this.extensionCount == 0) {
this.uninit();
}
},
trigger(when, window) {
- let state = this.getWindowState(window);
-
- if (state == "document_start") {
+ if (when == "document_start") {
for (let extension of ExtensionManager.extensions.values()) {
for (let script of extension.scripts) {
if (script.matches(window)) {
let context = this.getContentScriptContext(extension, window);
context.addScript(script);
}
}
}
} else {
let contexts = this.contentScriptWindows.get(getInnerWindowID(window)) || new Map();
for (let context of contexts.values()) {
- context.triggerScripts(state);
+ context.triggerScripts(when);
}
}
},
};
// Represents a browser extension in the content process.
function BrowserExtensionContent(data) {
this.id = data.id;
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -199,16 +199,18 @@ class BaseContext {
let {document} = contentWindow;
let docShell = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell);
this.innerWindowID = getInnerWindowID(contentWindow);
this.messageManager = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIContentFrameMessageManager);
+ MessageChannel.setupMessageManagers([this.messageManager]);
+
let onPageShow = event => {
if (!event || event.target === document) {
this.docShell = docShell;
this.contentWindow = contentWindow;
this.active = true;
}
};
let onPageHide = event => {
@@ -1434,18 +1436,16 @@ function getMessageManager(target) {
* `optionalFilter` and `recipient` are applied to filter incoming messages.
*/
function Messenger(context, messageManagers, sender, filter, optionalFilter) {
this.context = context;
this.messageManagers = messageManagers;
this.sender = sender;
this.filter = filter;
this.optionalFilter = optionalFilter;
-
- MessageChannel.setupMessageManagers(messageManagers);
}
Messenger.prototype = {
_sendMessage(messageManager, message, data, recipient) {
let options = {
recipient,
sender: this.sender,
responseType: MessageChannel.RESPONSE_FIRST,
@@ -2165,17 +2165,59 @@ function normalizeTime(date) {
? parseInt(date, 10) : date);
}
const stylesheetMap = new DefaultMap(url => {
let uri = NetUtil.newURI(url);
return styleSheetService.preloadSheet(uri, styleSheetService.AGENT_SHEET);
});
+/**
+ * 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 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);
+ },
+ });
+}
+
this.ExtensionUtils = {
+ defineLazyGetter,
detectLanguage,
extend,
flushJarCache,
getConsole,
getInnerWindowID,
ignoreEvent,
injectAPI,
instanceOf,
--- a/toolkit/components/extensions/test/xpcshell/test_ext_api_permissions.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_api_permissions.js
@@ -9,45 +9,56 @@ function getNextContext() {
Management.off("proxy-context-load", listener);
resolve(context);
});
});
}
add_task(function* test_storage_api_without_permissions() {
let extension = ExtensionTestUtils.loadExtension({
- background() {},
+ background() {
+ // Force API initialization.
+ void browser.storage;
+ },
manifest: {
permissions: [],
},
});
let contextPromise = getNextContext();
yield extension.startup();
let context = yield contextPromise;
+ // Force API initialization.
+ void context.apiObj;
+
ok(!("storage" in context._unwrappedAPIs),
"The storage API should not be initialized");
yield extension.unload();
});
add_task(function* test_storage_api_with_permissions() {
let extension = ExtensionTestUtils.loadExtension({
- background() {},
+ background() {
+ void browser.storage;
+ },
manifest: {
permissions: ["storage"],
},
});
let contextPromise = getNextContext();
yield extension.startup();
let context = yield contextPromise;
+ // Force API initialization.
+ void context.apiObj;
+
equal(typeof context._unwrappedAPIs.storage, "object",
"The storage API should be initialized");
yield extension.unload();
});