--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -1,14 +1,16 @@
/* 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";
+/* exported ExtensionChild */
+
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,56 +19,50 @@ this.EXPORTED_SYMBOLS = ["ExtensionChild
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
- "resource://gre/modules/ExtensionManagement.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionContent",
+ "resource://gre/modules/ExtensionContent.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs",
+ "resource://gre/modules/MatchPattern.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
+ "resource://gre/modules/MatchPattern.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
"resource://gre/modules/MessageChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NativeApp",
"resource://gre/modules/NativeMessaging.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
"resource://gre/modules/PromiseUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
- "resource://gre/modules/Schemas.jsm");
-
-const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon";
-const CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS = "webextension-scripts-devtools";
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
const {
DefaultMap,
+ EventEmitter,
LimitedSet,
+ LocaleData,
SingletonEventManager,
SpreadArgs,
defineLazyGetter,
- getInnerWindowID,
getMessageManager,
getUniqueId,
injectAPI,
- promiseEvent,
} = ExtensionUtils;
const {
- BaseContext,
- CanOfAPIs,
LocalAPIImplementation,
SchemaAPIInterface,
- SchemaAPIManager,
} = ExtensionCommon;
-var ExtensionChild;
-
/**
* Abstraction for a Port object in the extension API.
*
* @param {BaseContext} context The context that owns this port.
* @param {nsIMessageSender} senderMM The message manager to send messages to.
* @param {Array<nsIMessageListenerManager>} receiverMMs Message managers to
* listen on.
* @param {string} name Arbitrary port name as defined by the addon.
@@ -458,47 +454,124 @@ class Messenger {
return this._onConnect(name, sender => sender.id === this.sender.id);
}
onConnectExternal(name) {
return this._onConnect(name, sender => sender.id !== this.sender.id);
}
}
-var apiManager = new class extends SchemaAPIManager {
- constructor() {
- super("addon");
- this.initialized = false;
+// For test use only.
+var ExtensionManager = {
+ extensions: new Map(),
+};
+
+// Represents a browser extension in the content process.
+class BrowserExtensionContent extends EventEmitter {
+ constructor(data) {
+ super();
+
+ this.data = data;
+ this.id = data.id;
+ this.uuid = data.uuid;
+ this.instanceId = data.instanceId;
+
+ this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`;
+ Services.cpmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this);
+
+ defineLazyGetter(this, "scripts", () => {
+ return data.content_scripts.map(scriptData => new ExtensionContent.Script(this, scriptData));
+ });
+
+ this.webAccessibleResources = new MatchGlobs(data.webAccessibleResources);
+ this.whiteListedHosts = new MatchPattern(data.whiteListedHosts);
+ this.permissions = data.permissions;
+ this.optionalPermissions = data.optionalPermissions;
+ this.principal = data.principal;
+
+ this.localeData = new LocaleData(data.localeData);
+
+ this.manifest = data.manifest;
+ this.baseURI = Services.io.newURI(data.baseURL);
+
+ // Only used in addon processes.
+ this.views = new Set();
+
+ // Only used for devtools views.
+ this.devtoolsViews = new Set();
+
+ /* eslint-disable mozilla/balanced-listeners */
+ this.on("add-permissions", (ignoreEvent, permissions) => {
+ if (permissions.permissions.length > 0) {
+ for (let perm of permissions.permissions) {
+ this.permissions.add(perm);
+ }
+ }
+
+ if (permissions.origins.length > 0) {
+ this.whiteListedHosts = new MatchPattern(this.whiteListedHosts.pat.concat(...permissions.origins));
+ }
+ });
+
+ this.on("remove-permissions", (ignoreEvent, permissions) => {
+ if (permissions.permissions.length > 0) {
+ for (let perm of permissions.permissions) {
+ this.permissions.delete(perm);
+ }
+ }
+
+ if (permissions.origins.length > 0) {
+ for (let origin of permissions.origins) {
+ this.whiteListedHosts.removeOne(origin);
+ }
+ }
+ });
+ /* eslint-enable mozilla/balanced-listeners */
+
+ ExtensionManager.extensions.set(this.id, this);
}
- lazyInit() {
- if (!this.initialized) {
- this.initialized = true;
- for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_ADDON)) {
- this.loadScript(value);
- }
+ shutdown() {
+ ExtensionManager.extensions.delete(this.id);
+ ExtensionContent.shutdownExtension(this);
+ Services.cpmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this);
+ }
+
+ getContext(window) {
+ return ExtensionContent.getContext(this, window);
+ }
+
+ emit(event, ...args) {
+ Services.cpmm.sendAsyncMessage(this.MESSAGE_EMIT_EVENT, {event, args});
+
+ super.emit(event, ...args);
+ }
+
+ receiveMessage({name, data}) {
+ if (name === this.MESSAGE_EMIT_EVENT) {
+ super.emit(data.event, ...data.args);
}
}
-}();
-var devtoolsAPIManager = new class extends SchemaAPIManager {
- constructor() {
- super("devtools");
- this.initialized = false;
+ localizeMessage(...args) {
+ return this.localeData.localizeMessage(...args);
+ }
+
+ localize(...args) {
+ return this.localeData.localize(...args);
}
- lazyInit() {
- if (!this.initialized) {
- this.initialized = true;
- for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS)) {
- this.loadScript(value);
- }
+ hasPermission(perm) {
+ let match = /^manifest:(.*)/.exec(perm);
+ if (match) {
+ return this.manifest[match[1]] != null;
}
+ return this.permissions.has(perm);
}
-}();
+}
/**
* An object that runs an remote implementation of an API.
*/
class ProxyAPIImplementation extends SchemaAPIInterface {
/**
* @param {string} namespace The full path to the namespace that contains the
* `name` member. This may contain dots, e.g. "storage.local".
@@ -793,385 +866,14 @@ class ChildAPIManager {
return this.context.extension.optionalPermissions.includes(permission);
}
setPermissionsChangedCallback(callback) {
this.permissionsChangedCallbacks.add(callback);
}
}
-class ExtensionBaseContextChild extends BaseContext {
- /**
- * This ExtensionBaseContextChild represents an addon execution environment
- * that is running in an addon or devtools child process.
- *
- * @param {BrowserExtensionContent} extension This context's owner.
- * @param {object} params
- * @param {string} params.envType One of "addon_child" or "devtools_child".
- * @param {nsIDOMWindow} params.contentWindow The window where the addon runs.
- * @param {string} params.viewType One of "background", "popup", "tab",
- * "sidebar", "devtools_page" or "devtools_panel".
- * @param {number} [params.tabId] This tab's ID, used if viewType is "tab".
- */
- constructor(extension, params) {
- if (!params.envType) {
- throw new Error("Missing envType");
- }
-
- super(params.envType, extension);
- let {viewType, uri, contentWindow, tabId} = params;
- this.viewType = viewType;
- this.uri = uri || extension.baseURI;
-
- this.setContentWindow(contentWindow);
-
- // This is the MessageSender property passed to extension.
- // It can be augmented by the "page-open" hook.
- let sender = {id: extension.id};
- if (viewType == "tab") {
- sender.tabId = tabId;
- this.tabId = tabId;
- }
- if (uri) {
- sender.url = uri.spec;
- }
- this.sender = sender;
-
- Schemas.exportLazyGetter(contentWindow, "browser", () => {
- let browserObj = Cu.createObjectIn(contentWindow);
- Schemas.inject(browserObj, this.childManager);
- return browserObj;
- });
-
- Schemas.exportLazyGetter(contentWindow, "chrome", () => {
- let chromeApiWrapper = Object.create(this.childManager);
- chromeApiWrapper.isChromeCompat = true;
-
- let chromeObj = Cu.createObjectIn(contentWindow);
- Schemas.inject(chromeObj, chromeApiWrapper);
- return chromeObj;
- });
- }
-
- get cloneScope() {
- return this.contentWindow;
- }
-
- get principal() {
- return this.contentWindow.document.nodePrincipal;
- }
-
- get windowId() {
- if (["tab", "popup", "sidebar"].includes(this.viewType)) {
- let globalView = ExtensionChild.contentGlobals.get(this.messageManager);
- return globalView ? globalView.windowId : -1;
- }
- }
-
- // Called when the extension shuts down.
- shutdown() {
- this.unload();
- }
-
- // This method is called when an extension page navigates away or
- // its tab is closed.
- 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();
- }
-}
-
-defineLazyGetter(ExtensionBaseContextChild.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);
-});
-
-class ExtensionPageContextChild extends ExtensionBaseContextChild {
- /**
- * This ExtensionPageContextChild represents a privileged addon
- * execution environment that has full access to the WebExtensions
- * APIs (provided that the correct permissions have been requested).
- *
- * This is the child side of the ExtensionPageContextParent class
- * defined in ExtensionParent.jsm.
- *
- * @param {BrowserExtensionContent} extension This context's owner.
- * @param {object} params
- * @param {nsIDOMWindow} params.contentWindow The window where the addon runs.
- * @param {string} params.viewType One of "background", "popup", "sidebar" or "tab".
- * "background", "sidebar" and "tab" are used by `browser.extension.getViews`.
- * "popup" is only used internally to identify page action and browser
- * action popups and options_ui pages.
- * @param {number} [params.tabId] This tab's ID, used if viewType is "tab".
- */
- constructor(extension, params) {
- super(extension, Object.assign(params, {envType: "addon_child"}));
-
- this.extension.views.add(this);
- }
-
- unload() {
- super.unload();
- this.extension.views.delete(this);
- }
-}
-
-defineLazyGetter(ExtensionPageContextChild.prototype, "childManager", function() {
- apiManager.lazyInit();
-
- let localApis = {};
- let can = new CanOfAPIs(this, apiManager, localApis);
-
- let childManager = new ChildAPIManager(this, this.messageManager, can, {
- envType: "addon_parent",
- viewType: this.viewType,
- url: this.uri.spec,
- incognito: this.incognito,
- });
-
- this.callOnClose(childManager);
-
- if (this.viewType == "background") {
- apiManager.global.initializeBackgroundPage(this.contentWindow);
- }
-
- return childManager;
-});
-
-class DevToolsContextChild extends ExtensionBaseContextChild {
- /**
- * This DevToolsContextChild represents a devtools-related addon execution
- * environment that has access to the devtools API namespace and to the same subset
- * of APIs available in a content script execution environment.
- *
- * @param {BrowserExtensionContent} extension This context's owner.
- * @param {object} params
- * @param {nsIDOMWindow} params.contentWindow The window where the addon runs.
- * @param {string} params.viewType One of "devtools_page" or "devtools_panel".
- * @param {object} [params.devtoolsToolboxInfo] This devtools toolbox's information,
- * used if viewType is "devtools_page" or "devtools_panel".
- */
- constructor(extension, params) {
- super(extension, Object.assign(params, {envType: "devtools_child"}));
-
- this.devtoolsToolboxInfo = params.devtoolsToolboxInfo;
-
- this.extension.devtoolsViews.add(this);
- }
-
- unload() {
- super.unload();
- this.extension.devtoolsViews.delete(this);
- }
-}
-
-defineLazyGetter(DevToolsContextChild.prototype, "childManager", function() {
- devtoolsAPIManager.lazyInit();
-
- let localApis = {};
- let can = new CanOfAPIs(this, apiManager, localApis);
-
- let childManager = new ChildAPIManager(this, this.messageManager, can, {
- envType: "devtools_parent",
- viewType: this.viewType,
- url: this.uri.spec,
- incognito: this.incognito,
- });
-
- 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.
- */
- constructor(global) {
- this.global = global;
- // Unless specified otherwise assume that the extension page is in a tab,
- // because the majority of all class instances are going to be a tab. Any
- // special views (background page, extension popup) will immediately send an
- // Extension:InitExtensionView message to change the viewType.
- this.viewType = "tab";
- this.tabId = -1;
- this.windowId = -1;
- this.initialized = false;
-
- this.global.addMessageListener("Extension:InitExtensionView", this);
- this.global.addMessageListener("Extension:SetTabAndWindowId", this);
- }
-
- uninit() {
- this.global.removeMessageListener("Extension:InitExtensionView", this);
- this.global.removeMessageListener("Extension:SetTabAndWindowId", this);
- }
-
- ensureInitialized() {
- if (!this.initialized) {
- // Request tab and window ID in case "Extension:InitExtensionView" is not
- // sent (e.g. when `viewType` is "tab").
- let reply = this.global.sendSyncMessage("Extension:GetTabAndWindowId");
- this.handleSetTabAndWindowId(reply[0] || {});
- }
- return this;
- }
-
- receiveMessage({name, data}) {
- switch (name) {
- case "Extension:InitExtensionView":
- // The view type is initialized once and then fixed.
- this.global.removeMessageListener("Extension:InitExtensionView", this);
- this.viewType = data.viewType;
-
- // Force external links to open in tabs.
- if (["popup", "sidebar"].includes(this.viewType)) {
- this.global.docShell.isAppTab = true;
- }
-
- if (data.devtoolsToolboxInfo) {
- this.devtoolsToolboxInfo = data.devtoolsToolboxInfo;
- }
-
- promiseEvent(this.global, "DOMContentLoaded", true).then(() => {
- let windowId = getInnerWindowID(this.global.content);
- let context = ExtensionChild.extensionContexts.get(windowId);
-
- this.global.sendAsyncMessage("Extension:ExtensionViewLoaded",
- {childId: context && context.childManager.id});
- });
-
- /* FALLTHROUGH */
- case "Extension:SetTabAndWindowId":
- this.handleSetTabAndWindowId(data);
- break;
- }
- }
-
- handleSetTabAndWindowId(data) {
- let {tabId, windowId} = data;
-
- if (tabId) {
- // Tab IDs are not expected to change.
- if (this.tabId !== -1 && tabId !== this.tabId) {
- throw new Error("Attempted to change a tabId after it was set");
- }
- this.tabId = tabId;
- }
-
- if (windowId !== undefined) {
- // Window IDs may change if a tab is moved to a different location.
- // Note: This is the ID of the browser window for the extension API.
- // Do not confuse it with the innerWindowID of DOMWindows!
- this.windowId = windowId;
- }
- this.initialized = true;
- }
-}
-
-ExtensionChild = {
+var ExtensionChild = {
+ BrowserExtensionContent,
ChildAPIManager,
Messenger,
Port,
-
- // Map<nsIContentFrameMessageManager, ContentGlobal>
- contentGlobals: new Map(),
-
- // Map<innerWindowId, ExtensionPageContextChild>
- extensionContexts: new Map(),
-
- init(global) {
- if (!ExtensionManagement.isExtensionProcess) {
- throw new Error("Cannot init extension page global in current process");
- }
-
- this.contentGlobals.set(global, new ContentGlobal(global));
- },
-
- uninit(global) {
- this.contentGlobals.get(global).uninit();
- this.contentGlobals.delete(global);
- },
-
- /**
- * Create a privileged context at document-element-inserted.
- *
- * @param {BrowserExtensionContent} extension
- * The extension for which the context should be created.
- * @param {nsIDOMWindow} contentWindow The global of the page.
- */
- initExtensionContext(extension, contentWindow) {
- if (!ExtensionManagement.isExtensionProcess) {
- throw new Error("Cannot create an extension page context in current process");
- }
-
- let windowId = getInnerWindowID(contentWindow);
- let context = this.extensionContexts.get(windowId);
- if (context) {
- if (context.extension !== extension) {
- throw new Error("A different extension context already exists for this frame");
- }
- throw new Error("An extension context was already initialized for this frame");
- }
-
- let mm = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDocShell)
- .QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIContentFrameMessageManager);
-
- let {viewType, tabId, devtoolsToolboxInfo} = this.contentGlobals.get(mm).ensureInitialized();
-
- let uri = contentWindow.document.documentURIObject;
-
- if (devtoolsToolboxInfo) {
- context = new DevToolsContextChild(extension, {
- viewType, contentWindow, uri, tabId, devtoolsToolboxInfo,
- });
- } else {
- context = new ExtensionPageContextChild(extension, {viewType, contentWindow, uri, tabId});
- }
-
- this.extensionContexts.set(windowId, context);
- },
-
- /**
- * Close the ExtensionPageContextChild 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);
- }
- }
- },
};
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -10,20 +10,16 @@ this.EXPORTED_SYMBOLS = ["ExtensionConte
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
"resource:///modules/translation/LanguageDetector.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs",
- "resource://gre/modules/MatchPattern.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
- "resource://gre/modules/MatchPattern.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
"resource://gre/modules/MessageChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
"resource://gre/modules/WebNavigationFrames.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
@@ -38,39 +34,41 @@ const Timer = Components.Constructor("@m
Cu.import("resource://gre/modules/ExtensionChild.jsm");
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
const {
DefaultMap,
DefaultWeakMap,
- EventEmitter,
- LocaleData,
defineLazyGetter,
getInnerWindowID,
getWinUtils,
promiseDocumentLoaded,
promiseDocumentReady,
runSafeSyncWithoutClone,
} = ExtensionUtils;
const {
BaseContext,
CanOfAPIs,
SchemaAPIManager,
} = ExtensionCommon;
const {
+ BrowserExtensionContent,
ChildAPIManager,
Messenger,
} = ExtensionChild;
XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
+
+var DocumentManager;
+
const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
var apiManager = new class extends SchemaAPIManager {
constructor() {
super("content");
this.initialized = false;
}
@@ -171,16 +169,32 @@ class CSSCache extends CacheMap {
return;
}
}
super.delete(url);
}
}
+defineLazyGetter(BrowserExtensionContent.prototype, "staticScripts", () => {
+ return new ScriptCache({hasReturnValue: false});
+});
+
+defineLazyGetter(BrowserExtensionContent.prototype, "dynamicScripts", () => {
+ return new ScriptCache({hasReturnValue: true});
+});
+
+defineLazyGetter(BrowserExtensionContent.prototype, "userCSS", () => {
+ return new CSSCache(Ci.nsIStyleSheetService.USER_SHEET);
+});
+
+defineLazyGetter(BrowserExtensionContent.prototype, "authorCSS", () => {
+ return new CSSCache(Ci.nsIStyleSheetService.AUTHOR_SHEET);
+});
+
// Represents a content script.
class Script {
constructor(extension, options) {
this.extension = extension;
this.options = options;
this.runAt = this.options.run_at;
this.js = this.options.js || [];
@@ -243,16 +257,17 @@ class Script {
*
* @param {BaseContext} context
* The content script context into which to inject the scripts.
* @returns {Promise<any>}
* Resolves to the last value in the evaluated script, when
* execution is complete.
*/
async inject(context) {
+ DocumentManager.lazyInit();
if (this.requiresCleanup) {
context.addScript(this);
}
let cssPromise;
if (this.cssURLs.length) {
let window = context.contentWindow;
let winUtils = getWinUtils(window);
@@ -315,18 +330,16 @@ defineLazyGetter(Script.prototype, "cssU
if (this.options.cssCode) {
urls.push("data:text/css;charset=utf-8," + encodeURIComponent(this.options.cssCode));
}
return urls;
});
-var DocumentManager;
-
/**
* An execution context for semi-privileged extension content scripts.
*
* This is the child side of the ContentScriptContextParent class
* defined in ExtensionParent.jsm.
*/
class ContentScriptContextChild extends BaseContext {
constructor(extension, contentWindow) {
@@ -480,21 +493,16 @@ defineLazyGetter(ContentScriptContextChi
url: this.url,
});
this.callOnClose(childManager);
return childManager;
});
-// For test use only.
-var ExtensionManager = {
- extensions: new Map(),
-};
-
// Responsible for creating ExtensionContexts and injecting content
// scripts into them when new documents are created.
DocumentManager = {
// Map[windowId -> Map[ExtensionChild -> ContentScriptContextChild]]
contexts: new Map(),
initialized: false,
@@ -523,18 +531,16 @@ DocumentManager = {
if (this.contexts.has(windowId)) {
let extensions = this.contexts.get(windowId);
for (let context of extensions.values()) {
context.close();
}
this.contexts.delete(windowId);
}
-
- ExtensionChild.destroyExtensionContext(windowId);
},
"memory-pressure"(subject, topic, data) {
let timeout = data === "heap-minimize" ? 0 : undefined;
for (let cache of ChromeUtils.nondeterministicGetWeakSetKeys(scriptCaches)) {
cache.clear(timeout);
}
},
@@ -585,160 +591,47 @@ DocumentManager = {
return [];
},
initExtensionContext(extension, window) {
extension.getContext(window).injectAPI();
},
};
-// Represents a browser extension in the content process.
-class BrowserExtensionContent extends EventEmitter {
- constructor(data) {
- super();
-
- this.data = data;
- this.id = data.id;
- this.uuid = data.uuid;
- this.instanceId = data.instanceId;
-
- this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`;
- Services.cpmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this);
-
- defineLazyGetter(this, "scripts", () => {
- return data.content_scripts.map(scriptData => new Script(this, scriptData));
- });
-
- this.webAccessibleResources = new MatchGlobs(data.webAccessibleResources);
- this.whiteListedHosts = new MatchPattern(data.whiteListedHosts);
- this.permissions = data.permissions;
- this.optionalPermissions = data.optionalPermissions;
- this.principal = data.principal;
-
- this.localeData = new LocaleData(data.localeData);
-
- this.manifest = data.manifest;
- this.baseURI = Services.io.newURI(data.baseURL);
-
- // Only used in addon processes.
- this.views = new Set();
-
- // Only used for devtools views.
- this.devtoolsViews = new Set();
-
- /* eslint-disable mozilla/balanced-listeners */
- this.on("add-permissions", (ignoreEvent, permissions) => {
- if (permissions.permissions.length > 0) {
- for (let perm of permissions.permissions) {
- this.permissions.add(perm);
- }
- }
-
- if (permissions.origins.length > 0) {
- this.whiteListedHosts = new MatchPattern(this.whiteListedHosts.pat.concat(...permissions.origins));
- }
- });
-
- this.on("remove-permissions", (ignoreEvent, permissions) => {
- if (permissions.permissions.length > 0) {
- for (let perm of permissions.permissions) {
- this.permissions.delete(perm);
- }
- }
-
- if (permissions.origins.length > 0) {
- for (let origin of permissions.origins) {
- this.whiteListedHosts.removeOne(origin);
- }
- }
- });
- /* eslint-enable mozilla/balanced-listeners */
-
- ExtensionManager.extensions.set(this.id, this);
- DocumentManager.lazyInit();
- }
-
- shutdown() {
- ExtensionManager.extensions.delete(this.id);
- DocumentManager.shutdownExtension(this);
- Services.cpmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this);
- }
-
- getContext(window) {
- let extensions = DocumentManager.getContexts(window);
-
- let context = extensions.get(this);
- if (!context) {
- context = new ContentScriptContextChild(this, window);
- extensions.set(this, context);
- }
- return context;
- }
-
- emit(event, ...args) {
- Services.cpmm.sendAsyncMessage(this.MESSAGE_EMIT_EVENT, {event, args});
-
- super.emit(event, ...args);
- }
-
- receiveMessage({name, data}) {
- if (name === this.MESSAGE_EMIT_EVENT) {
- super.emit(data.event, ...data.args);
- }
- }
-
- localizeMessage(...args) {
- return this.localeData.localizeMessage(...args);
- }
-
- localize(...args) {
- return this.localeData.localize(...args);
- }
-
- hasPermission(perm) {
- let match = /^manifest:(.*)/.exec(perm);
- if (match) {
- return this.manifest[match[1]] != null;
- }
- return this.permissions.has(perm);
- }
-}
-
-defineLazyGetter(BrowserExtensionContent.prototype, "staticScripts", () => {
- return new ScriptCache({hasReturnValue: false});
-});
-
-defineLazyGetter(BrowserExtensionContent.prototype, "dynamicScripts", () => {
- return new ScriptCache({hasReturnValue: true});
-});
-
-defineLazyGetter(BrowserExtensionContent.prototype, "userCSS", () => {
- return new CSSCache(Ci.nsIStyleSheetService.USER_SHEET);
-});
-
-defineLazyGetter(BrowserExtensionContent.prototype, "authorCSS", () => {
- return new CSSCache(Ci.nsIStyleSheetService.AUTHOR_SHEET);
-});
-
this.ExtensionContent = {
BrowserExtensionContent,
Script,
+ shutdownExtension(extension) {
+ DocumentManager.shutdownExtension(extension);
+ },
+
// This helper is exported to be integrated in the devtools RDP actors,
// that can use it to retrieve the existent WebExtensions ContentScripts
// of a target window and be able to show the ContentScripts source in the
// DevTools Debugger panel.
getContentScriptGlobals(window) {
return DocumentManager.getContentScriptGlobals(window);
},
initExtensionContext(extension, window) {
DocumentManager.initExtensionContext(extension, window);
},
+ getContext(extension, window) {
+ let extensions = DocumentManager.getContexts(window);
+
+ let context = extensions.get(this);
+ if (!context) {
+ context = new ContentScriptContextChild(extension, window);
+ extensions.set(extension, context);
+ }
+ return context;
+ },
+
handleExtensionCapture(global, width, height, options) {
let win = global.content;
const XHTML_NS = "http://www.w3.org/1999/xhtml";
let canvas = win.document.createElementNS(XHTML_NS, "canvas");
canvas.width = width;
canvas.height = height;
canvas.mozOpaque = true;
copy from toolkit/components/extensions/ExtensionChild.jsm
copy to toolkit/components/extensions/ExtensionPageChild.jsm
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionPageChild.jsm
@@ -1,472 +1,61 @@
/* 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 = ["ExtensionChild"];
+/* exported ExtensionPageChild */
+
+this.EXPORTED_SYMBOLS = ["ExtensionPageChild"];
-/*
- * 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.
+/**
+ * This file handles privileged extension page logic that runs in the
+ * child process.
*/
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
"resource://gre/modules/ExtensionManagement.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
- "resource://gre/modules/MessageChannel.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NativeApp",
- "resource://gre/modules/NativeMessaging.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
- "resource://gre/modules/PromiseUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon";
const CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS = "webextension-scripts-devtools";
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
+Cu.import("resource://gre/modules/ExtensionChild.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
const {
- DefaultMap,
- LimitedSet,
- SingletonEventManager,
- SpreadArgs,
defineLazyGetter,
getInnerWindowID,
- getMessageManager,
- getUniqueId,
- injectAPI,
promiseEvent,
} = ExtensionUtils;
const {
BaseContext,
CanOfAPIs,
- LocalAPIImplementation,
- SchemaAPIInterface,
SchemaAPIManager,
} = ExtensionCommon;
-var ExtensionChild;
-
-/**
- * Abstraction for a Port object in the extension API.
- *
- * @param {BaseContext} context The context that owns this port.
- * @param {nsIMessageSender} senderMM The message manager to send messages to.
- * @param {Array<nsIMessageListenerManager>} receiverMMs Message managers to
- * listen on.
- * @param {string} name Arbitrary port name as defined by the addon.
- * @param {string} id An ID that uniquely identifies this port's channel.
- * @param {object} sender The `port.sender` property.
- * @param {object} recipient The recipient of messages sent from this port.
- */
-class Port {
- constructor(context, senderMM, receiverMMs, name, id, sender, recipient) {
- this.context = context;
- this.senderMM = senderMM;
- this.receiverMMs = receiverMMs;
- this.name = name;
- this.id = id;
- this.sender = sender;
- this.recipient = recipient;
- this.disconnected = false;
- this.disconnectListeners = new Set();
- this.unregisterMessageFuncs = new Set();
-
- // Common options for onMessage and onDisconnect.
- this.handlerBase = {
- messageFilterStrict: {portId: id},
-
- filterMessage: (sender, recipient) => {
- return sender.contextId !== this.context.contextId;
- },
- };
-
- this.disconnectHandler = Object.assign({
- receiveMessage: ({data}) => this.disconnectByOtherEnd(data),
- }, this.handlerBase);
-
- MessageChannel.addListener(this.receiverMMs, "Extension:Port:Disconnect", this.disconnectHandler);
-
- this.context.callOnClose(this);
- }
-
- api() {
- let portObj = Cu.createObjectIn(this.context.cloneScope);
-
- let portError = null;
- let publicAPI = {
- name: this.name,
-
- disconnect: () => {
- this.disconnect();
- },
-
- postMessage: json => {
- this.postMessage(json);
- },
-
- onDisconnect: new SingletonEventManager(this.context, "Port.onDisconnect", fire => {
- return this.registerOnDisconnect(error => {
- portError = error && this.context.normalizeError(error);
- fire.asyncWithoutClone(portObj);
- });
- }).api(),
-
- onMessage: new SingletonEventManager(this.context, "Port.onMessage", fire => {
- return this.registerOnMessage(msg => {
- msg = Cu.cloneInto(msg, this.context.cloneScope);
- fire.asyncWithoutClone(msg, portObj);
- });
- }).api(),
-
- get error() {
- return portError;
- },
- };
-
- if (this.sender) {
- publicAPI.sender = this.sender;
- }
-
- injectAPI(publicAPI, portObj);
- return portObj;
- }
-
- postMessage(json) {
- if (this.disconnected) {
- throw new this.context.cloneScope.Error("Attempt to postMessage on disconnected port");
- }
-
- this._sendMessage("Extension:Port:PostMessage", json);
- }
-
- /**
- * Register a callback that is called when the port is disconnected by the
- * *other* end. The callback is automatically unregistered when the port or
- * context is closed.
- *
- * @param {function} callback Called when the other end disconnects the port.
- * If the disconnect is caused by an error, the first parameter is an
- * object with a "message" string property that describes the cause.
- * @returns {function} Function to unregister the listener.
- */
- registerOnDisconnect(callback) {
- let listener = error => {
- if (this.context.active && !this.disconnected) {
- callback(error);
- }
- };
- this.disconnectListeners.add(listener);
- return () => {
- this.disconnectListeners.delete(listener);
- };
- }
-
- /**
- * Register a callback that is called when a message is received. The callback
- * is automatically unregistered when the port or context is closed.
- *
- * @param {function} callback Called when a message is received.
- * @returns {function} Function to unregister the listener.
- */
- registerOnMessage(callback) {
- let handler = Object.assign({
- receiveMessage: ({data}) => {
- if (this.context.active && !this.disconnected) {
- callback(data);
- }
- },
- }, this.handlerBase);
-
- let unregister = () => {
- this.unregisterMessageFuncs.delete(unregister);
- MessageChannel.removeListener(this.receiverMMs, "Extension:Port:PostMessage", handler);
- };
- MessageChannel.addListener(this.receiverMMs, "Extension:Port:PostMessage", handler);
- this.unregisterMessageFuncs.add(unregister);
- return unregister;
- }
-
- _sendMessage(message, data) {
- let options = {
- recipient: Object.assign({}, this.recipient, {portId: this.id}),
- responseType: MessageChannel.RESPONSE_NONE,
- };
-
- return this.context.sendMessage(this.senderMM, message, data, options);
- }
-
- handleDisconnection() {
- MessageChannel.removeListener(this.receiverMMs, "Extension:Port:Disconnect", this.disconnectHandler);
- for (let unregister of this.unregisterMessageFuncs) {
- unregister();
- }
- this.context.forgetOnClose(this);
- this.disconnected = true;
- }
-
- /**
- * Disconnect the port from the other end (which may not even exist).
- *
- * @param {Error|{message: string}} [error] The reason for disconnecting,
- * if it is an abnormal disconnect.
- */
- disconnectByOtherEnd(error = null) {
- if (this.disconnected) {
- return;
- }
-
- for (let listener of this.disconnectListeners) {
- listener(error);
- }
-
- this.handleDisconnection();
- }
-
- /**
- * Disconnect the port from this end.
- *
- * @param {Error|{message: string}} [error] The reason for disconnecting,
- * if it is an abnormal disconnect.
- */
- disconnect(error = null) {
- if (this.disconnected) {
- // disconnect() may be called without side effects even after the port is
- // closed - https://developer.chrome.com/extensions/runtime#type-Port
- return;
- }
- this.handleDisconnection();
- if (error) {
- error = {message: this.context.normalizeError(error).message};
- }
- this._sendMessage("Extension:Port:Disconnect", error);
- }
+const {
+ ChildAPIManager,
+ Messenger,
+} = ExtensionChild;
- close() {
- this.disconnect();
- }
-}
-
-class NativePort extends Port {
- postMessage(data) {
- data = NativeApp.encodeMessage(this.context, data);
-
- return super.postMessage(data);
- }
-}
-
-/**
- * Each extension context gets its own Messenger object. It handles the
- * basics of sendMessage, onMessage, connect and onConnect.
- *
- * @param {BaseContext} context The context to which this Messenger is tied.
- * @param {Array<nsIMessageListenerManager>} messageManagers
- * The message managers used to receive messages (e.g. onMessage/onConnect
- * requests).
- * @param {object} sender Describes this sender to the recipient. This object
- * is extended further by BaseContext's sendMessage method and appears as
- * the `sender` object to `onConnect` and `onMessage`.
- * Do not set the `extensionId`, `contextId` or `tab` properties. The former
- * two are added by BaseContext's sendMessage, while `sender.tab` is set by
- * the ProxyMessenger in the main process.
- * @param {object} filter A recipient filter to apply to incoming messages from
- * the broker. Messages are only handled by this Messenger if all key-value
- * pairs match the `recipient` as specified by the sender of the message.
- * In other words, this filter defines the required fields of `recipient`.
- * @param {object} [optionalFilter] An additional filter to apply to incoming
- * messages. Unlike `filter`, the keys from `optionalFilter` are allowed to
- * be omitted from `recipient`. Only keys that are present in both
- * `optionalFilter` and `recipient` are applied to filter incoming messages.
- */
-class Messenger {
- constructor(context, messageManagers, sender, filter, optionalFilter) {
- this.context = context;
- this.messageManagers = messageManagers;
- this.sender = sender;
- this.filter = filter;
- this.optionalFilter = optionalFilter;
- }
-
- _sendMessage(messageManager, message, data, recipient) {
- let options = {
- recipient,
- sender: this.sender,
- responseType: MessageChannel.RESPONSE_FIRST,
- };
-
- return this.context.sendMessage(messageManager, message, data, options);
- }
-
- sendMessage(messageManager, msg, recipient, responseCallback) {
- let promise = this._sendMessage(messageManager, "Extension:Message", msg, recipient)
- .catch(error => {
- if (error.result == MessageChannel.RESULT_NO_HANDLER) {
- return Promise.reject({message: "Could not establish connection. Receiving end does not exist."});
- } else if (error.result != MessageChannel.RESULT_NO_RESPONSE) {
- return Promise.reject({message: error.message});
- }
- });
-
- return this.context.wrapPromise(promise, responseCallback);
- }
-
- sendNativeMessage(messageManager, msg, recipient, responseCallback) {
- msg = NativeApp.encodeMessage(this.context, msg);
- return this.sendMessage(messageManager, msg, recipient, responseCallback);
- }
-
- _onMessage(name, filter) {
- return new SingletonEventManager(this.context, name, fire => {
- let listener = {
- messageFilterPermissive: this.optionalFilter,
- messageFilterStrict: this.filter,
-
- filterMessage: (sender, recipient) => {
- // Ignore the message if it was sent by this Messenger.
- return (sender.contextId !== this.context.contextId &&
- filter(sender, recipient));
- },
-
- receiveMessage: ({target, data: message, sender, recipient}) => {
- if (!this.context.active) {
- return;
- }
-
- let sendResponse;
- let response = undefined;
- let promise = new Promise(resolve => {
- sendResponse = value => {
- resolve(value);
- response = promise;
- };
- });
-
- message = Cu.cloneInto(message, this.context.cloneScope);
- sender = Cu.cloneInto(sender, this.context.cloneScope);
- sendResponse = Cu.exportFunction(sendResponse, this.context.cloneScope);
-
- // Note: We intentionally do not use runSafe here so that any
- // errors are propagated to the message sender.
- let result = fire.raw(message, sender, sendResponse);
- if (result instanceof this.context.cloneScope.Promise) {
- return result;
- } else if (result === true) {
- return promise;
- }
- return response;
- },
- };
-
- MessageChannel.addListener(this.messageManagers, "Extension:Message", listener);
- return () => {
- MessageChannel.removeListener(this.messageManagers, "Extension:Message", listener);
- };
- }).api();
- }
-
- onMessage(name) {
- return this._onMessage(name, sender => sender.id === this.sender.id);
- }
-
- onMessageExternal(name) {
- return this._onMessage(name, sender => sender.id !== this.sender.id);
- }
-
- _connect(messageManager, port, recipient) {
- let msg = {
- name: port.name,
- portId: port.id,
- };
-
- this._sendMessage(messageManager, "Extension:Connect", msg, recipient).catch(error => {
- if (error.result === MessageChannel.RESULT_NO_HANDLER) {
- error = {message: "Could not establish connection. Receiving end does not exist."};
- } else if (error.result === MessageChannel.RESULT_DISCONNECTED) {
- error = null;
- }
- port.disconnectByOtherEnd(error);
- });
-
- return port.api();
- }
-
- connect(messageManager, name, recipient) {
- let portId = getUniqueId();
-
- let port = new Port(this.context, messageManager, this.messageManagers, name, portId, null, recipient);
-
- return this._connect(messageManager, port, recipient);
- }
-
- connectNative(messageManager, name, recipient) {
- let portId = getUniqueId();
-
- let port = new NativePort(this.context, messageManager, this.messageManagers, name, portId, null, recipient);
-
- return this._connect(messageManager, port, recipient);
- }
-
- _onConnect(name, filter) {
- return new SingletonEventManager(this.context, name, fire => {
- let listener = {
- messageFilterPermissive: this.optionalFilter,
- messageFilterStrict: this.filter,
-
- filterMessage: (sender, recipient) => {
- // Ignore the port if it was created by this Messenger.
- return (sender.contextId !== this.context.contextId &&
- filter(sender, recipient));
- },
-
- receiveMessage: ({target, data: message, sender}) => {
- let {name, portId} = message;
- let mm = getMessageManager(target);
- let recipient = Object.assign({}, sender);
- if (recipient.tab) {
- recipient.tabId = recipient.tab.id;
- delete recipient.tab;
- }
- let port = new Port(this.context, mm, this.messageManagers, name, portId, sender, recipient);
- fire.asyncWithoutClone(port.api());
- return true;
- },
- };
-
- MessageChannel.addListener(this.messageManagers, "Extension:Connect", listener);
- return () => {
- MessageChannel.removeListener(this.messageManagers, "Extension:Connect", listener);
- };
- }).api();
- }
-
- onConnect(name) {
- return this._onConnect(name, sender => sender.id === this.sender.id);
- }
-
- onConnectExternal(name) {
- return this._onConnect(name, sender => sender.id !== this.sender.id);
- }
-}
+var ExtensionPageChild;
var apiManager = new class extends SchemaAPIManager {
constructor() {
super("addon");
this.initialized = false;
}
lazyInit() {
@@ -490,319 +79,16 @@ var devtoolsAPIManager = new class exten
this.initialized = true;
for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS)) {
this.loadScript(value);
}
}
}
}();
-/**
- * An object that runs an remote implementation of an API.
- */
-class ProxyAPIImplementation extends SchemaAPIInterface {
- /**
- * @param {string} namespace The full path to the namespace that contains the
- * `name` member. This may contain dots, e.g. "storage.local".
- * @param {string} name The name of the method or property.
- * @param {ChildAPIManager} childApiManager The owner of this implementation.
- */
- constructor(namespace, name, childApiManager) {
- super();
- this.path = `${namespace}.${name}`;
- this.childApiManager = childApiManager;
- }
-
- revoke() {
- let map = this.childApiManager.listeners.get(this.path);
- for (let listener of map.keys()) {
- this.removeListener(listener);
- }
-
- this.path = null;
- this.childApiManager = null;
- }
-
- callFunctionNoReturn(args) {
- this.childApiManager.callParentFunctionNoReturn(this.path, args);
- }
-
- callAsyncFunction(args, callback) {
- return this.childApiManager.callParentAsyncFunction(this.path, args, callback);
- }
-
- addListener(listener, args) {
- let map = this.childApiManager.listeners.get(this.path);
-
- if (map.listeners.has(listener)) {
- // TODO: Called with different args?
- return;
- }
-
- let id = getUniqueId();
-
- map.ids.set(id, listener);
- map.listeners.set(listener, id);
-
- this.childApiManager.messageManager.sendAsyncMessage("API:AddListener", {
- childId: this.childApiManager.id,
- listenerId: id,
- path: this.path,
- args,
- });
- }
-
- removeListener(listener) {
- let map = this.childApiManager.listeners.get(this.path);
-
- if (!map.listeners.has(listener)) {
- return;
- }
-
- let id = map.listeners.get(listener);
- map.listeners.delete(listener);
- map.ids.delete(id);
- map.removedIds.add(id);
-
- this.childApiManager.messageManager.sendAsyncMessage("API:RemoveListener", {
- childId: this.childApiManager.id,
- listenerId: id,
- path: this.path,
- });
- }
-
- hasListener(listener) {
- let map = this.childApiManager.listeners.get(this.path);
- return map.listeners.has(listener);
- }
-}
-
-// We create one instance of this class for every extension context that
-// needs to use remote APIs. It uses the message manager to communicate
-// with the ParentAPIManager singleton in ExtensionParent.jsm. It
-// handles asynchronous function calls as well as event listeners.
-class ChildAPIManager {
- constructor(context, messageManager, localAPICan, contextData) {
- this.context = context;
- this.messageManager = messageManager;
- this.url = contextData.url;
-
- // The root namespace of all locally implemented APIs. If an extension calls
- // an API that does not exist in this object, then the implementation is
- // delegated to the ParentAPIManager.
- this.localApis = localAPICan.root;
- this.apiCan = localAPICan;
-
- this.id = `${context.extension.id}.${context.contextId}`;
-
- MessageChannel.addListener(messageManager, "API:RunListener", this);
- messageManager.addMessageListener("API:CallResult", this);
-
- this.messageFilterStrict = {childId: this.id};
-
- this.listeners = new DefaultMap(() => ({
- ids: new Map(),
- listeners: new Map(),
- removedIds: new LimitedSet(10),
- }));
-
- // Map[callId -> Deferred]
- this.callPromises = new Map();
-
- let params = {
- childId: this.id,
- extensionId: context.extension.id,
- principal: context.principal,
- };
- Object.assign(params, contextData);
-
- this.messageManager.sendAsyncMessage("API:CreateProxyContext", params);
-
- this.permissionsChangedCallbacks = new Set();
- this.updatePermissions = null;
- if (this.context.extension.optionalPermissions.length > 0) {
- this.updatePermissions = () => {
- for (let callback of this.permissionsChangedCallbacks) {
- try {
- callback();
- } catch (err) {
- Cu.reportError(err);
- }
- }
- };
- this.context.extension.on("add-permissions", this.updatePermissions);
- this.context.extension.on("remove-permissions", this.updatePermissions);
- }
- }
-
- receiveMessage({name, messageName, data}) {
- if (data.childId != this.id) {
- return;
- }
-
- switch (name || messageName) {
- case "API:RunListener":
- let map = this.listeners.get(data.path);
- let listener = map.ids.get(data.listenerId);
-
- if (listener) {
- return this.context.runSafe(listener, ...data.args);
- }
- if (!map.removedIds.has(data.listenerId)) {
- Services.console.logStringMessage(
- `Unknown listener at childId=${data.childId} path=${data.path} listenerId=${data.listenerId}\n`);
- }
- break;
-
- case "API:CallResult":
- let deferred = this.callPromises.get(data.callId);
- if ("error" in data) {
- deferred.reject(data.error);
- } else {
- deferred.resolve(new SpreadArgs(data.result));
- }
- this.callPromises.delete(data.callId);
- break;
- }
- }
-
- /**
- * Call a function in the parent process and ignores its return value.
- *
- * @param {string} path The full name of the method, e.g. "tabs.create".
- * @param {Array} args The parameters for the function.
- */
- callParentFunctionNoReturn(path, args) {
- this.messageManager.sendAsyncMessage("API:Call", {
- childId: this.id,
- path,
- args,
- });
- }
-
- /**
- * Calls a function in the parent process and returns its result
- * asynchronously.
- *
- * @param {string} path The full name of the method, e.g. "tabs.create".
- * @param {Array} args The parameters for the function.
- * @param {function(*)} [callback] The callback to be called when the function
- * completes.
- * @returns {Promise|undefined} Must be void if `callback` is set, and a
- * promise otherwise. The promise is resolved when the function completes.
- */
- callParentAsyncFunction(path, args, callback) {
- let callId = getUniqueId();
- let deferred = PromiseUtils.defer();
- this.callPromises.set(callId, deferred);
-
- this.messageManager.sendAsyncMessage("API:Call", {
- childId: this.id,
- callId,
- path,
- args,
- });
-
- return this.context.wrapPromise(deferred.promise, callback);
- }
-
- /**
- * Create a proxy for an event in the parent process. The returned event
- * object shares its internal state with other instances. For instance, if
- * `removeListener` is used on a listener that was added on another object
- * through `addListener`, then the event is unregistered.
- *
- * @param {string} path The full name of the event, e.g. "tabs.onCreated".
- * @returns {object} An object with the addListener, removeListener and
- * hasListener methods. See SchemaAPIInterface for documentation.
- */
- getParentEvent(path) {
- path = path.split(".");
-
- let name = path.pop();
- let namespace = path.join(".");
-
- let impl = new ProxyAPIImplementation(namespace, name, this);
- return {
- addListener: (listener, ...args) => impl.addListener(listener, args),
- removeListener: (listener) => impl.removeListener(listener),
- hasListener: (listener) => impl.hasListener(listener),
- };
- }
-
- close() {
- this.messageManager.sendAsyncMessage("API:CloseProxyContext", {childId: this.id});
- if (this.updatePermissions) {
- this.context.extension.off("add-permissions", this.updatePermissions);
- this.context.extension.off("remove-permissions", this.updatePermissions);
- }
- }
-
- get cloneScope() {
- return this.context.cloneScope;
- }
-
- get principal() {
- return this.context.principal;
- }
-
- shouldInject(namespace, name, allowedContexts) {
- // Do not generate content script APIs, unless explicitly allowed.
- if (this.context.envType === "content_child" &&
- !allowedContexts.includes("content")) {
- return false;
- }
- if (allowedContexts.includes("addon_parent_only")) {
- return false;
- }
-
- // Do not generate devtools APIs, unless explicitly allowed.
- if (this.context.envType === "devtools_child" &&
- !allowedContexts.includes("devtools")) {
- return false;
- }
-
- // Do not generate devtools APIs, unless explicitly allowed.
- if (this.context.envType !== "devtools_child" &&
- allowedContexts.includes("devtools_only")) {
- return false;
- }
-
- return true;
- }
-
- getImplementation(namespace, name) {
- this.apiCan.findAPIPath(`${namespace}.${name}`);
- let obj = this.apiCan.findAPIPath(namespace);
-
- if (obj && name in obj) {
- return new LocalAPIImplementation(obj, name, this.context);
- }
-
- return this.getFallbackImplementation(namespace, name);
- }
-
- getFallbackImplementation(namespace, name) {
- // No local API found, defer implementation to the parent.
- return new ProxyAPIImplementation(namespace, name, this);
- }
-
- hasPermission(permission) {
- return this.context.extension.hasPermission(permission);
- }
-
- isPermissionRevokable(permission) {
- return this.context.extension.optionalPermissions.includes(permission);
- }
-
- setPermissionsChangedCallback(callback) {
- this.permissionsChangedCallbacks.add(callback);
- }
-}
-
class ExtensionBaseContextChild extends BaseContext {
/**
* This ExtensionBaseContextChild represents an addon execution environment
* that is running in an addon or devtools child process.
*
* @param {BrowserExtensionContent} extension This context's owner.
* @param {object} params
* @param {string} params.envType One of "addon_child" or "devtools_child".
@@ -856,17 +142,17 @@ class ExtensionBaseContextChild extends
}
get principal() {
return this.contentWindow.document.nodePrincipal;
}
get windowId() {
if (["tab", "popup", "sidebar"].includes(this.viewType)) {
- let globalView = ExtensionChild.contentGlobals.get(this.messageManager);
+ let globalView = ExtensionPageChild.contentGlobals.get(this.messageManager);
return globalView ? globalView.windowId : -1;
}
}
// Called when the extension shuts down.
shutdown() {
this.unload();
}
@@ -1049,17 +335,17 @@ class ContentGlobal {
}
if (data.devtoolsToolboxInfo) {
this.devtoolsToolboxInfo = data.devtoolsToolboxInfo;
}
promiseEvent(this.global, "DOMContentLoaded", true).then(() => {
let windowId = getInnerWindowID(this.global.content);
- let context = ExtensionChild.extensionContexts.get(windowId);
+ let context = ExtensionPageChild.extensionContexts.get(windowId);
this.global.sendAsyncMessage("Extension:ExtensionViewLoaded",
{childId: context && context.childManager.id});
});
/* FALLTHROUGH */
case "Extension:SetTabAndWindowId":
this.handleSetTabAndWindowId(data);
@@ -1083,48 +369,69 @@ class ContentGlobal {
// Note: This is the ID of the browser window for the extension API.
// Do not confuse it with the innerWindowID of DOMWindows!
this.windowId = windowId;
}
this.initialized = true;
}
}
-ExtensionChild = {
- ChildAPIManager,
- Messenger,
- Port,
-
+ExtensionPageChild = {
// Map<nsIContentFrameMessageManager, ContentGlobal>
contentGlobals: new Map(),
// Map<innerWindowId, ExtensionPageContextChild>
extensionContexts: new Map(),
+ initialized: false,
+
+ _init() {
+ if (this.initialized) {
+ return;
+ }
+ this.initialized = true;
+
+ Services.obs.addObserver(this, "inner-window-destroyed", false); // eslint-ignore-line mozilla/balanced-listeners
+ },
+
init(global) {
if (!ExtensionManagement.isExtensionProcess) {
throw new Error("Cannot init extension page global in current process");
}
- this.contentGlobals.set(global, new ContentGlobal(global));
+ if (!this.contentGlobals.has(global)) {
+ this.contentGlobals.set(global, new ContentGlobal(global));
+ }
},
uninit(global) {
- this.contentGlobals.get(global).uninit();
- this.contentGlobals.delete(global);
+ if (this.contentGlobals.has(global)) {
+ this.contentGlobals.get(global).uninit();
+ this.contentGlobals.delete(global);
+ }
+ },
+
+ observe(subject, topic, data) {
+ if (topic === "inner-window-destroyed") {
+ let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+
+ this.destroyExtensionContext(windowId);
+ }
},
/**
* Create a privileged context at document-element-inserted.
*
* @param {BrowserExtensionContent} extension
* The extension for which the context should be created.
* @param {nsIDOMWindow} contentWindow The global of the page.
*/
initExtensionContext(extension, contentWindow) {
+ this._init();
+
if (!ExtensionManagement.isExtensionProcess) {
throw new Error("Cannot create an extension page context in current process");
}
let windowId = getInnerWindowID(contentWindow);
let context = this.extensionContexts.get(windowId);
if (context) {
if (context.extension !== extension) {
--- a/toolkit/components/extensions/LegacyExtensionsUtils.jsm
+++ b/toolkit/components/extensions/LegacyExtensionsUtils.jsm
@@ -14,30 +14,27 @@ this.EXPORTED_SYMBOLS = ["LegacyExtensio
*/
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Extension",
"resource://gre/modules/Extension.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionChild",
+ "resource://gre/modules/ExtensionChild.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/ExtensionChild.jsm");
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
var {
BaseContext,
} = ExtensionCommon;
-var {
- Messenger,
-} = ExtensionChild;
-
/**
* Instances created from this class provide to a legacy extension
* a simple API to exchange messages with a webextension.
*/
var LegacyExtensionContext = class extends BaseContext {
/**
* Create a new LegacyExtensionContext given a target Extension instance.
*
@@ -64,17 +61,17 @@ var LegacyExtensionContext = class exten
{value: cloneScope, enumerable: true, configurable: true, writable: true}
);
let sender = {id: targetExtension.id};
let filter = {extensionId: targetExtension.id};
// Legacy addons live in the main process. Messages from other addons are
// Messages from WebExtensions are sent to the main process and forwarded via
// the parent process manager to the legacy extension.
- this.messenger = new Messenger(this, [Services.cpmm], sender, filter);
+ this.messenger = new ExtensionChild.Messenger(this, [Services.cpmm], sender, filter);
this.api = {
browser: {
runtime: {
onConnect: this.messenger.onConnect("runtime.onConnect"),
onMessage: this.messenger.onMessage("runtime.onMessage"),
},
},
--- a/toolkit/components/extensions/ProxyScriptContext.jsm
+++ b/toolkit/components/extensions/ProxyScriptContext.jsm
@@ -7,20 +7,21 @@
this.EXPORTED_SYMBOLS = ["ProxyScriptContext"];
/* exported ProxyScriptContext */
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/ExtensionChild.jsm");
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionChild",
+ "resource://gre/modules/ExtensionChild.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "ProxyService",
"@mozilla.org/network/protocol-proxy-service;1",
"nsIProtocolProxyService");
const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
@@ -33,20 +34,16 @@ const {
const {
BaseContext,
CanOfAPIs,
LocalAPIImplementation,
SchemaAPIManager,
} = ExtensionCommon;
-const {
- Messenger,
-} = ExtensionChild;
-
const PROXY_TYPES = Object.freeze({
DIRECT: "direct",
HTTPS: "https",
PROXY: "proxy",
SOCKS: "socks",
});
class ProxyScriptContext extends BaseContext {
@@ -272,17 +269,17 @@ class ProxyScriptInjectionContext {
get principal() {
return this.context.principal;
}
}
defineLazyGetter(ProxyScriptContext.prototype, "messenger", function() {
let sender = {id: this.extension.id, frameId: this.frameId, url: this.url};
let filter = {extensionId: this.extension.id, toProxyScript: true};
- return new Messenger(this, [this.messageManager], sender, filter);
+ return new ExtensionChild.Messenger(this, [this.messageManager], sender, filter);
});
let proxyScriptAPIManager = new ProxyScriptAPIManager();
defineLazyGetter(ProxyScriptContext.prototype, "browserObj", function() {
let localAPIs = {};
let can = new CanOfAPIs(this, proxyScriptAPIManager, localAPIs);
proxyScriptAPIManager.lazyInit();
--- a/toolkit/components/extensions/extension-process-script.js
+++ b/toolkit/components/extensions/extension-process-script.js
@@ -25,16 +25,18 @@ XPCOMUtils.defineLazyModuleGetter(this,
"resource://gre/modules/MessageChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
"resource://gre/modules/WebNavigationFrames.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionChild",
"resource://gre/modules/ExtensionChild.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionContent",
"resource://gre/modules/ExtensionContent.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionPageChild",
+ "resource://gre/modules/ExtensionPageChild.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
"resource://gre/modules/ExtensionUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionUtils.getConsole());
XPCOMUtils.defineLazyGetter(this, "getInnerWindowID", () => ExtensionUtils.getInnerWindowID);
const isContentProcess = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
@@ -264,40 +266,54 @@ DocumentManager = {
// to observe "content-document-global-created".
initAboutBlankMatchers() {
Services.obs.addObserver(this, "content-document-global-created", false);
},
uninitAboutBlankMatchers() {
Services.obs.removeObserver(this, "content-document-global-created");
},
+ extensionProcessInitialized: false,
+ initExtensionProcess() {
+ if (this.extensionProcessInitialized || !ExtensionManagement.isExtensionProcess) {
+ return;
+ }
+ this.extensionProcessInitialized = true;
+
+ for (let global of this.globals.keys()) {
+ ExtensionPageChild.init(global);
+ }
+ },
+
// Initialize a frame script global which extension contexts may be loaded
// into.
initGlobal(global) {
// Note: {once: true} does not work as expected here.
global.addEventListener("unload", event => { // eslint-disable-line mozilla/balanced-listeners
this.uninitGlobal(global);
});
this.globals.set(global, new ExtensionGlobal(global));
- if (ExtensionManagement.isExtensionProcess) {
- ExtensionChild.init(global);
+ this.initExtensionProcess();
+ if (this.extensionProcessInitialized && ExtensionManagement.isExtensionProcess) {
+ ExtensionPageChild.init(global);
}
},
uninitGlobal(global) {
- if (ExtensionManagement.isExtensionProcess) {
- ExtensionChild.uninit(global);
+ if (this.extensionProcessInitialized) {
+ ExtensionPageChild.uninit(global);
}
this.globals.get(global).uninit();
this.globals.delete(global);
},
initExtension(extension) {
if (this.extensionCount === 0) {
this.init();
+ this.initExtensionProcess();
}
this.extensionCount++;
for (let script of extension.scripts) {
this.addContentScript(script);
}
this.injectExtensionScripts(extension);
@@ -448,17 +464,17 @@ DocumentManager = {
}
let apiLevel = ExtensionManagement.getAPILevelForWindow(window, extensionId);
const levels = ExtensionManagement.API_LEVELS;
if (apiLevel === levels.CONTENTSCRIPT_PRIVILEGES) {
ExtensionContent.initExtensionContext(extension.realExtension, window);
} else if (apiLevel === levels.FULL_PRIVILEGES) {
- ExtensionChild.initExtensionContext(extension.realExtension, window);
+ ExtensionPageChild.initExtensionContext(extension.realExtension, window);
} else {
throw new Error(`Unexpected window with extension ID ${extensionId}`);
}
},
// Helpers
* enumerateWindows(docShell) {
@@ -511,17 +527,17 @@ class StubExtension {
if (this._realExtension) {
this._realExtension.shutdown();
}
}
// Lazily create the real extension object when needed.
get realExtension() {
if (!this._realExtension) {
- this._realExtension = new ExtensionContent.BrowserExtensionContent(this.data);
+ this._realExtension = new ExtensionChild.BrowserExtensionContent(this.data);
}
return this._realExtension;
}
// Forward functions needed by ExtensionManagement.
hasPermission(...args) {
return this.realExtension.hasPermission(...args);
}
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -6,16 +6,17 @@
EXTRA_JS_MODULES += [
'Extension.jsm',
'ExtensionAPI.jsm',
'ExtensionChild.jsm',
'ExtensionCommon.jsm',
'ExtensionContent.jsm',
'ExtensionManagement.jsm',
+ 'ExtensionPageChild.jsm',
'ExtensionParent.jsm',
'ExtensionPermissions.jsm',
'ExtensionPreferencesManager.jsm',
'ExtensionSettingsStore.jsm',
'ExtensionStorage.jsm',
'ExtensionStorageSync.jsm',
'ExtensionTabs.jsm',
'ExtensionUtils.jsm',
--- a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html
@@ -39,17 +39,17 @@ add_task(function* test_contentscript_ca
},
});
yield extension.startup();
let origin = yield extension.awaitMessage("origin");
let scriptUrl = `${origin}/content_script.js`;
- let {ExtensionManager} = SpecialPowers.Cu.import("resource://gre/modules/ExtensionContent.jsm", {});
+ let {ExtensionManager} = SpecialPowers.Cu.import("resource://gre/modules/ExtensionChild.jsm", {});
let ext = ExtensionManager.extensions.get(extension.id);
ext.staticScripts.expiryTimeout = 3000;
is(ext.staticScripts.size, 0, "Should have no cached scripts");
let win = window.open("http://example.com/");
yield extension.awaitMessage("content-script-loaded");
@@ -59,17 +59,17 @@ add_task(function* test_contentscript_ca
}
let chromeScript, chromeScriptDone;
let {appinfo} = SpecialPowers.Services;
if (appinfo.processType === appinfo.PROCESS_TYPE_CONTENT) {
/* globals addMessageListener, assert */
chromeScript = SpecialPowers.loadChromeScript(() => {
addMessageListener("check-script-cache", extensionId => {
- let {ExtensionManager} = Components.utils.import("resource://gre/modules/ExtensionContent.jsm", {});
+ let {ExtensionManager} = Components.utils.import("resource://gre/modules/ExtensionChild.jsm", {});
let ext = ExtensionManager.extensions.get(extensionId);
if (ext) {
assert.equal(ext.staticScripts.size, 0, "Should have no cached scripts in the parent process");
}
sendAsyncMessage("done");
});