Bug 1287007 - Move extension context initialization to ExtensionContent
This is a simple move of ExtensionContext creation logic to
ExtensionChild.
Before the change, ExtensionContext was initialized as follows:
1. (ext-backgroundPage.js) Create background page
2. (Extension.jsm) document-element-inserted observed.
3. (Extension.jsm) new ExtensionContext + unload observer.
After this commit:
1. (ext-backgroundPage.js) Create background page
2. (ext-backgroundPage.js) emit extension-browser-inserted event
3. (Extension.jsm) Pass global to ExtensionContent + unload listener.
4. (ExtensionContent.jsm) document-element-inserted observed.
5. (ExtensionChild.jsm) new ExtensionContext
The next step is to use frame scripts and synchronize state.
MozReview-Commit-ID: K6mPdq7KQ2T
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -235,16 +235,18 @@ class BasePopup {
// Note: When using noautohide panels, the popup manager will add width and
// height attributes to the panel, breaking our resize code, if the browser
// starts out smaller than 30px by 10px. This isn't an issue now, but it
// will be if and when we popup debugging.
viewNode.appendChild(this.browser);
+ extensions.emit("extension-browser-inserted", this.browser);
+
let initBrowser = browser => {
let mm = browser.messageManager;
mm.addMessageListener("DOMTitleChanged", this);
mm.addMessageListener("Extension:BrowserBackgroundChanged", this);
mm.addMessageListener("Extension:BrowserContentLoaded", this);
mm.addMessageListener("Extension:BrowserResized", this);
mm.addMessageListener("Extension:DOMWindowClose", this, true);
};
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -27,18 +27,16 @@ Cu.import("resource://gre/modules/XPCOMU
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionAPIs",
"resource://gre/modules/ExtensionAPI.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionContext",
- "resource://gre/modules/ExtensionChild.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
"resource://gre/modules/ExtensionStorage.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Locale",
"resource://gre/modules/Locale.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Log",
"resource://gre/modules/Log.jsm");
@@ -599,40 +597,47 @@ var UninstallObserver = {
GlobalManager = {
// Map[extension ID -> Extension]. Determines which extension is
// responsible for content under a particular extension ID.
extensionMap: new Map(),
initialized: false,
init(extension) {
if (this.extensionMap.size == 0) {
- Services.obs.addObserver(this, "document-element-inserted", false);
UninstallObserver.init();
ProxyMessenger.init();
- // This initializes the default message handler for messages targeted at
- // an addon process, in case the addon process receives a message before
- // its Messenger has been instantiated. For example, if a content script
- // sends a message while there is no background page.
- // TODO(robwu): Move this to the addon process once we have one.
- MessageChannel.setupMessageManagers([Services.cpmm]);
+ Management.on("extension-browser-inserted", this._onExtensionBrowser);
this.initialized = true;
}
this.extensionMap.set(extension.id, extension);
},
uninit(extension) {
this.extensionMap.delete(extension.id);
if (this.extensionMap.size == 0 && this.initialized) {
- Services.obs.removeObserver(this, "document-element-inserted");
+ Management.off("extension-browser-inserted", this._onExtensionBrowser);
this.initialized = false;
}
},
+ _onExtensionBrowser(type, browser) {
+ // TODO(robwu): Move this logic inside a frame script.
+ let global = browser.docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+ ExtensionContent.init(global);
+ /* eslint-disable mozilla/balanced-listeners */
+ global.addEventListener("unload", function() {
+ ExtensionContent.uninit(this);
+ });
+ /* eslint-enable mozilla/balanced-listeners */
+ },
+
getExtension(extensionId) {
return this.extensionMap.get(extensionId);
},
injectInObject(context, isChromeCompat, dest) {
let apis = {
extensionTypes: {},
};
@@ -671,77 +676,16 @@ GlobalManager = {
getImplementation(namespace, name) {
let pathObj = findPathInObject(apis, namespace);
return new LocalAPIImplementation(pathObj, name, context);
},
};
Schemas.inject(dest, schemaWrapper);
},
-
- observe(document, topic, data) {
- let contentWindow = document.defaultView;
- if (!contentWindow) {
- return;
- }
-
- let id = ExtensionManagement.getAddonIdForWindow(contentWindow);
-
- // We don't inject privileged APIs into sub-frames of a UI page.
- const {FULL_PRIVILEGES} = ExtensionManagement.API_LEVELS;
- if (ExtensionManagement.getAPILevelForWindow(contentWindow, id) !== FULL_PRIVILEGES) {
- return;
- }
-
- // We don't inject privileged APIs if the addonId is null
- // or doesn't exist.
- if (!this.extensionMap.has(id)) {
- return;
- }
-
- let docShell = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDocShell);
-
- let parentDocument = docShell.parent.QueryInterface(Ci.nsIDocShell)
- .contentViewer.DOMDocument;
-
- let browser = docShell.chromeEventHandler;
- // If this is a sub-frame of the add-on manager, use that <browser>
- // element rather than the top-level chrome event handler.
- if (contentWindow.frameElement && parentDocument.documentURI == "about:addons") {
- browser = contentWindow.frameElement;
- }
-
- let viewType = "tab";
- if (browser.hasAttribute("webextension-view-type")) {
- viewType = browser.getAttribute("webextension-view-type");
- } else if (browser.classList.contains("inline-options-browser")) {
- // Options pages are currently displayed inline, but in Chrome
- // and in our UI mock-ups for a later milestone, they're
- // pop-ups.
- viewType = "popup";
- }
-
- let extension = this.extensionMap.get(id);
- let uri = document.documentURIObject;
-
- let context = new ExtensionContext(extension, {viewType, contentWindow, uri, docShell});
-
- let innerWindowID = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
-
- let onUnload = subject => {
- let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
- if (windowId == innerWindowID) {
- Services.obs.removeObserver(onUnload, "inner-window-destroyed");
- context.unload();
- }
- };
- Services.obs.addObserver(onUnload, "inner-window-destroyed", false);
- },
};
// Represents the data contained in an extension, contained either
// in a directory or a zip file, which may or may not be installed.
// This class implements the functionality of the Extension class,
// primarily related to manifest parsing and localization, which is
// useful prior to extension installation or initialization.
//
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -1,15 +1,15 @@
/* 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";
-this.EXPORTED_SYMBOLS = ["ExtensionContext"];
+this.EXPORTED_SYMBOLS = ["ExtensionChild"];
/*
* This file handles addon logic that is independent of the chrome process.
* When addons run out-of-process, this is the main entry point.
* Its primary function is managing addon globals.
*
* Don't put contentscript logic here, use ExtensionContent.jsm instead.
*/
@@ -17,35 +17,40 @@ this.EXPORTED_SYMBOLS = ["ExtensionConte
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
+ "resource://gre/modules/MessageChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon";
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
+ getInnerWindowID,
BaseContext,
ChildAPIManager,
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.
XPCOMUtils.defineLazyGetter(this, "findPathInObject",
() => Cu.import("resource://gre/modules/Extension.jsm", {}).findPathInObject);
+XPCOMUtils.defineLazyGetter(this, "GlobalManager",
+ () => Cu.import("resource://gre/modules/Extension.jsm", {}).GlobalManager);
XPCOMUtils.defineLazyGetter(this, "Management",
() => Cu.import("resource://gre/modules/Extension.jsm", {}).Management);
XPCOMUtils.defineLazyGetter(this, "ParentAPIManager",
() => Cu.import("resource://gre/modules/Extension.jsm", {}).ParentAPIManager);
var apiManager = new class extends SchemaAPIManager {
constructor() {
super("addon");
@@ -127,17 +132,17 @@ class WannabeChildAPIManager extends Chi
// (viewType="background"), popups (viewType="popup"), and any extension
// content loaded into browser tabs (viewType="tab").
//
// |params| is an object with the following properties:
// |viewType| is one of "background", "popup", or "tab".
// |contentWindow| is the DOM window the content runs in.
// |uri| is the URI of the content (optional).
// |docShell| is the docshell the content runs in (optional).
-this.ExtensionContext = class extends BaseContext {
+class ExtensionContext extends BaseContext {
constructor(extension, params) {
super("addon_child", extension);
if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) {
// This check is temporary. It should be removed once the proxy creation
// is asynchronous.
throw new Error("ExtensionContext cannot be created in child processes");
}
@@ -217,11 +222,107 @@ this.ExtensionContext = class extends Ba
super.unload();
this.childManager.close();
if (this.externallyVisible) {
this.extension.views.delete(this);
}
}
+}
+
+this.ExtensionChild = {
+ // Map<innerWindowId, ExtensionContext>
+ extensionContexts: new Map(),
+
+ initOnce() {
+ // This initializes the default message handler for messages targeted at
+ // an addon process, in case the addon process receives a message before
+ // its Messenger has been instantiated. For example, if a content script
+ // sends a message while there is no background page.
+ MessageChannel.setupMessageManagers([Services.cpmm]);
+ },
+
+ /**
+ * Create a privileged context at document-element-inserted.
+ *
+ * @param {Extension|BrowserExtensionContent} extension
+ * The extension for which the context should be created.
+ * @param {nsIDOMWindow} contentWindow The global of the page.
+ */
+ createExtensionContext(extension, contentWindow) {
+ // TODO(robwu): Remove dependencies on the bloated Extension from
+ // Extension.jsm and use the thin BrowserExtensionContent from
+ // ExtensionContent.jsm instead.
+ extension = GlobalManager.extensionMap.get(extension.id);
+ let windowId = getInnerWindowID(contentWindow);
+ let context = this.extensionContexts.get(windowId);
+ if (context) {
+ if (context.extension !== extension) {
+ // Oops. This should never happen.
+ Cu.reportError("A different extension context already exists in this frame!");
+ } else {
+ // This should not happen either.
+ Cu.reportError("The extension context was already initialized in this frame.");
+ }
+ return;
+ }
+
+ let docShell = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell);
+
+ let parentDocument = docShell.parent.QueryInterface(Ci.nsIDocShell)
+ .contentViewer.DOMDocument;
+
+ let browser = docShell.chromeEventHandler;
+ // If this is a sub-frame of the add-on manager, use that <browser>
+ // element rather than the top-level chrome event handler.
+ if (contentWindow.frameElement && parentDocument.documentURI == "about:addons") {
+ browser = contentWindow.frameElement;
+ }
+
+ let viewType = "tab";
+ if (browser.hasAttribute("webextension-view-type")) {
+ viewType = browser.getAttribute("webextension-view-type");
+ } else if (browser.classList.contains("inline-options-browser")) {
+ // Options pages are currently displayed inline, but in Chrome
+ // and in our UI mock-ups for a later milestone, they're
+ // pop-ups.
+ viewType = "popup";
+ }
+
+ let uri = contentWindow.document.documentURIObject;
+
+ context = new ExtensionContext(extension, {viewType, contentWindow, uri, docShell});
+ this.extensionContexts.set(windowId, context);
+ },
+
+ /**
+ * Close the ExtensionContext belonging to the given window, if any.
+ *
+ * @param {number} windowId The inner window ID of the destroyed context.
+ */
+ destroyExtensionContext(windowId) {
+ let context = this.extensionContexts.get(windowId);
+ if (context) {
+ context.unload();
+ this.extensionContexts.delete(windowId);
+ }
+ },
+
+ shutdownExtension(extensionId) {
+ for (let [windowId, context] of this.extensionContexts) {
+ if (context.extension.id == extensionId) {
+ context.shutdown();
+ this.extensionContexts.delete(windowId);
+ }
+ }
+ },
};
-
+// TODO(robwu): Change this condition when addons move to a separate process.
+if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ Object.keys(ExtensionChild).forEach(function(key) {
+ if (typeof ExtensionChild[key] == "function") {
+ ExtensionChild[key] = () => {};
+ }
+ });
+}
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -7,16 +7,20 @@
this.EXPORTED_SYMBOLS = ["ExtensionContent"];
/* globals ExtensionContent */
/*
* This file handles the content process side of extensions. It mainly
* takes care of content script injection, content script APIs, and
* messaging.
+ *
+ * This file is also the initial entry point for addon processes.
+ * ExtensionChild.jsm is responsible for functionality specific to addon
+ * processes.
*/
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@@ -35,16 +39,18 @@ XPCOMUtils.defineLazyModuleGetter(this,
"resource://gre/modules/MessageChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
"resource://gre/modules/PromiseUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
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,
BaseContext,
LocaleData,
Messenger,
flushJarCache,
getInnerWindowID,
@@ -452,23 +458,32 @@ DocumentManager = {
// was called on (i.e., not frames for social or sidebars).
let mm = getWindowMessageManager(window);
if (!mm || !ExtensionContent.globals.has(mm)) {
return;
}
// Enable the content script APIs should be available in subframes' window
// if it is recognized as a valid addon id (see Bug 1214658 for rationale).
- const {CONTENTSCRIPT_PRIVILEGES} = ExtensionManagement.API_LEVELS;
+ const {
+ NO_PRIVILEGES,
+ CONTENTSCRIPT_PRIVILEGES,
+ FULL_PRIVILEGES,
+ } = ExtensionManagement.API_LEVELS;
let extensionId = ExtensionManagement.getAddonIdForWindow(window);
+ let apiLevel = ExtensionManagement.getAPILevelForWindow(window, extensionId);
- if (ExtensionManagement.getAPILevelForWindow(window, extensionId) == CONTENTSCRIPT_PRIVILEGES) {
+ if (apiLevel != NO_PRIVILEGES) {
let extension = ExtensionManager.get(extensionId);
if (extension) {
- DocumentManager.getExtensionPageContext(extension, window);
+ if (apiLevel == CONTENTSCRIPT_PRIVILEGES) {
+ DocumentManager.getExtensionPageContext(extension, window);
+ } else if (apiLevel == FULL_PRIVILEGES) {
+ ExtensionChild.createExtensionContext(extension, window);
+ }
}
}
this.trigger("document_start", window);
/* eslint-disable mozilla/balanced-listeners */
window.addEventListener("DOMContentLoaded", this, true);
window.addEventListener("load", this, true);
/* eslint-enable mozilla/balanced-listeners */
@@ -488,16 +503,18 @@ DocumentManager = {
}
// Close any existent iframe extension page context for the destroyed window.
if (this.extensionPageWindows.has(windowId)) {
let context = this.extensionPageWindows.get(windowId);
context.close();
this.extensionPageWindows.delete(windowId);
}
+
+ ExtensionChild.destroyExtensionContext(windowId);
}
},
handleEvent: function(event) {
let window = event.currentTarget;
if (event.target != window.document) {
// We use capturing listeners so we have precedence over content script
// listeners, but only care about events targeted to the element we're
@@ -634,16 +651,18 @@ DocumentManager = {
// Clean up iframe extension page contexts on extension shutdown.
for (let [winId, context] of this.extensionPageWindows) {
if (context.extension.id == extensionId) {
context.close();
this.extensionPageWindows.delete(winId);
}
}
+ ExtensionChild.shutdownExtension(extensionId);
+
MessageChannel.abortResponses({extensionId});
this.extensionCount--;
if (this.extensionCount == 0) {
this.uninit();
}
},
@@ -712,16 +731,17 @@ BrowserExtensionContent.prototype = {
};
ExtensionManager = {
// Map[extensionId, BrowserExtensionContent]
extensions: new Map(),
init() {
Schemas.init();
+ ExtensionChild.initOnce();
Services.cpmm.addMessageListener("Extension:Startup", this);
Services.cpmm.addMessageListener("Extension:Shutdown", this);
Services.cpmm.addMessageListener("Extension:FlushJarCache", this);
if (Services.cpmm.initialProcessData && "Extension:Extensions" in Services.cpmm.initialProcessData) {
let extensions = Services.cpmm.initialProcessData["Extension:Extensions"];
for (let data of extensions) {
--- a/toolkit/components/extensions/ext-backgroundPage.js
+++ b/toolkit/components/extensions/ext-backgroundPage.js
@@ -75,16 +75,17 @@ BackgroundPage.prototype = {
let browser = chromeDoc.createElement("browser");
browser.setAttribute("type", "content");
browser.setAttribute("disableglobalhistory", "true");
browser.setAttribute("webextension-view-type", "background");
browser.setAttribute("src", url);
chromeDoc.documentElement.appendChild(browser);
+ extensions.emit("extension-browser-inserted", browser);
yield new Promise(resolve => {
browser.addEventListener("load", function onLoad(event) {
if (event.target === browser.contentDocument) {
browser.removeEventListener("load", onLoad, true);
resolve();
}
}, true);