--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -29,30 +29,28 @@ Cu.import("resource://gre/modules/Servic
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, "ExtensionStorage",
"resource://gre/modules/ExtensionStorage.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
- "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestCommon",
+ "resource://testing-common/ExtensionTestCommon.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Locale",
"resource://gre/modules/Locale.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Log",
"resource://gre/modules/Log.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, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
"resource://gre/modules/Preferences.jsm");
@@ -60,39 +58,32 @@ XPCOMUtils.defineLazyModuleGetter(this,
"resource://devtools/shared/Loader.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/ExtensionContent.jsm");
Cu.import("resource://gre/modules/ExtensionManagement.jsm");
+Cu.import("resource://gre/modules/ExtensionParent.jsm");
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator");
-const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
-const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
-const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
-
-let schemaURLs = new Set();
+var {
+ GlobalManager,
+ ParentAPIManager,
+ apiManager: Management,
+} = ExtensionParent;
-if (!AppConstants.RELEASE_OR_BETA) {
- schemaURLs.add("chrome://extensions/content/schemas/experiments.json");
-}
-
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-var {
- BaseContext,
+const {
EventEmitter,
LocaleData,
- SchemaAPIManager,
- SpreadArgs,
- defineLazyGetter,
flushJarCache,
instanceOf,
} = ExtensionUtils;
XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
const LOGGER_ID_BASE = "addons.webextension.";
const UUID_MAP_PREF = "extensions.webextensions.uuids";
@@ -106,485 +97,16 @@ const COMMENT_REGEXP = new RegExp(String
[^"\n] |
" (?:[^"\\\n] | \\.)* "
)*?
)
//.*
`.replace(/\s+/g, ""), "gm");
-var GlobalManager;
-var ParentAPIManager;
-
-// This object loads the ext-*.js scripts that define the extension API.
-var Management = new class extends SchemaAPIManager {
- constructor() {
- super("main");
- this.initialized = null;
- }
-
- // Loads all the ext-*.js scripts currently registered.
- lazyInit() {
- if (this.initialized) {
- return this.initialized;
- }
-
- // Load order matters here. The base manifest defines types which are
- // extended by other schemas, so needs to be loaded first.
- let promise = Schemas.load(BASE_SCHEMA).then(() => {
- let promises = [];
- for (let [/* name */, url] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCHEMAS)) {
- promises.push(Schemas.load(url));
- }
- for (let url of schemaURLs) {
- promises.push(Schemas.load(url));
- }
- return Promise.all(promises);
- });
-
- for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS)) {
- this.loadScript(value);
- }
-
- this.initialized = promise;
- return this.initialized;
- }
-
- registerSchemaAPI(namespace, envType, getAPI) {
- if (envType == "addon_parent" || envType == "content_parent") {
- super.registerSchemaAPI(namespace, envType, getAPI);
- }
- }
-}();
-
-// Subscribes to messages related to the extension messaging API and forwards it
-// to the relevant message manager. The "sender" field for the `onMessage` and
-// `onConnect` events are updated if needed.
-let ProxyMessenger = {
- _initialized: false,
- init() {
- if (this._initialized) {
- return;
- }
- this._initialized = true;
-
- // TODO(robwu): When addons move to a separate process, we should use the
- // parent process manager(s) of the addon process(es) instead of the
- // in-process one.
- let pipmm = Services.ppmm.getChildAt(0);
- // Listen on the global frame message manager because content scripts send
- // and receive extension messages via their frame.
- // Listen on the parent process message manager because `runtime.connect`
- // and `runtime.sendMessage` requests must be delivered to all frames in an
- // addon process (by the API contract).
- // And legacy addons are not associated with a frame, so that is another
- // reason for having a parent process manager here.
- let messageManagers = [Services.mm, pipmm];
-
- MessageChannel.addListener(messageManagers, "Extension:Connect", this);
- MessageChannel.addListener(messageManagers, "Extension:Message", this);
- MessageChannel.addListener(messageManagers, "Extension:Port:Disconnect", this);
- MessageChannel.addListener(messageManagers, "Extension:Port:PostMessage", this);
- },
-
- receiveMessage({target, messageName, channelId, sender, recipient, data, responseType}) {
- if (recipient.toNativeApp) {
- let {childId, toNativeApp} = recipient;
- if (messageName == "Extension:Message") {
- let context = ParentAPIManager.getContextById(childId);
- return new NativeApp(context, toNativeApp).sendMessage(data);
- }
- if (messageName == "Extension:Connect") {
- let context = ParentAPIManager.getContextById(childId);
- NativeApp.onConnectNative(context, target.messageManager, data.portId, sender, toNativeApp);
- return true;
- }
- // "Extension:Port:Disconnect" and "Extension:Port:PostMessage" for
- // native messages are handled by NativeApp.
- return;
- }
- let extension = GlobalManager.extensionMap.get(sender.extensionId);
- let receiverMM = this._getMessageManagerForRecipient(recipient);
- if (!extension || !receiverMM) {
- return Promise.reject({
- result: MessageChannel.RESULT_NO_HANDLER,
- message: "No matching message handler for the given recipient.",
- });
- }
-
- if ((messageName == "Extension:Message" ||
- messageName == "Extension:Connect") &&
- Management.global.tabGetSender) {
- // From ext-tabs.js, undefined on Android.
- Management.global.tabGetSender(extension, target, sender);
- }
- return MessageChannel.sendMessage(receiverMM, messageName, data, {
- sender,
- recipient,
- responseType,
- });
- },
-
- /**
- * @param {object} recipient An object that was passed to
- * `MessageChannel.sendMessage`.
- * @returns {object|null} The message manager matching the recipient if found.
- */
- _getMessageManagerForRecipient(recipient) {
- let {extensionId, tabId} = recipient;
- // tabs.sendMessage / tabs.connect
- if (tabId) {
- // `tabId` being set implies that the tabs API is supported, so we don't
- // need to check whether `TabManager` exists.
- let tab = Management.global.TabManager.getTab(tabId, null, null);
- return tab && tab.linkedBrowser.messageManager;
- }
-
- // runtime.sendMessage / runtime.connect
- if (extensionId) {
- // TODO(robwu): map the extensionId to the addon parent process's message
- // manager when they run in a separate process.
- return Services.ppmm.getChildAt(0);
- }
-
- return null;
- },
-};
-
-class BrowserDocshellFollower {
- /**
- * Follows the <browser> belonging to the `xulBrowser`'s current docshell.
- *
- * @param {XULElement} xulBrowser A <browser> tag.
- * @param {function} onBrowserChange Called when the <browser> changes.
- */
- constructor(xulBrowser, onBrowserChange) {
- this.xulBrowser = xulBrowser;
- this.onBrowserChange = onBrowserChange;
-
- xulBrowser.addEventListener("SwapDocShells", this);
- }
-
- destroy() {
- this.xulBrowser.removeEventListener("SwapDocShells", this);
- this.xulBrowser = null;
- }
-
- handleEvent({detail: otherBrowser}) {
- this.xulBrowser.removeEventListener("SwapDocShells", this);
- this.xulBrowser = otherBrowser;
- this.xulBrowser.addEventListener("SwapDocShells", this);
- this.onBrowserChange(otherBrowser);
- }
-}
-
-class ProxyContext extends BaseContext {
- constructor(envType, extension, params, xulBrowser, principal) {
- super(envType, extension);
-
- this.uri = NetUtil.newURI(params.url);
-
- this.incognito = params.incognito;
-
- // 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));
-
- Object.defineProperty(this, "principal", {
- value: principal, enumerable: true, configurable: true,
- });
-
- this.listenerProxies = new Map();
-
- Management.emit("proxy-context-load", this);
- }
-
- 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.
- if (!browser.messageManager) {
- throw new Error("BrowserDocshellFollower: The new browser has no message manager");
- }
-
- this.currentMessageManager = browser.messageManager;
- }
-
- shutdown() {
- this.unload();
- }
-
- unload() {
- if (this.unloaded) {
- 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.
- this.xulBrowser = xulBrowser;
- }
-
- // The window that contains this context. This may change due to moving tabs.
- get xulWindow() {
- return this.xulBrowser.ownerGlobal;
- }
-
- get windowId() {
- if (!Management.global.WindowManager || this.viewType == "background") {
- return;
- }
- // viewType popup or tab:
- return Management.global.WindowManager.getId(this.xulWindow);
- }
-
- get tabId() {
- if (!Management.global.TabManager) {
- return; // Not yet supported on Android.
- }
- let {gBrowser} = this.xulBrowser.ownerGlobal;
- let tab = gBrowser && gBrowser.getTabForBrowser(this.xulBrowser);
- return tab && Management.global.TabManager.getId(tab);
- }
-
- onBrowserChange(browser) {
- super.onBrowserChange(browser);
- this.xulBrowser = browser;
- }
-
- shutdown() {
- Management.emit("page-shutdown", this);
- super.shutdown();
- }
-}
-
-function findPathInObject(obj, path, printErrors = true) {
- let parent;
- for (let elt of path.split(".")) {
- if (!obj || !(elt in obj)) {
- if (printErrors) {
- Cu.reportError(`WebExtension API ${path} not found (it may be unimplemented by Firefox).`);
- }
- return null;
- }
-
- parent = obj;
- obj = obj[elt];
- }
-
- if (typeof obj === "function") {
- return obj.bind(parent);
- }
- return obj;
-}
-
-ParentAPIManager = {
- proxyContexts: new Map(),
-
- init() {
- Services.obs.addObserver(this, "message-manager-close", false);
-
- Services.mm.addMessageListener("API:CreateProxyContext", this);
- Services.mm.addMessageListener("API:CloseProxyContext", this, true);
- Services.mm.addMessageListener("API:Call", this);
- Services.mm.addMessageListener("API:AddListener", this);
- Services.mm.addMessageListener("API:RemoveListener", this);
- },
-
- // "message-manager-close" observer.
- observe(subject, topic, data) {
- let mm = subject;
- for (let [childId, context] of this.proxyContexts) {
- if (context.currentMessageManager == mm) {
- this.closeProxyContext(childId);
- }
- }
- },
-
- shutdownExtension(extensionId) {
- for (let [childId, context] of this.proxyContexts) {
- if (context.extension.id == extensionId) {
- context.shutdown();
- this.proxyContexts.delete(childId);
- }
- }
- },
-
- receiveMessage({name, data, target}) {
- switch (name) {
- case "API:CreateProxyContext":
- this.createProxyContext(data, target);
- break;
-
- case "API:CloseProxyContext":
- this.closeProxyContext(data.childId);
- break;
-
- case "API:Call":
- this.call(data, target);
- break;
-
- case "API:AddListener":
- this.addListener(data, target);
- break;
-
- case "API:RemoveListener":
- this.removeListener(data);
- break;
- }
- },
-
- createProxyContext(data, target) {
- let {envType, extensionId, childId, principal} = data;
- if (this.proxyContexts.has(childId)) {
- throw new Error("A WebExtension context with the given ID already exists!");
- }
-
- let extension = GlobalManager.getExtension(extensionId);
- if (!extension) {
- throw new Error(`No WebExtension found with ID ${extensionId}`);
- }
-
- let context;
- if (envType == "addon_parent") {
- // Privileged addon contexts can only be loaded in documents whose main
- // frame is also the same addon.
- if (principal.URI.prePath != extension.baseURI.prePath ||
- !target.contentPrincipal.subsumes(principal)) {
- throw new Error(`Refused to create privileged WebExtension context for ${principal.URI.spec}`);
- }
- context = new ExtensionChildProxyContext(envType, extension, data, target);
- } else if (envType == "content_parent") {
- context = new ProxyContext(envType, extension, data, target, principal);
- } else {
- throw new Error(`Invalid WebExtension context envType: ${envType}`);
- }
- this.proxyContexts.set(childId, context);
- },
-
- closeProxyContext(childId) {
- let context = this.proxyContexts.get(childId);
- if (!context) {
- return;
- }
- context.unload();
- this.proxyContexts.delete(childId);
- },
-
- call(data, target) {
- let context = this.getContextById(data.childId);
- if (context.currentMessageManager !== target.messageManager) {
- Cu.reportError("WebExtension warning: Message manager unexpectedly changed");
- }
-
- try {
- let args = Cu.cloneInto(data.args, context.sandbox);
- let result = findPathInObject(context.apiObj, data.path)(...args);
-
- if (data.callId) {
- result = result || Promise.resolve();
-
- result.then(result => {
- result = result instanceof SpreadArgs ? [...result] : [result];
-
- context.currentMessageManager.sendAsyncMessage("API:CallResult", {
- childId: data.childId,
- callId: data.callId,
- result,
- });
- }, error => {
- error = context.normalizeError(error);
- context.currentMessageManager.sendAsyncMessage("API:CallResult", {
- childId: data.childId,
- callId: data.callId,
- error: {message: error.message},
- });
- });
- }
- } catch (e) {
- if (data.callId) {
- let error = context.normalizeError(e);
- context.currentMessageManager.sendAsyncMessage("API:CallResult", {
- childId: data.childId,
- callId: data.callId,
- error: {message: error.message},
- });
- } else {
- Cu.reportError(e);
- }
- }
- },
-
- addListener(data, target) {
- let context = this.getContextById(data.childId);
- if (context.currentMessageManager !== target.messageManager) {
- Cu.reportError("WebExtension warning: Message manager unexpectedly changed");
- }
-
- function listener(...listenerArgs) {
- context.currentMessageManager.sendAsyncMessage("API:RunListener", {
- childId: data.childId,
- path: data.path,
- args: listenerArgs,
- });
- }
-
- context.listenerProxies.set(data.path, listener);
-
- let args = Cu.cloneInto(data.args, context.sandbox);
- findPathInObject(context.apiObj, data.path).addListener(listener, ...args);
- },
-
- removeListener(data) {
- let context = this.getContextById(data.childId);
- let listener = context.listenerProxies.get(data.path);
- findPathInObject(context.apiObj, data.path).removeListener(listener);
- },
-
- getContextById(childId) {
- let context = this.proxyContexts.get(childId);
- if (!context) {
- let error = new Error("WebExtension context not found!");
- Cu.reportError(error);
- throw error;
- }
- return context;
- },
-};
-
-ParentAPIManager.init();
-
// All moz-extension URIs use a machine-specific UUID rather than the
// extension's own ID in the host component. This makes it more
// difficult for web pages to detect whether a user has a given add-on
// installed (by trying to load a moz-extension URI referring to a
// web_accessible_resource from the extension). UUIDMap.get()
// returns the UUID for a given add-on ID.
var UUIDMap = {
_read() {
@@ -680,62 +202,17 @@ var UninstallObserver = {
if (!this.leaveUuid) {
// Clear the entry in the UUID map
UUIDMap.remove(addon.id);
}
},
};
-// Responsible for loading extension APIs into the right globals.
-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) {
- UninstallObserver.init();
- ProxyMessenger.init();
- 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) {
- Management.off("extension-browser-inserted", this._onExtensionBrowser);
- this.initialized = false;
- }
- },
-
- _onExtensionBrowser(type, browser) {
- browser.messageManager.loadFrameScript(`data:,
- Components.utils.import("resource://gre/modules/ExtensionContent.jsm");
- ExtensionContent.init(this);
- addEventListener("unload", function() {
- ExtensionContent.uninit(this);
- });
- `, false);
- },
-
- getExtension(extensionId) {
- return this.extensionMap.get(extensionId);
- },
-
- injectInObject(context, isChromeCompat, dest) {
- Management.generateAPIs(context, dest);
- SchemaAPIManager.generateAPIs(context, context.extension.apis, dest);
- },
-};
+UninstallObserver.init();
// 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.
//
// No functionality of this class is guaranteed to work before
@@ -1082,105 +559,16 @@ this.ExtensionData = class {
let results = yield Promise.all(promises);
this.localeData.selectedLocale = locale;
return results[0];
}.bind(this));
}
};
-
-/**
- * A skeleton Extension-like object, used for testing, which installs an
- * add-on via the add-on manager when startup() is called, and
- * uninstalles it on shutdown().
- *
- * @param {string} id
- * @param {nsIFile} file
- * @param {nsIURI} rootURI
- * @param {string} installType
- */
-class MockExtension {
- constructor(file, rootURI, installType) {
- this.id = null;
- this.file = file;
- this.rootURI = rootURI;
- this.installType = installType;
- this.addon = null;
-
- let promiseEvent = eventName => new Promise(resolve => {
- let onstartup = (msg, extension) => {
- if (this.addon && extension.id == this.addon.id) {
- Management.off(eventName, onstartup);
-
- this.id = extension.id;
- this._extension = extension;
- resolve(extension);
- }
- };
- Management.on(eventName, onstartup);
- });
-
- this._extension = null;
- this._extensionPromise = promiseEvent("startup");
- this._readyPromise = promiseEvent("ready");
- }
-
- testMessage(...args) {
- return this._extension.testMessage(...args);
- }
-
- on(...args) {
- this._extensionPromise.then(extension => {
- extension.on(...args);
- });
- }
-
- off(...args) {
- this._extensionPromise.then(extension => {
- extension.off(...args);
- });
- }
-
- startup() {
- if (this.installType == "temporary") {
- return AddonManager.installTemporaryAddon(this.file).then(addon => {
- this.addon = addon;
- return this._readyPromise;
- });
- } else if (this.installType == "permanent") {
- return new Promise((resolve, reject) => {
- AddonManager.getInstallForFile(this.file, install => {
- let listener = {
- onInstallFailed: reject,
- onInstallEnded: (install, newAddon) => {
- this.addon = newAddon;
- resolve(this._readyPromise);
- },
- };
-
- install.addListener(listener);
- install.install();
- });
- });
- }
- throw new Error("installType must be one of: temporary, permanent");
- }
-
- shutdown() {
- this.addon.uninstall();
- return this.cleanupGeneratedFile();
- }
-
- cleanupGeneratedFile() {
- flushJarCache(this.file);
- return OS.File.remove(this.file.path);
- }
-}
-
let _browserUpdated = false;
// We create one instance of this class per extension. |addonData|
// comes directly from bootstrap.js when initializing.
this.Extension = class extends ExtensionData {
constructor(addonData, startupReason) {
super(addonData.resourceURI);
@@ -1216,217 +604,26 @@ this.Extension = class extends Extension
static set browserUpdated(updated) {
_browserUpdated = updated;
}
static get browserUpdated() {
return _browserUpdated;
}
- /**
- * This code is designed to make it easy to test a WebExtension
- * without creating a bunch of files. Everything is contained in a
- * single JSON blob.
- *
- * Properties:
- * "background": "<JS code>"
- * A script to be loaded as the background script.
- * The "background" section of the "manifest" property is overwritten
- * if this is provided.
- * "manifest": {...}
- * Contents of manifest.json
- * "files": {"filename1": "contents1", ...}
- * Data to be included as files. Can be referenced from the manifest.
- * If a manifest file is provided here, it takes precedence over
- * a generated one. Always use "/" as a directory separator.
- * Directories should appear here only implicitly (as a prefix
- * to file names)
- *
- * To make things easier, the value of "background" and "files"[] can
- * be a function, which is converted to source that is run.
- *
- * The generated extension is stored in the system temporary directory,
- * and an nsIFile object pointing to it is returned.
- *
- * @param {object} data
- * @returns {nsIFile}
- */
static generateXPI(data) {
- let manifest = data.manifest;
- if (!manifest) {
- manifest = {};
- }
-
- let files = data.files;
- if (!files) {
- files = {};
- }
-
- function provide(obj, keys, value, override = false) {
- if (keys.length == 1) {
- if (!(keys[0] in obj) || override) {
- obj[keys[0]] = value;
- }
- } else {
- if (!(keys[0] in obj)) {
- obj[keys[0]] = {};
- }
- provide(obj[keys[0]], keys.slice(1), value, override);
- }
- }
-
- provide(manifest, ["name"], "Generated extension");
- provide(manifest, ["manifest_version"], 2);
- provide(manifest, ["version"], "1.0");
-
- if (data.background) {
- let bgScript = uuidGen.generateUUID().number + ".js";
-
- provide(manifest, ["background", "scripts"], [bgScript], true);
- files[bgScript] = data.background;
- }
-
- provide(files, ["manifest.json"], manifest);
-
- if (data.embedded) {
- // Package this as a webextension embedded inside a legacy
- // extension.
-
- let xpiFiles = {
- "install.rdf": `<?xml version="1.0" encoding="UTF-8"?>
- <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:em="http://www.mozilla.org/2004/em-rdf#">
- <Description about="urn:mozilla:install-manifest"
- em:id="${manifest.applications.gecko.id}"
- em:name="${manifest.name}"
- em:type="2"
- em:version="${manifest.version}"
- em:description=""
- em:hasEmbeddedWebExtension="true"
- em:bootstrap="true">
-
- <!-- Firefox -->
- <em:targetApplication>
- <Description
- em:id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"
- em:minVersion="51.0a1"
- em:maxVersion="*"/>
- </em:targetApplication>
- </Description>
- </RDF>
- `,
-
- "bootstrap.js": `
- function install() {}
- function uninstall() {}
- function shutdown() {}
-
- function startup(data) {
- data.webExtension.startup();
- }
- `,
- };
-
- for (let [path, data] of Object.entries(files)) {
- xpiFiles[`webextension/${path}`] = data;
- }
-
- files = xpiFiles;
- }
-
- return this.generateZipFile(files);
+ return ExtensionTestCommon.generateXPI(data);
}
static generateZipFile(files, baseName = "generated-extension.xpi") {
- let ZipWriter = Components.Constructor("@mozilla.org/zipwriter;1", "nsIZipWriter");
- let zipW = new ZipWriter();
-
- let file = FileUtils.getFile("TmpD", [baseName]);
- file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
-
- const MODE_WRONLY = 0x02;
- const MODE_TRUNCATE = 0x20;
- zipW.open(file, MODE_WRONLY | MODE_TRUNCATE);
-
- // Needs to be in microseconds for some reason.
- let time = Date.now() * 1000;
-
- function generateFile(filename) {
- let components = filename.split("/");
- let path = "";
- for (let component of components.slice(0, -1)) {
- path += component + "/";
- if (!zipW.hasEntry(path)) {
- zipW.addEntryDirectory(path, time, false);
- }
- }
- }
-
- for (let filename in files) {
- let script = files[filename];
- if (typeof(script) == "function") {
- script = "(" + script.toString() + ")()";
- } else if (instanceOf(script, "Object") || instanceOf(script, "Array")) {
- script = JSON.stringify(script);
- }
-
- if (!instanceOf(script, "ArrayBuffer")) {
- script = new TextEncoder("utf-8").encode(script).buffer;
- }
-
- let stream = Cc["@mozilla.org/io/arraybuffer-input-stream;1"].createInstance(Ci.nsIArrayBufferInputStream);
- stream.setData(script, 0, script.byteLength);
-
- generateFile(filename);
- zipW.addEntryStream(filename, time, 0, stream, false);
- }
-
- zipW.close();
-
- return file;
+ return ExtensionTestCommon.generateZipFile(files, baseName);
}
- /**
- * Generates a new extension using |Extension.generateXPI|, and initializes a
- * new |Extension| instance which will execute it.
- *
- * @param {object} data
- * @returns {Extension}
- */
static generate(data) {
- let file = this.generateXPI(data);
-
- flushJarCache(file);
- Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", {path: file.path});
-
- let fileURI = Services.io.newFileURI(file);
- let jarURI = Services.io.newURI("jar:" + fileURI.spec + "!/", null, null);
-
- // This may be "temporary" or "permanent".
- if (data.useAddonManager) {
- return new MockExtension(file, jarURI, data.useAddonManager);
- }
-
- let id;
- if (data.manifest) {
- if (data.manifest.applications && data.manifest.applications.gecko) {
- id = data.manifest.applications.gecko.id;
- } else if (data.manifest.browser_specific_settings && data.manifest.browser_specific_settings.gecko) {
- id = data.manifest.browser_specific_settings.gecko.id;
- }
- }
- if (!id) {
- id = uuidGen.generateUUID().number;
- }
-
- return new Extension({
- id,
- resourceURI: jarURI,
- cleanupFile: file,
- });
+ return ExtensionTestCommon.generate(data);
}
on(hook, f) {
return this.emitter.on(hook, f);
}
off(hook, f) {
return this.emitter.off(hook, f);
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -14,44 +14,414 @@ this.EXPORTED_SYMBOLS = ["ExtensionChild
* Don't put contentscript logic here, use ExtensionContent.jsm instead.
*/
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");
-Cu.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent",
+ "resource://gre/modules/ExtensionParent.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
"resource://gre/modules/MessageChannel.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
+ "resource://gre/modules/PromiseUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
+XPCOMUtils.defineLazyGetter(this, "ParentAPIManager",
+ () => ExtensionParent.ParentAPIManager);
+
const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon";
+Cu.import("resource://gre/modules/ExtensionCommon.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-var {
+
+const {
+ EventManager,
+ SingletonEventManager,
+ SpreadArgs,
+ defineLazyGetter,
+ findPathInObject,
getInnerWindowID,
- BaseContext,
- ChildAPIManager,
- defineLazyGetter,
- LocalAPIImplementation,
- Messenger,
- SchemaAPIManager,
+ getMessageManager,
+ injectAPI,
} = 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, "ParentAPIManager",
- () => Cu.import("resource://gre/modules/Extension.jsm", {}).ParentAPIManager);
+const {
+ BaseContext,
+ LocalAPIImplementation,
+ SchemaAPIInterface,
+ SchemaAPIManager,
+} = ExtensionCommon;
+
+var ExtensionChild;
+
+let gNextPortId = 1;
+
+/**
+ * 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 EventManager(this.context, "Port.onDisconnect", fire => {
+ return this.registerOnDisconnect(error => {
+ portError = error && this.context.normalizeError(error);
+ fire.withoutClone(portObj);
+ });
+ }).api(),
+
+ onMessage: new EventManager(this.context, "Port.onMessage", fire => {
+ return this.registerOnMessage(msg => {
+ msg = Cu.cloneInto(msg, this.context.cloneScope);
+ fire.withoutClone(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);
+ }
+
+ close() {
+ this.disconnect();
+ }
+}
+
+/**
+ * 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);
+ }
+
+ onMessage(name) {
+ return new SingletonEventManager(this.context, name, callback => {
+ 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;
+ },
+
+ 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 = callback(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();
+ }
+
+ connectGetRawPort(messageManager, name, recipient) {
+ let portId = `${gNextPortId++}-${Services.appinfo.uniqueProcessID}`;
+ let port = new Port(this.context, messageManager, this.messageManagers, name, portId, null, recipient);
+ let msg = {name, portId};
+ this._sendMessage(messageManager, "Extension:Connect", msg, recipient)
+ .catch(e => {
+ if (e.result === MessageChannel.RESULT_NO_HANDLER) {
+ e = {message: "Could not establish connection. Receiving end does not exist."};
+ } else if (e.result === MessageChannel.RESULT_DISCONNECTED) {
+ e = null;
+ }
+ port.disconnectByOtherEnd(e);
+ });
+ return port;
+ }
+
+ connect(messageManager, name, recipient) {
+ let port = this.connectGetRawPort(messageManager, name, recipient);
+ return port.api();
+ }
+
+ onConnect(name) {
+ return new SingletonEventManager(this.context, name, callback => {
+ 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;
+ },
+
+ 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);
+ this.context.runSafeWithoutClone(callback, port.api());
+ return true;
+ },
+ };
+
+ MessageChannel.addListener(this.messageManagers, "Extension:Connect", listener);
+ return () => {
+ MessageChannel.removeListener(this.messageManagers, "Extension:Connect", listener);
+ };
+ }).api();
+ }
+}
var apiManager = new class extends SchemaAPIManager {
constructor() {
super("addon");
this.initialized = false;
}
generateAPIs(...args) {
@@ -66,16 +436,262 @@ var apiManager = new class extends Schem
registerSchemaAPI(namespace, envType, getAPI) {
if (envType == "addon_child") {
super.registerSchemaAPI(namespace, envType, getAPI);
}
}
}();
+/**
+ * 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;
+ }
+
+ callFunctionNoReturn(args) {
+ this.childApiManager.callParentFunctionNoReturn(this.path, args);
+ }
+
+ callAsyncFunction(args, callback) {
+ return this.childApiManager.callParentAsyncFunction(this.path, args, callback);
+ }
+
+ addListener(listener, args) {
+ let set = this.childApiManager.listeners.get(this.path);
+ if (!set) {
+ set = new Set();
+ this.childApiManager.listeners.set(this.path, set);
+ }
+
+ set.add(listener);
+
+ if (set.size == 1) {
+ args = args.slice(1);
+
+ this.childApiManager.messageManager.sendAsyncMessage("API:AddListener", {
+ childId: this.childApiManager.id,
+ path: this.path,
+ args,
+ });
+ }
+ }
+
+ removeListener(listener) {
+ let set = this.childApiManager.listeners.get(this.path);
+ if (!set) {
+ return;
+ }
+ set.delete(listener);
+
+ if (set.size == 0) {
+ this.childApiManager.messageManager.sendAsyncMessage("API:RemoveListener", {
+ childId: this.childApiManager.id,
+ path: this.path,
+ });
+ }
+ }
+
+ hasListener(listener) {
+ let set = this.childApiManager.listeners.get(this.path);
+ return set ? set.has(listener) : false;
+ }
+}
+
+let nextId = 1;
+
+// 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
+// Extension.jsm. It handles asynchronous function calls as well as
+// event listeners.
+class ChildAPIManager {
+ constructor(context, messageManager, localApis, contextData) {
+ this.context = context;
+ this.messageManager = messageManager;
+
+ // 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 = localApis;
+
+ let id = String(context.extension.id) + "." + String(context.contextId);
+ this.id = id;
+
+ let data = {childId: id, extensionId: context.extension.id, principal: context.principal};
+ Object.assign(data, contextData);
+
+ messageManager.addMessageListener("API:RunListener", this);
+ messageManager.addMessageListener("API:CallResult", this);
+
+ // Map[path -> Set[listener]]
+ // path is, e.g., "runtime.onMessage".
+ this.listeners = new Map();
+
+ // Map[callId -> Deferred]
+ this.callPromises = new Map();
+
+ this.createProxyContextInConstructor(data);
+ }
+
+ createProxyContextInConstructor(data) {
+ this.messageManager.sendAsyncMessage("API:CreateProxyContext", data);
+ }
+
+ receiveMessage({name, data}) {
+ if (data.childId != this.id) {
+ return;
+ }
+
+ switch (name) {
+ case "API:RunListener":
+ let listeners = this.listeners.get(data.path);
+ for (let callback of listeners) {
+ this.context.runSafe(callback, ...data.args);
+ }
+ 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 = nextId++;
+ 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) {
+ let parsed = /^(.+)\.(on[A-Z][^.]+)$/.exec(path);
+ if (!parsed) {
+ throw new Error("getParentEvent: Invalid event name: " + path);
+ }
+ let [, namespace, name] = parsed;
+ 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});
+ }
+
+ 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;
+ }
+ return true;
+ }
+
+ getImplementation(namespace, name) {
+ let pathObj = this.localApis;
+ if (pathObj) {
+ for (let part of namespace.split(".")) {
+ pathObj = pathObj[part];
+ if (!pathObj) {
+ break;
+ }
+ }
+ if (pathObj && name in pathObj) {
+ return new LocalAPIImplementation(pathObj, 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);
+ }
+}
+
+
// A class that behaves identical to a ChildAPIManager, except
// 1) creation of the ProxyContext in the parent is synchronous, and
// 2) APIs without a local implementation and marked as incompatible with the
// out-of-process model fall back to directly invoking the parent methods.
// TODO(robwu): Remove this when all APIs have migrated.
class PseudoChildAPIManager extends ChildAPIManager {
createProxyContextInConstructor(originalData) {
// Create a structured clone to simulate IPC.
@@ -356,17 +972,17 @@ class ContentGlobal {
return;
}
this.global.removeEventListener("DOMContentLoaded", this);
this.global.sendAsyncMessage("Extension:ExtensionViewLoaded");
}
}
}
-this.ExtensionChild = {
+ExtensionChild = {
// Map<nsIContentFrameMessageManager, ContentGlobal>
contentGlobals: new Map(),
// Map<innerWindowId, ExtensionContext>
extensionContexts: new Map(),
initOnce() {
// This initializes the default message handler for messages targeted at
@@ -441,12 +1057,20 @@ this.ExtensionChild = {
}
},
};
// 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] = () => {};
}
});
}
+
+Object.assign(ExtensionChild, {
+ ChildAPIManager,
+ Messenger,
+ Port,
+});
+
copy from toolkit/components/extensions/ExtensionUtils.jsm
copy to toolkit/components/extensions/ExtensionCommon.jsm
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -1,193 +1,45 @@
/* 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 = ["ExtensionUtils"];
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
-const Ci = Components.interfaces;
-const Cc = Components.classes;
-const Cu = Components.utils;
-const Cr = Components.results;
+/* exported ExtensionCommon */
-const INTEGER = /^[1-9]\d*$/;
+this.EXPORTED_SYMBOLS = ["ExtensionCommon"];
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
- "resource://gre/modules/AddonManager.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
- "resource://gre/modules/AppConstants.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPI",
- "resource://gre/modules/Console.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
- "resource:///modules/translation/LanguageDetector.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Locale",
- "resource://gre/modules/Locale.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
"resource://gre/modules/MessageChannel.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
- "resource://gre/modules/NetUtil.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
- "resource://gre/modules/Preferences.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
- "resource://gre/modules/PromiseUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
-XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
- "@mozilla.org/content/style-sheet-service;1",
- "nsIStyleSheetService");
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-function getConsole() {
- return new ConsoleAPI({
- maxLogLevelPref: "extensions.webextensions.log.level",
- prefix: "WebExtensions",
- });
-}
+var {
+ EventEmitter,
+ ExtensionError,
+ SpreadArgs,
+ getConsole,
+ getInnerWindowID,
+ runSafeSync,
+ runSafeSyncWithoutClone,
+ instanceOf,
+} = ExtensionUtils;
XPCOMUtils.defineLazyGetter(this, "console", getConsole);
-/**
- * An Error subclass for which complete error messages are always passed
- * to extensions, rather than being interpreted as an unknown error.
- */
-class ExtensionError extends Error {}
-
-function filterStack(error) {
- return String(error.stack).replace(/(^.*(Task\.jsm|Promise-backend\.js).*\n)+/gm, "<Promise Chain>\n");
-}
-
-// Run a function and report exceptions.
-function runSafeSyncWithoutClone(f, ...args) {
- try {
- return f(...args);
- } catch (e) {
- dump(`Extension error: ${e} ${e.fileName} ${e.lineNumber}\n[[Exception stack\n${filterStack(e)}Current stack\n${filterStack(Error())}]]\n`);
- Cu.reportError(e);
- }
-}
-
-// Run a function and report exceptions.
-function runSafeWithoutClone(f, ...args) {
- if (typeof(f) != "function") {
- dump(`Extension error: expected function\n${filterStack(Error())}`);
- return;
- }
-
- Promise.resolve().then(() => {
- runSafeSyncWithoutClone(f, ...args);
- });
-}
-
-// Run a function, cloning arguments into context.cloneScope, and
-// report exceptions. |f| is expected to be in context.cloneScope.
-function runSafeSync(context, f, ...args) {
- if (context.unloaded) {
- Cu.reportError("runSafeSync called after context unloaded");
- return;
- }
-
- try {
- args = Cu.cloneInto(args, context.cloneScope);
- } catch (e) {
- Cu.reportError(e);
- dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${filterStack(Error())}`);
- }
- return runSafeSyncWithoutClone(f, ...args);
-}
-
-// Run a function, cloning arguments into context.cloneScope, and
-// report exceptions. |f| is expected to be in context.cloneScope.
-function runSafe(context, f, ...args) {
- try {
- args = Cu.cloneInto(args, context.cloneScope);
- } catch (e) {
- Cu.reportError(e);
- dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${filterStack(Error())}`);
- }
- if (context.unloaded) {
- dump(`runSafe failure: context is already unloaded ${filterStack(new Error())}\n`);
- return undefined;
- }
- return runSafeWithoutClone(f, ...args);
-}
-
-function getInnerWindowID(window) {
- return window.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDOMWindowUtils)
- .currentInnerWindowID;
-}
-
-// Return true if the given value is an instance of the given
-// native type.
-function instanceOf(value, type) {
- return {}.toString.call(value) == `[object ${type}]`;
-}
-
-// Extend the object |obj| with the property descriptors of each object in
-// |args|.
-function extend(obj, ...args) {
- for (let arg of args) {
- let props = [...Object.getOwnPropertyNames(arg),
- ...Object.getOwnPropertySymbols(arg)];
- for (let prop of props) {
- let descriptor = Object.getOwnPropertyDescriptor(arg, prop);
- Object.defineProperty(obj, prop, descriptor);
- }
- }
-
- return obj;
-}
-
-/**
- * Similar to a WeakMap, but creates a new key with the given
- * constructor if one is not present.
- */
-class DefaultWeakMap extends WeakMap {
- constructor(defaultConstructor, init) {
- super(init);
- this.defaultConstructor = defaultConstructor;
- }
-
- get(key) {
- if (!this.has(key)) {
- this.set(key, this.defaultConstructor());
- }
- return super.get(key);
- }
-}
-
-class DefaultMap extends Map {
- constructor(defaultConstructor, init) {
- super(init);
- this.defaultConstructor = defaultConstructor;
- }
-
- get(key) {
- if (!this.has(key)) {
- this.set(key, this.defaultConstructor(key));
- }
- return super.get(key);
- }
-}
-
-class SpreadArgs extends Array {
- constructor(args) {
- super();
- this.push(...args);
- }
-}
-
let gContextId = 0;
class BaseContext {
constructor(envType, extension) {
this.envType = envType;
this.onClose = new Set();
this.checkedLastError = false;
this._lastError = null;
@@ -512,1112 +364,16 @@ class BaseContext {
/**
* A simple proxy for unload(), for use with callOnClose().
*/
close() {
this.unload();
}
}
-// Manages icon details for toolbar buttons in the |pageAction| and
-// |browserAction| APIs.
-let IconDetails = {
- // Normalizes the various acceptable input formats into an object
- // with icon size as key and icon URL as value.
- //
- // If a context is specified (function is called from an extension):
- // Throws an error if an invalid icon size was provided or the
- // extension is not allowed to load the specified resources.
- //
- // If no context is specified, instead of throwing an error, this
- // function simply logs a warning message.
- normalize(details, extension, context = null) {
- let result = {};
-
- try {
- if (details.imageData) {
- let imageData = details.imageData;
-
- if (typeof imageData == "string") {
- imageData = {"19": imageData};
- }
-
- for (let size of Object.keys(imageData)) {
- if (!INTEGER.test(size)) {
- throw new ExtensionError(`Invalid icon size ${size}, must be an integer`);
- }
- result[size] = imageData[size];
- }
- }
-
- if (details.path) {
- let path = details.path;
- if (typeof path != "object") {
- path = {"19": path};
- }
-
- let baseURI = context ? context.uri : extension.baseURI;
-
- for (let size of Object.keys(path)) {
- if (!INTEGER.test(size)) {
- throw new ExtensionError(`Invalid icon size ${size}, must be an integer`);
- }
-
- let url = baseURI.resolve(path[size]);
-
- // The Chrome documentation specifies these parameters as
- // relative paths. We currently accept absolute URLs as well,
- // which means we need to check that the extension is allowed
- // to load them. This will throw an error if it's not allowed.
- try {
- Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
- extension.principal, url,
- Services.scriptSecurityManager.DISALLOW_SCRIPT);
- } catch (e) {
- throw new ExtensionError(`Illegal URL ${url}`);
- }
-
- result[size] = url;
- }
- }
- } catch (e) {
- // Function is called from extension code, delegate error.
- if (context) {
- throw e;
- }
- // If there's no context, it's because we're handling this
- // as a manifest directive. Log a warning rather than
- // raising an error.
- extension.manifestError(`Invalid icon data: ${e}`);
- }
-
- return result;
- },
-
- // Returns the appropriate icon URL for the given icons object and the
- // screen resolution of the given window.
- getPreferredIcon(icons, extension = null, size = 16) {
- const DEFAULT = "chrome://browser/content/extension.svg";
-
- let bestSize = null;
- if (icons[size]) {
- bestSize = size;
- } else if (icons[2 * size]) {
- bestSize = 2 * size;
- } else {
- let sizes = Object.keys(icons)
- .map(key => parseInt(key, 10))
- .sort((a, b) => a - b);
-
- bestSize = sizes.find(candidate => candidate > size) || sizes.pop();
- }
-
- if (bestSize) {
- return {size: bestSize, icon: icons[bestSize]};
- }
-
- return {size, icon: DEFAULT};
- },
-
- convertImageURLToDataURL(imageURL, contentWindow, browserWindow, size = 18) {
- return new Promise((resolve, reject) => {
- let image = new contentWindow.Image();
- image.onload = function() {
- let canvas = contentWindow.document.createElement("canvas");
- let ctx = canvas.getContext("2d");
- let dSize = size * browserWindow.devicePixelRatio;
-
- // Scales the image while maintaing width to height ratio.
- // If the width and height differ, the image is centered using the
- // smaller of the two dimensions.
- let dWidth, dHeight, dx, dy;
- if (this.width > this.height) {
- dWidth = dSize;
- dHeight = image.height * (dSize / image.width);
- dx = 0;
- dy = (dSize - dHeight) / 2;
- } else {
- dWidth = image.width * (dSize / image.height);
- dHeight = dSize;
- dx = (dSize - dWidth) / 2;
- dy = 0;
- }
-
- ctx.drawImage(this, 0, 0, this.width, this.height, dx, dy, dWidth, dHeight);
- resolve(canvas.toDataURL("image/png"));
- };
- image.onerror = reject;
- image.src = imageURL;
- });
- },
-};
-
-const LISTENERS = Symbol("listeners");
-
-class EventEmitter {
- constructor() {
- this[LISTENERS] = new Map();
- }
-
- /**
- * Adds the given function as a listener for the given event.
- *
- * The listener function may optionally return a Promise which
- * resolves when it has completed all operations which event
- * dispatchers may need to block on.
- *
- * @param {string} event
- * The name of the event to listen for.
- * @param {function(string, ...any)} listener
- * The listener to call when events are emitted.
- */
- on(event, listener) {
- if (!this[LISTENERS].has(event)) {
- this[LISTENERS].set(event, new Set());
- }
-
- this[LISTENERS].get(event).add(listener);
- }
-
- /**
- * Removes the given function as a listener for the given event.
- *
- * @param {string} event
- * The name of the event to stop listening for.
- * @param {function(string, ...any)} listener
- * The listener function to remove.
- */
- off(event, listener) {
- if (this[LISTENERS].has(event)) {
- let set = this[LISTENERS].get(event);
-
- set.delete(listener);
- if (!set.size) {
- this[LISTENERS].delete(event);
- }
- }
- }
-
- /**
- * Triggers all listeners for the given event, and returns a promise
- * which resolves when all listeners have been called, and any
- * promises they have returned have likewise resolved.
- *
- * @param {string} event
- * The name of the event to emit.
- * @param {any} args
- * Arbitrary arguments to pass to the listener functions, after
- * the event name.
- * @returns {Promise}
- */
- emit(event, ...args) {
- let listeners = this[LISTENERS].get(event) || new Set();
-
- let promises = Array.from(listeners, listener => {
- return runSafeSyncWithoutClone(listener, event, ...args);
- });
-
- return Promise.all(promises);
- }
-}
-
-function LocaleData(data) {
- this.defaultLocale = data.defaultLocale;
- this.selectedLocale = data.selectedLocale;
- this.locales = data.locales || new Map();
- this.warnedMissingKeys = new Set();
-
- // Map(locale-name -> Map(message-key -> localized-string))
- //
- // Contains a key for each loaded locale, each of which is a
- // Map of message keys to their localized strings.
- this.messages = data.messages || new Map();
-
- if (data.builtinMessages) {
- this.messages.set(this.BUILTIN, data.builtinMessages);
- }
-}
-
-
-LocaleData.prototype = {
- // Representation of the object to send to content processes. This
- // should include anything the content process might need.
- serialize() {
- return {
- defaultLocale: this.defaultLocale,
- selectedLocale: this.selectedLocale,
- messages: this.messages,
- locales: this.locales,
- };
- },
-
- BUILTIN: "@@BUILTIN_MESSAGES",
-
- has(locale) {
- return this.messages.has(locale);
- },
-
- // https://developer.chrome.com/extensions/i18n
- localizeMessage(message, substitutions = [], options = {}) {
- let defaultOptions = {
- locale: this.selectedLocale,
- defaultValue: "",
- cloneScope: null,
- };
-
- options = Object.assign(defaultOptions, options);
-
- let locales = new Set([this.BUILTIN, options.locale, this.defaultLocale]
- .filter(locale => this.messages.has(locale)));
-
- // Message names are case-insensitive, so normalize them to lower-case.
- message = message.toLowerCase();
- for (let locale of locales) {
- let messages = this.messages.get(locale);
- if (messages.has(message)) {
- let str = messages.get(message);
-
- if (!Array.isArray(substitutions)) {
- substitutions = [substitutions];
- }
-
- let replacer = (matched, index, dollarSigns) => {
- if (index) {
- // This is not quite Chrome-compatible. Chrome consumes any number
- // of digits following the $, but only accepts 9 substitutions. We
- // accept any number of substitutions.
- index = parseInt(index, 10) - 1;
- return index in substitutions ? substitutions[index] : "";
- }
- // For any series of contiguous `$`s, the first is dropped, and
- // the rest remain in the output string.
- return dollarSigns;
- };
- return str.replace(/\$(?:([1-9]\d*)|(\$+))/g, replacer);
- }
- }
-
- // Check for certain pre-defined messages.
- if (message == "@@ui_locale") {
- return this.uiLocale;
- } else if (message.startsWith("@@bidi_")) {
- let registry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry);
- let rtl = registry.isLocaleRTL("global");
-
- if (message == "@@bidi_dir") {
- return rtl ? "rtl" : "ltr";
- } else if (message == "@@bidi_reversed_dir") {
- return rtl ? "ltr" : "rtl";
- } else if (message == "@@bidi_start_edge") {
- return rtl ? "right" : "left";
- } else if (message == "@@bidi_end_edge") {
- return rtl ? "left" : "right";
- }
- }
-
- if (!this.warnedMissingKeys.has(message)) {
- let error = `Unknown localization message ${message}`;
- if (options.cloneScope) {
- error = new options.cloneScope.Error(error);
- }
- Cu.reportError(error);
- this.warnedMissingKeys.add(message);
- }
- return options.defaultValue;
- },
-
- // Localize a string, replacing all |__MSG_(.*)__| tokens with the
- // matching string from the current locale, as determined by
- // |this.selectedLocale|.
- //
- // This may not be called before calling either |initLocale| or
- // |initAllLocales|.
- localize(str, locale = this.selectedLocale) {
- if (!str) {
- return str;
- }
-
- return str.replace(/__MSG_([A-Za-z0-9@_]+?)__/g, (matched, message) => {
- return this.localizeMessage(message, [], {locale, defaultValue: matched});
- });
- },
-
- // Validates the contents of a locale JSON file, normalizes the
- // messages into a Map of message key -> localized string pairs.
- addLocale(locale, messages, extension) {
- let result = new Map();
-
- // Chrome does not document the semantics of its localization
- // system very well. It handles replacements by pre-processing
- // messages, replacing |$[a-zA-Z0-9@_]+$| tokens with the value of their
- // replacements. Later, it processes the resulting string for
- // |$[0-9]| replacements.
- //
- // Again, it does not document this, but it accepts any number
- // of sequential |$|s, and replaces them with that number minus
- // 1. It also accepts |$| followed by any number of sequential
- // digits, but refuses to process a localized string which
- // provides more than 9 substitutions.
- if (!instanceOf(messages, "Object")) {
- extension.packagingError(`Invalid locale data for ${locale}`);
- return result;
- }
-
- for (let key of Object.keys(messages)) {
- let msg = messages[key];
-
- if (!instanceOf(msg, "Object") || typeof(msg.message) != "string") {
- extension.packagingError(`Invalid locale message data for ${locale}, message ${JSON.stringify(key)}`);
- continue;
- }
-
- // Substitutions are case-insensitive, so normalize all of their names
- // to lower-case.
- let placeholders = new Map();
- if (instanceOf(msg.placeholders, "Object")) {
- for (let key of Object.keys(msg.placeholders)) {
- placeholders.set(key.toLowerCase(), msg.placeholders[key]);
- }
- }
-
- let replacer = (match, name) => {
- let replacement = placeholders.get(name.toLowerCase());
- if (instanceOf(replacement, "Object") && "content" in replacement) {
- return replacement.content;
- }
- return "";
- };
-
- let value = msg.message.replace(/\$([A-Za-z0-9@_]+)\$/g, replacer);
-
- // Message names are also case-insensitive, so normalize them to lower-case.
- result.set(key.toLowerCase(), value);
- }
-
- this.messages.set(locale, result);
- return result;
- },
-
- get acceptLanguages() {
- let result = Preferences.get("intl.accept_languages", "", Ci.nsIPrefLocalizedString);
- return result.split(/\s*,\s*/g);
- },
-
-
- get uiLocale() {
- // Return the browser locale, but convert it to a Chrome-style
- // locale code.
- return Locale.getLocale().replace(/-/g, "_");
- },
-};
-
-// This is a generic class for managing event listeners. Example usage:
-//
-// new EventManager(context, "api.subAPI", fire => {
-// let listener = (...) => {
-// // Fire any listeners registered with addListener.
-// fire(arg1, arg2);
-// };
-// // Register the listener.
-// SomehowRegisterListener(listener);
-// return () => {
-// // Return a way to unregister the listener.
-// SomehowUnregisterListener(listener);
-// };
-// }).api()
-//
-// The result is an object with addListener, removeListener, and
-// hasListener methods. |context| is an add-on scope (either an
-// ExtensionContext in the chrome process or ExtensionContext in a
-// content process). |name| is for debugging. |register| is a function
-// to register the listener. |register| is only called once, even if
-// multiple listeners are registered. |register| should return an
-// unregister function that will unregister the listener.
-function EventManager(context, name, register) {
- this.context = context;
- this.name = name;
- this.register = register;
- this.unregister = null;
- this.callbacks = new Set();
-}
-
-EventManager.prototype = {
- addListener(callback) {
- if (typeof(callback) != "function") {
- dump(`Expected function\n${Error().stack}`);
- return;
- }
- if (this.context.unloaded) {
- dump(`Cannot add listener to ${this.name} after context unloaded`);
- return;
- }
-
- if (!this.callbacks.size) {
- this.context.callOnClose(this);
-
- let fireFunc = this.fire.bind(this);
- let fireWithoutClone = this.fireWithoutClone.bind(this);
- fireFunc.withoutClone = fireWithoutClone;
- this.unregister = this.register(fireFunc);
- }
- this.callbacks.add(callback);
- },
-
- removeListener(callback) {
- if (!this.callbacks.size) {
- return;
- }
-
- this.callbacks.delete(callback);
- if (this.callbacks.size == 0) {
- this.unregister();
- this.unregister = null;
-
- this.context.forgetOnClose(this);
- }
- },
-
- hasListener(callback) {
- return this.callbacks.has(callback);
- },
-
- fire(...args) {
- this._fireCommon("runSafe", args);
- },
-
- fireWithoutClone(...args) {
- this._fireCommon("runSafeWithoutClone", args);
- },
-
- _fireCommon(runSafeMethod, args) {
- for (let callback of this.callbacks) {
- Promise.resolve(callback).then(callback => {
- if (this.context.unloaded) {
- dump(`${this.name} event fired after context unloaded.\n`);
- } else if (!this.context.active) {
- dump(`${this.name} event fired while context is inactive.\n`);
- } else if (this.callbacks.has(callback)) {
- this.context[runSafeMethod](callback, ...args);
- }
- });
- }
- },
-
- close() {
- if (this.callbacks.size) {
- this.unregister();
- }
- this.callbacks.clear();
- this.register = null;
- this.unregister = null;
- },
-
- api() {
- return {
- addListener: callback => this.addListener(callback),
- removeListener: callback => this.removeListener(callback),
- hasListener: callback => this.hasListener(callback),
- };
- },
-};
-
-// Similar to EventManager, but it doesn't try to consolidate event
-// notifications. Each addListener call causes us to register once. It
-// allows extra arguments to be passed to addListener.
-function SingletonEventManager(context, name, register) {
- this.context = context;
- this.name = name;
- this.register = register;
- this.unregister = new Map();
-}
-
-SingletonEventManager.prototype = {
- addListener(callback, ...args) {
- let wrappedCallback = (...args) => {
- if (this.context.unloaded) {
- dump(`${this.name} event fired after context unloaded.\n`);
- } else if (this.unregister.has(callback)) {
- return callback(...args);
- }
- };
-
- let unregister = this.register(wrappedCallback, ...args);
- this.unregister.set(callback, unregister);
- this.context.callOnClose(this);
- },
-
- removeListener(callback) {
- if (!this.unregister.has(callback)) {
- return;
- }
-
- let unregister = this.unregister.get(callback);
- this.unregister.delete(callback);
- unregister();
- },
-
- hasListener(callback) {
- return this.unregister.has(callback);
- },
-
- close() {
- for (let unregister of this.unregister.values()) {
- unregister();
- }
- },
-
- api() {
- return {
- addListener: (...args) => this.addListener(...args),
- removeListener: (...args) => this.removeListener(...args),
- hasListener: (...args) => this.hasListener(...args),
- };
- },
-};
-
-// Simple API for event listeners where events never fire.
-function ignoreEvent(context, name) {
- return {
- addListener: function(callback) {
- let id = context.extension.id;
- let frame = Components.stack.caller;
- let msg = `In add-on ${id}, attempting to use listener "${name}", which is unimplemented.`;
- let scriptError = Cc["@mozilla.org/scripterror;1"]
- .createInstance(Ci.nsIScriptError);
- scriptError.init(msg, frame.filename, null, frame.lineNumber,
- frame.columnNumber, Ci.nsIScriptError.warningFlag,
- "content javascript");
- let consoleService = Cc["@mozilla.org/consoleservice;1"]
- .getService(Ci.nsIConsoleService);
- consoleService.logMessage(scriptError);
- },
- removeListener: function(callback) {},
- hasListener: function(callback) {},
- };
-}
-
-// Copy an API object from |source| into the scope |dest|.
-function injectAPI(source, dest) {
- for (let prop in source) {
- // Skip names prefixed with '_'.
- if (prop[0] == "_") {
- continue;
- }
-
- let desc = Object.getOwnPropertyDescriptor(source, prop);
- if (typeof(desc.value) == "function") {
- Cu.exportFunction(desc.value, dest, {defineAs: prop});
- } else if (typeof(desc.value) == "object") {
- let obj = Cu.createObjectIn(dest, {defineAs: prop});
- injectAPI(desc.value, obj);
- } else {
- Object.defineProperty(dest, prop, desc);
- }
- }
-}
-
-/**
- * Returns a Promise which resolves when the given document's DOM has
- * fully loaded.
- *
- * @param {Document} doc The document to await the load of.
- * @returns {Promise<Document>}
- */
-function promiseDocumentReady(doc) {
- if (doc.readyState == "interactive" || doc.readyState == "complete") {
- return Promise.resolve(doc);
- }
-
- return new Promise(resolve => {
- doc.addEventListener("DOMContentLoaded", function onReady(event) {
- if (event.target === event.currentTarget) {
- doc.removeEventListener("DOMContentLoaded", onReady, true);
- resolve(doc);
- }
- }, true);
- });
-}
-
-/**
- * Returns a Promise which resolves when the given document is fully
- * loaded.
- *
- * @param {Document} doc The document to await the load of.
- * @returns {Promise<Document>}
- */
-function promiseDocumentLoaded(doc) {
- if (doc.readyState == "complete") {
- return Promise.resolve(doc);
- }
-
- return new Promise(resolve => {
- doc.defaultView.addEventListener("load", function onReady(event) {
- doc.defaultView.removeEventListener("load", onReady);
- resolve(doc);
- });
- });
-}
-
-/**
- * Returns a Promise which resolves when the given event is dispatched to the
- * given element.
- *
- * @param {Element} element
- * The element on which to listen.
- * @param {string} eventName
- * The event to listen for.
- * @param {boolean} [useCapture = true]
- * If true, listen for the even in the capturing rather than
- * bubbling phase.
- * @param {Event} [test]
- * An optional test function which, when called with the
- * observer's subject and data, should return true if this is the
- * expected event, false otherwise.
- * @returns {Promise<Event>}
- */
-function promiseEvent(element, eventName, useCapture = true, test = event => true) {
- return new Promise(resolve => {
- function listener(event) {
- if (test(event)) {
- element.removeEventListener(eventName, listener, useCapture);
- resolve(event);
- }
- }
- element.addEventListener(eventName, listener, useCapture);
- });
-}
-
-/**
- * Returns a Promise which resolves the given observer topic has been
- * observed.
- *
- * @param {string} topic
- * The topic to observe.
- * @param {function(nsISupports, string)} [test]
- * An optional test function which, when called with the
- * observer's subject and data, should return true if this is the
- * expected notification, false otherwise.
- * @returns {Promise<object>}
- */
-function promiseObserved(topic, test = () => true) {
- return new Promise(resolve => {
- let observer = (subject, topic, data) => {
- if (test(subject, data)) {
- Services.obs.removeObserver(observer, topic);
- resolve({subject, data});
- }
- };
- Services.obs.addObserver(observer, topic, false);
- });
-}
-
-
-/*
- * Messaging primitives.
- */
-
-let gNextPortId = 1;
-
-/**
- * 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.
- */
-function Port(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) => {
- if (!sender.contextId) {
- Cu.reportError("Missing sender.contextId in message to Port");
- return false;
- }
- 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);
-}
-
-Port.prototype = {
- 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 EventManager(this.context, "Port.onDisconnect", fire => {
- return this.registerOnDisconnect(error => {
- portError = error && this.context.normalizeError(error);
- fire.withoutClone(portObj);
- });
- }).api(),
- onMessage: new EventManager(this.context, "Port.onMessage", fire => {
- return this.registerOnMessage(msg => {
- msg = Cu.cloneInto(msg, this.context.cloneScope);
- fire.withoutClone(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);
- },
-
- close() {
- this.disconnect();
- },
-};
-
-function getMessageManager(target) {
- if (target instanceof Ci.nsIFrameLoaderOwner) {
- return target.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager;
- }
- return target.QueryInterface(Ci.nsIMessageSender);
-}
-
-/**
- * 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.
- */
-function Messenger(context, messageManagers, sender, filter, optionalFilter) {
- this.context = context;
- this.messageManagers = messageManagers;
- this.sender = sender;
- this.filter = filter;
- this.optionalFilter = optionalFilter;
-}
-
-Messenger.prototype = {
- _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);
- },
-
- onMessage(name) {
- return new SingletonEventManager(this.context, name, callback => {
- 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;
- },
-
- 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 = callback(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();
- },
-
- connectGetRawPort(messageManager, name, recipient) {
- let portId = `${gNextPortId++}-${Services.appinfo.uniqueProcessID}`;
- let port = new Port(this.context, messageManager, this.messageManagers, name, portId, null, recipient);
- let msg = {name, portId};
- this._sendMessage(messageManager, "Extension:Connect", msg, recipient)
- .catch(e => {
- if (e.result === MessageChannel.RESULT_NO_HANDLER) {
- e = {message: "Could not establish connection. Receiving end does not exist."};
- } else if (e.result === MessageChannel.RESULT_DISCONNECTED) {
- e = null;
- }
- port.disconnectByOtherEnd(e);
- });
- return port;
- },
-
- connect(messageManager, name, recipient) {
- let port = this.connectGetRawPort(messageManager, name, recipient);
- return port.api();
- },
-
- onConnect(name) {
- return new SingletonEventManager(this.context, name, callback => {
- 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;
- },
-
- 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);
- this.context.runSafeWithoutClone(callback, port.api());
- return true;
- },
- };
-
- MessageChannel.addListener(this.messageManagers, "Extension:Connect", listener);
- return () => {
- MessageChannel.removeListener(this.messageManagers, "Extension:Connect", listener);
- };
- }).api();
- },
-};
-
-function flushJarCache(jarFile) {
- Services.obs.notifyObservers(jarFile, "flush-cache-entry", null);
-}
-
-const PlatformInfo = Object.freeze({
- os: (function() {
- let os = AppConstants.platform;
- if (os == "macosx") {
- os = "mac";
- }
- return os;
- })(),
- arch: (function() {
- let abi = Services.appinfo.XPCOMABI;
- let [arch] = abi.split("-");
- if (arch == "x86") {
- arch = "x86-32";
- } else if (arch == "x86_64") {
- arch = "x86-64";
- }
- return arch;
- })(),
-});
-
-function detectLanguage(text) {
- return LanguageDetector.detectLanguage(text).then(result => ({
- isReliable: result.confident,
- languages: result.languages.map(lang => {
- return {
- language: lang.languageCode,
- percentage: lang.percent,
- };
- }),
- }));
-}
-
/**
* An object that runs the implementation of a schema API. Instantiations of
* this interfaces are used by Schemas.jsm.
*
* @interface
*/
class SchemaAPIInterface {
/**
@@ -1767,261 +523,16 @@ class LocalAPIImplementation extends Sch
return this.pathObj[this.name].hasListener.call(null, listener);
}
removeListener(listener) {
this.pathObj[this.name].removeListener.call(null, listener);
}
}
-let nextId = 1;
-
-/**
- * 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;
- }
-
- callFunctionNoReturn(args) {
- this.childApiManager.callParentFunctionNoReturn(this.path, args);
- }
-
- callAsyncFunction(args, callback) {
- return this.childApiManager.callParentAsyncFunction(this.path, args, callback);
- }
-
- addListener(listener, args) {
- let set = this.childApiManager.listeners.get(this.path);
- if (!set) {
- set = new Set();
- this.childApiManager.listeners.set(this.path, set);
- }
-
- set.add(listener);
-
- if (set.size == 1) {
- args = args.slice(1);
-
- this.childApiManager.messageManager.sendAsyncMessage("API:AddListener", {
- childId: this.childApiManager.id,
- path: this.path,
- args,
- });
- }
- }
-
- removeListener(listener) {
- let set = this.childApiManager.listeners.get(this.path);
- if (!set) {
- return;
- }
- set.delete(listener);
-
- if (set.size == 0) {
- this.childApiManager.messageManager.sendAsyncMessage("API:RemoveListener", {
- childId: this.childApiManager.id,
- path: this.path,
- });
- }
- }
-
- hasListener(listener) {
- let set = this.childApiManager.listeners.get(this.path);
- return set ? set.has(listener) : false;
- }
-}
-
-// 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
-// Extension.jsm. It handles asynchronous function calls as well as
-// event listeners.
-class ChildAPIManager {
- constructor(context, messageManager, localApis, contextData) {
- this.context = context;
- this.messageManager = messageManager;
-
- // 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 = localApis;
-
- let id = String(context.extension.id) + "." + String(context.contextId);
- this.id = id;
-
- let data = {childId: id, extensionId: context.extension.id, principal: context.principal};
- Object.assign(data, contextData);
-
- messageManager.addMessageListener("API:RunListener", this);
- messageManager.addMessageListener("API:CallResult", this);
-
- // Map[path -> Set[listener]]
- // path is, e.g., "runtime.onMessage".
- this.listeners = new Map();
-
- // Map[callId -> Deferred]
- this.callPromises = new Map();
-
- this.createProxyContextInConstructor(data);
- }
-
- createProxyContextInConstructor(data) {
- this.messageManager.sendAsyncMessage("API:CreateProxyContext", data);
- }
-
- receiveMessage({name, data}) {
- if (data.childId != this.id) {
- return;
- }
-
- switch (name) {
- case "API:RunListener":
- let listeners = this.listeners.get(data.path);
- for (let callback of listeners) {
- runSafe(this.context, callback, ...data.args);
- }
- 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 = nextId++;
- 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) {
- let parsed = /^(.+)\.(on[A-Z][^.]+)$/.exec(path);
- if (!parsed) {
- throw new Error("getParentEvent: Invalid event name: " + path);
- }
- let [, namespace, name] = parsed;
- 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});
- }
-
- 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;
- }
- return true;
- }
-
- getImplementation(namespace, name) {
- let pathObj = this.localApis;
- if (pathObj) {
- for (let part of namespace.split(".")) {
- pathObj = pathObj[part];
- if (!pathObj) {
- break;
- }
- }
- if (pathObj && name in pathObj) {
- return new LocalAPIImplementation(pathObj, 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);
- }
-}
-
/**
* This object loads the ext-*.js scripts that define the extension API.
*
* This class instance is shared with the scripts that it loads, so that the
* ext-*.js scripts and the instantiator can communicate with each other.
*/
class SchemaAPIManager extends EventEmitter {
/**
@@ -2154,110 +665,14 @@ class SchemaAPIManager extends EventEmit
if (Schemas.checkPermissions(api.namespace, context.extension)) {
api = api.getAPI(context);
copy(obj, api);
}
}
}
}
-/**
- * Convert any of several different representations of a date/time to a Date object.
- * Accepts several formats:
- * a Date object, an ISO8601 string, or a number of milliseconds since the epoch as
- * either a number or a string.
- *
- * @param {Date|string|number} date
- * The date to convert.
- * @returns {Date}
- * A Date object
- */
-function normalizeTime(date) {
- // Of all the formats we accept the "number of milliseconds since the epoch as a string"
- // is an outlier, everything else can just be passed directly to the Date constructor.
- return new Date((typeof date == "string" && /^\d+$/.test(date))
- ? parseInt(date, 10) : date);
-}
-
-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 prototype object on which to define the getter.
- * @param {string|Symbol} prop
- * The property name for which to define the getter.
- * @param {function} getter
- * The function to call in order to generate the final property
- * value.
- */
-function defineLazyGetter(object, prop, getter) {
- let redefine = (obj, value) => {
- Object.defineProperty(obj, prop, {
- enumerable: true,
- configurable: true,
- writable: true,
- value,
- });
- return value;
- };
-
- Object.defineProperty(object, prop, {
- enumerable: true,
- configurable: true,
-
- get() {
- return redefine(this, getter.call(this));
- },
-
- set(value) {
- redefine(this, value);
- },
- });
-}
-
-this.ExtensionUtils = {
- defineLazyGetter,
- detectLanguage,
- extend,
- flushJarCache,
- getConsole,
- getInnerWindowID,
- ignoreEvent,
- injectAPI,
- instanceOf,
- normalizeTime,
- promiseDocumentLoaded,
- promiseDocumentReady,
- promiseEvent,
- promiseObserved,
- runSafe,
- runSafeSync,
- runSafeSyncWithoutClone,
- runSafeWithoutClone,
- stylesheetMap,
+const ExtensionCommon = {
BaseContext,
- DefaultMap,
- DefaultWeakMap,
- EventEmitter,
- EventManager,
- ExtensionError,
- IconDetails,
LocalAPIImplementation,
- LocaleData,
- Messenger,
- Port,
- PlatformInfo,
SchemaAPIInterface,
- SingletonEventManager,
- SpreadArgs,
- ChildAPIManager,
SchemaAPIManager,
};
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -40,30 +40,37 @@ XPCOMUtils.defineLazyModuleGetter(this,
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/ExtensionCommon.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-var {
- runSafeSyncWithoutClone,
+
+const {
+ LocaleData,
defineLazyGetter,
- BaseContext,
- LocaleData,
- Messenger,
flushJarCache,
getInnerWindowID,
promiseDocumentReady,
- ChildAPIManager,
+ runSafeSyncWithoutClone,
+} = ExtensionUtils;
+
+const {
+ BaseContext,
SchemaAPIManager,
-} = ExtensionUtils;
+} = ExtensionCommon;
+
+const {
+ ChildAPIManager,
+ Messenger,
+} = ExtensionChild;
XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
function isWhenBeforeOrSame(when1, when2) {
let table = {"document_start": 0,
"document_end": 1,
copy from toolkit/components/extensions/Extension.jsm
copy to toolkit/components/extensions/ExtensionParent.jsm
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -1,126 +1,65 @@
/* 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";
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
-this.EXPORTED_SYMBOLS = ["Extension", "ExtensionData"];
-
-/* globals Extension ExtensionData */
+/* exported ExtensionParent */
-/*
- * This file is the main entry point for extensions. When an extension
- * loads, its bootstrap.js file creates a Extension instance
- * and calls .startup() on it. It calls .shutdown() when the extension
- * unloads. Extension manages any extension-specific state in
- * the chrome process.
- */
+this.EXPORTED_SYMBOLS = ["ExtensionParent"];
-const Ci = Components.interfaces;
-const Cc = Components.classes;
-const Cu = Components.utils;
-const Cr = Components.results;
-
-Cu.importGlobalProperties(["TextEncoder"]);
-
+Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-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, "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");
-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, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "OS",
- "resource://gre/modules/osfile.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
- "resource://gre/modules/PrivateBrowsingUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
- "resource://gre/modules/Preferences.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "require",
- "resource://devtools/shared/Loader.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Task",
- "resource://gre/modules/Task.jsm");
+
+Cu.import("resource://gre/modules/ExtensionCommon.jsm");
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-Cu.import("resource://gre/modules/ExtensionContent.jsm");
-Cu.import("resource://gre/modules/ExtensionManagement.jsm");
+var {
+ BaseContext,
+ SchemaAPIManager,
+} = ExtensionCommon;
-XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
- "@mozilla.org/uuid-generator;1",
- "nsIUUIDGenerator");
+var {
+ SpreadArgs,
+ defineLazyGetter,
+ findPathInObject,
+} = ExtensionUtils;
const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
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,
- EventEmitter,
- LocaleData,
- SchemaAPIManager,
- SpreadArgs,
- defineLazyGetter,
- flushJarCache,
- instanceOf,
-} = ExtensionUtils;
-
-XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
-
-const LOGGER_ID_BASE = "addons.webextension.";
-const UUID_MAP_PREF = "extensions.webextensions.uuids";
-const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall";
-const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall";
-
-const COMMENT_REGEXP = new RegExp(String.raw`
- ^
- (
- (?:
- [^"\n] |
- " (?:[^"\\\n] | \\.)* "
- )*?
- )
-
- //.*
- `.replace(/\s+/g, ""), "gm");
-
-var GlobalManager;
-var ParentAPIManager;
+let GlobalManager;
+let ParentAPIManager;
+let ProxyMessenger;
// This object loads the ext-*.js scripts that define the extension API.
-var Management = new class extends SchemaAPIManager {
+let apiManager = new class extends SchemaAPIManager {
constructor() {
super("main");
this.initialized = null;
}
// Loads all the ext-*.js scripts currently registered.
lazyInit() {
if (this.initialized) {
@@ -153,17 +92,17 @@ var Management = new class extends Schem
super.registerSchemaAPI(namespace, envType, getAPI);
}
}
}();
// Subscribes to messages related to the extension messaging API and forwards it
// to the relevant message manager. The "sender" field for the `onMessage` and
// `onConnect` events are updated if needed.
-let ProxyMessenger = {
+ProxyMessenger = {
_initialized: false,
init() {
if (this._initialized) {
return;
}
this._initialized = true;
// TODO(robwu): When addons move to a separate process, we should use the
@@ -207,19 +146,19 @@ let ProxyMessenger = {
return Promise.reject({
result: MessageChannel.RESULT_NO_HANDLER,
message: "No matching message handler for the given recipient.",
});
}
if ((messageName == "Extension:Message" ||
messageName == "Extension:Connect") &&
- Management.global.tabGetSender) {
+ apiManager.global.tabGetSender) {
// From ext-tabs.js, undefined on Android.
- Management.global.tabGetSender(extension, target, sender);
+ apiManager.global.tabGetSender(extension, target, sender);
}
return MessageChannel.sendMessage(receiverMM, messageName, data, {
sender,
recipient,
responseType,
});
},
@@ -229,31 +168,77 @@ let ProxyMessenger = {
* @returns {object|null} The message manager matching the recipient if found.
*/
_getMessageManagerForRecipient(recipient) {
let {extensionId, tabId} = recipient;
// tabs.sendMessage / tabs.connect
if (tabId) {
// `tabId` being set implies that the tabs API is supported, so we don't
// need to check whether `TabManager` exists.
- let tab = Management.global.TabManager.getTab(tabId, null, null);
+ let tab = apiManager.global.TabManager.getTab(tabId, null, null);
return tab && tab.linkedBrowser.messageManager;
}
// runtime.sendMessage / runtime.connect
if (extensionId) {
// TODO(robwu): map the extensionId to the addon parent process's message
// manager when they run in a separate process.
return Services.ppmm.getChildAt(0);
}
return null;
},
};
+// Responsible for loading extension APIs into the right globals.
+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) {
+ ProxyMessenger.init();
+ apiManager.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) {
+ apiManager.off("extension-browser-inserted", this._onExtensionBrowser);
+ this.initialized = false;
+ }
+ },
+
+ _onExtensionBrowser(type, browser) {
+ browser.messageManager.loadFrameScript(`data:,
+ Components.utils.import("resource://gre/modules/ExtensionContent.jsm");
+ ExtensionContent.init(this);
+ addEventListener("unload", function() {
+ ExtensionContent.uninit(this);
+ });
+ `, false);
+ },
+
+ getExtension(extensionId) {
+ return this.extensionMap.get(extensionId);
+ },
+
+ injectInObject(context, isChromeCompat, dest) {
+ apiManager.generateAPIs(context, dest);
+ SchemaAPIManager.generateAPIs(context, context.extension.apis, dest);
+ },
+};
+
class BrowserDocshellFollower {
/**
* Follows the <browser> belonging to the `xulBrowser`'s current docshell.
*
* @param {XULElement} xulBrowser A <browser> tag.
* @param {function} onBrowserChange Called when the <browser> changes.
*/
constructor(xulBrowser, onBrowserChange) {
@@ -293,17 +278,17 @@ class ProxyContext extends BaseContext {
xulBrowser, this.onBrowserChange.bind(this));
Object.defineProperty(this, "principal", {
value: principal, enumerable: true, configurable: true,
});
this.listenerProxies = new Map();
- Management.emit("proxy-context-load", this);
+ apiManager.emit("proxy-context-load", this);
}
get cloneScope() {
return this.sandbox;
}
onBrowserChange(browser) {
// Make sure that the message manager is set. Otherwise the ProxyContext may
@@ -321,17 +306,17 @@ class ProxyContext extends BaseContext {
}
unload() {
if (this.unloaded) {
return;
}
this._docShellTracker.destroy();
super.unload();
- Management.emit("proxy-context-unload", this);
+ apiManager.emit("proxy-context-unload", this);
}
}
defineLazyGetter(ProxyContext.prototype, "apiObj", function() {
let obj = {};
GlobalManager.injectInObject(this, false, obj);
return obj;
});
@@ -352,82 +337,63 @@ class ExtensionChildProxyContext extends
}
// The window that contains this context. This may change due to moving tabs.
get xulWindow() {
return this.xulBrowser.ownerGlobal;
}
get windowId() {
- if (!Management.global.WindowManager || this.viewType == "background") {
+ if (!apiManager.global.WindowManager || this.viewType == "background") {
return;
}
// viewType popup or tab:
- return Management.global.WindowManager.getId(this.xulWindow);
+ return apiManager.global.WindowManager.getId(this.xulWindow);
}
get tabId() {
- if (!Management.global.TabManager) {
+ if (!apiManager.global.TabManager) {
return; // Not yet supported on Android.
}
let {gBrowser} = this.xulBrowser.ownerGlobal;
let tab = gBrowser && gBrowser.getTabForBrowser(this.xulBrowser);
- return tab && Management.global.TabManager.getId(tab);
+ return tab && apiManager.global.TabManager.getId(tab);
}
onBrowserChange(browser) {
super.onBrowserChange(browser);
this.xulBrowser = browser;
}
shutdown() {
- Management.emit("page-shutdown", this);
+ apiManager.emit("page-shutdown", this);
super.shutdown();
}
}
-function findPathInObject(obj, path, printErrors = true) {
- let parent;
- for (let elt of path.split(".")) {
- if (!obj || !(elt in obj)) {
- if (printErrors) {
- Cu.reportError(`WebExtension API ${path} not found (it may be unimplemented by Firefox).`);
- }
- return null;
- }
-
- parent = obj;
- obj = obj[elt];
- }
-
- if (typeof obj === "function") {
- return obj.bind(parent);
- }
- return obj;
-}
-
ParentAPIManager = {
proxyContexts: new Map(),
init() {
Services.obs.addObserver(this, "message-manager-close", false);
Services.mm.addMessageListener("API:CreateProxyContext", this);
Services.mm.addMessageListener("API:CloseProxyContext", this, true);
Services.mm.addMessageListener("API:Call", this);
Services.mm.addMessageListener("API:AddListener", this);
Services.mm.addMessageListener("API:RemoveListener", this);
},
- // "message-manager-close" observer.
observe(subject, topic, data) {
- let mm = subject;
- for (let [childId, context] of this.proxyContexts) {
- if (context.currentMessageManager == mm) {
- this.closeProxyContext(childId);
+ if (topic === "message-manager-close") {
+ let mm = subject;
+ for (let [childId, context] of this.proxyContexts) {
+ if (context.currentMessageManager === mm) {
+ this.closeProxyContext(childId);
+ }
}
}
},
shutdownExtension(extensionId) {
for (let [childId, context] of this.proxyContexts) {
if (context.extension.id == extensionId) {
context.shutdown();
@@ -470,36 +436,35 @@ ParentAPIManager = {
if (!extension) {
throw new Error(`No WebExtension found with ID ${extensionId}`);
}
let context;
if (envType == "addon_parent") {
// Privileged addon contexts can only be loaded in documents whose main
// frame is also the same addon.
- if (principal.URI.prePath != extension.baseURI.prePath ||
+ if (principal.URI.prePath !== extension.baseURI.prePath ||
!target.contentPrincipal.subsumes(principal)) {
throw new Error(`Refused to create privileged WebExtension context for ${principal.URI.spec}`);
}
context = new ExtensionChildProxyContext(envType, extension, data, target);
} else if (envType == "content_parent") {
context = new ProxyContext(envType, extension, data, target, principal);
} else {
throw new Error(`Invalid WebExtension context envType: ${envType}`);
}
this.proxyContexts.set(childId, context);
},
closeProxyContext(childId) {
let context = this.proxyContexts.get(childId);
- if (!context) {
- return;
+ if (context) {
+ context.unload();
+ this.proxyContexts.delete(childId);
}
- context.unload();
- this.proxyContexts.delete(childId);
},
call(data, target) {
let context = this.getContextById(data.childId);
if (context.currentMessageManager !== target.messageManager) {
Cu.reportError("WebExtension warning: Message manager unexpectedly changed");
}
@@ -575,1112 +540,14 @@ ParentAPIManager = {
throw error;
}
return context;
},
};
ParentAPIManager.init();
-// All moz-extension URIs use a machine-specific UUID rather than the
-// extension's own ID in the host component. This makes it more
-// difficult for web pages to detect whether a user has a given add-on
-// installed (by trying to load a moz-extension URI referring to a
-// web_accessible_resource from the extension). UUIDMap.get()
-// returns the UUID for a given add-on ID.
-var UUIDMap = {
- _read() {
- let pref = Preferences.get(UUID_MAP_PREF, "{}");
- try {
- return JSON.parse(pref);
- } catch (e) {
- Cu.reportError(`Error parsing ${UUID_MAP_PREF}.`);
- return {};
- }
- },
- _write(map) {
- Preferences.set(UUID_MAP_PREF, JSON.stringify(map));
- },
-
- get(id, create = true) {
- let map = this._read();
-
- if (id in map) {
- return map[id];
- }
-
- let uuid = null;
- if (create) {
- uuid = uuidGen.generateUUID().number;
- uuid = uuid.slice(1, -1); // Strip { and } off the UUID.
-
- map[id] = uuid;
- this._write(map);
- }
- return uuid;
- },
-
- remove(id) {
- let map = this._read();
- delete map[id];
- this._write(map);
- },
-};
-
-// This is the old interface that UUIDMap replaced, to be removed when
-// the references listed in bug 1291399 are updated.
-/* exported getExtensionUUID */
-function getExtensionUUID(id) {
- return UUIDMap.get(id, true);
-}
-
-// For extensions that have called setUninstallURL(), send an event
-// so the browser can display the URL.
-var UninstallObserver = {
- initialized: false,
-
- init() {
- if (!this.initialized) {
- AddonManager.addAddonListener(this);
- XPCOMUtils.defineLazyPreferenceGetter(this, "leaveStorage", LEAVE_STORAGE_PREF, false);
- XPCOMUtils.defineLazyPreferenceGetter(this, "leaveUuid", LEAVE_UUID_PREF, false);
- this.initialized = true;
- }
- },
-
- onUninstalling(addon) {
- let extension = GlobalManager.extensionMap.get(addon.id);
- if (extension) {
- // Let any other interested listeners respond
- // (e.g., display the uninstall URL)
- Management.emit("uninstall", extension);
- }
- },
-
- onUninstalled(addon) {
- let uuid = UUIDMap.get(addon.id, false);
- if (!uuid) {
- return;
- }
-
- if (!this.leaveStorage) {
- // Clear browser.local.storage
- ExtensionStorage.clear(addon.id);
-
- // Clear any IndexedDB storage created by the extension
- let baseURI = NetUtil.newURI(`moz-extension://${uuid}/`);
- let principal = Services.scriptSecurityManager.createCodebasePrincipal(
- baseURI, {addonId: addon.id}
- );
- Services.qms.clearStoragesForPrincipal(principal);
-
- // Clear localStorage created by the extension
- let attrs = JSON.stringify({addonId: addon.id});
- Services.obs.notifyObservers(null, "clear-origin-attributes-data", attrs);
- }
-
- if (!this.leaveUuid) {
- // Clear the entry in the UUID map
- UUIDMap.remove(addon.id);
- }
- },
-};
-
-// Responsible for loading extension APIs into the right globals.
-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) {
- UninstallObserver.init();
- ProxyMessenger.init();
- 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) {
- Management.off("extension-browser-inserted", this._onExtensionBrowser);
- this.initialized = false;
- }
- },
-
- _onExtensionBrowser(type, browser) {
- browser.messageManager.loadFrameScript(`data:,
- Components.utils.import("resource://gre/modules/ExtensionContent.jsm");
- ExtensionContent.init(this);
- addEventListener("unload", function() {
- ExtensionContent.uninit(this);
- });
- `, false);
- },
-
- getExtension(extensionId) {
- return this.extensionMap.get(extensionId);
- },
-
- injectInObject(context, isChromeCompat, dest) {
- Management.generateAPIs(context, dest);
- SchemaAPIManager.generateAPIs(context, context.extension.apis, dest);
- },
-};
-
-// 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.
-//
-// No functionality of this class is guaranteed to work before
-// |readManifest| has been called, and completed.
-this.ExtensionData = class {
- constructor(rootURI) {
- this.rootURI = rootURI;
-
- this.manifest = null;
- this.id = null;
- this.uuid = null;
- this.localeData = null;
- this._promiseLocales = null;
-
- this.apiNames = new Set();
- this.dependencies = new Set();
- this.permissions = new Set();
-
- this.errors = [];
- }
-
- get builtinMessages() {
- return null;
- }
-
- get logger() {
- let id = this.id || "<unknown>";
- return Log.repository.getLogger(LOGGER_ID_BASE + id);
- }
-
- // Report an error about the extension's manifest file.
- manifestError(message) {
- this.packagingError(`Reading manifest: ${message}`);
- }
-
- // Report an error about the extension's general packaging.
- packagingError(message) {
- this.errors.push(message);
- this.logger.error(`Loading extension '${this.id}': ${message}`);
- }
-
- /**
- * Returns the moz-extension: URL for the given path within this
- * extension.
- *
- * Must not be called unless either the `id` or `uuid` property has
- * already been set.
- *
- * @param {string} path The path portion of the URL.
- * @returns {string}
- */
- getURL(path = "") {
- if (!(this.id || this.uuid)) {
- throw new Error("getURL may not be called before an `id` or `uuid` has been set");
- }
- if (!this.uuid) {
- this.uuid = UUIDMap.get(this.id);
- }
- return `moz-extension://${this.uuid}/${path}`;
- }
-
- readDirectory(path) {
- return Task.spawn(function* () {
- if (this.rootURI instanceof Ci.nsIFileURL) {
- let uri = NetUtil.newURI(this.rootURI.resolve("./" + path));
- let fullPath = uri.QueryInterface(Ci.nsIFileURL).file.path;
-
- let iter = new OS.File.DirectoryIterator(fullPath);
- let results = [];
-
- try {
- yield iter.forEach(entry => {
- results.push(entry);
- });
- } catch (e) {
- // Always return a list, even if the directory does not exist (or is
- // not a directory) for symmetry with the ZipReader behavior.
- }
- iter.close();
-
- return results;
- }
-
- // FIXME: We need a way to do this without main thread IO.
-
- let uri = this.rootURI.QueryInterface(Ci.nsIJARURI);
-
- let file = uri.JARFile.QueryInterface(Ci.nsIFileURL).file;
- let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader);
- zipReader.open(file);
- try {
- let results = [];
-
- // Normalize the directory path.
- path = `${uri.JAREntry}/${path}`;
- path = path.replace(/\/\/+/g, "/").replace(/^\/|\/$/g, "") + "/";
-
- // Escape pattern metacharacters.
- let pattern = path.replace(/[[\]()?*~|$\\]/g, "\\$&");
-
- let enumerator = zipReader.findEntries(pattern + "*");
- while (enumerator.hasMore()) {
- let name = enumerator.getNext();
- if (!name.startsWith(path)) {
- throw new Error("Unexpected ZipReader entry");
- }
-
- // The enumerator returns the full path of all entries.
- // Trim off the leading path, and filter out entries from
- // subdirectories.
- name = name.slice(path.length);
- if (name && !/\/./.test(name)) {
- results.push({
- name: name.replace("/", ""),
- isDir: name.endsWith("/"),
- });
- }
- }
-
- return results;
- } finally {
- zipReader.close();
- }
- }.bind(this));
- }
-
- readJSON(path) {
- return new Promise((resolve, reject) => {
- let uri = this.rootURI.resolve(`./${path}`);
-
- NetUtil.asyncFetch({uri, loadUsingSystemPrincipal: true}, (inputStream, status) => {
- if (!Components.isSuccessCode(status)) {
- // Convert status code to a string
- let e = Components.Exception("", status);
- reject(new Error(`Error while loading '${uri}' (${e.name})`));
- return;
- }
- try {
- let text = NetUtil.readInputStreamToString(inputStream, inputStream.available(),
- {charset: "utf-8"});
-
- text = text.replace(COMMENT_REGEXP, "$1");
-
- resolve(JSON.parse(text));
- } catch (e) {
- reject(e);
- }
- });
- });
- }
-
- // Reads the extension's |manifest.json| file, and stores its
- // parsed contents in |this.manifest|.
- readManifest() {
- return Promise.all([
- this.readJSON("manifest.json"),
- Management.lazyInit(),
- ]).then(([manifest]) => {
- this.manifest = manifest;
- this.rawManifest = manifest;
-
- if (manifest && manifest.default_locale) {
- return this.initLocale();
- }
- }).then(() => {
- let context = {
- url: this.baseURI && this.baseURI.spec,
-
- principal: this.principal,
-
- logError: error => {
- this.logger.warn(`Loading extension '${this.id}': Reading manifest: ${error}`);
- },
-
- preprocessors: {},
- };
-
- if (this.localeData) {
- context.preprocessors.localize = (value, context) => this.localize(value);
- }
-
- let normalized = Schemas.normalize(this.manifest, "manifest.WebExtensionManifest", context);
- if (normalized.error) {
- this.manifestError(normalized.error);
- } else {
- this.manifest = normalized.value;
- }
-
- try {
- // Do not override the add-on id that has been already assigned.
- if (!this.id && this.manifest.applications.gecko.id) {
- this.id = this.manifest.applications.gecko.id;
- }
- } catch (e) {
- // Errors are handled by the type checks above.
- }
-
- let permissions = this.manifest.permissions || [];
-
- let whitelist = [];
- for (let perm of permissions) {
- this.permissions.add(perm);
-
- let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm);
- if (!match) {
- whitelist.push(perm);
- } else if (match[1] == "experiments" && match[2]) {
- this.apiNames.add(match[2]);
- }
- }
- this.whiteListedHosts = new MatchPattern(whitelist);
-
- for (let api of this.apiNames) {
- this.dependencies.add(`${api}@experiments.addons.mozilla.org`);
- }
-
- return this.manifest;
- });
- }
-
- localizeMessage(...args) {
- return this.localeData.localizeMessage(...args);
- }
-
- localize(...args) {
- return this.localeData.localize(...args);
- }
-
- // If a "default_locale" is specified in that manifest, returns it
- // as a Gecko-compatible locale string. Otherwise, returns null.
- get defaultLocale() {
- if (this.manifest.default_locale != null) {
- return this.normalizeLocaleCode(this.manifest.default_locale);
- }
-
- return null;
- }
-
- // Normalizes a Chrome-compatible locale code to the appropriate
- // Gecko-compatible variant. Currently, this means simply
- // replacing underscores with hyphens.
- normalizeLocaleCode(locale) {
- return String.replace(locale, /_/g, "-");
- }
-
- // Reads the locale file for the given Gecko-compatible locale code, and
- // stores its parsed contents in |this.localeMessages.get(locale)|.
- readLocaleFile(locale) {
- return Task.spawn(function* () {
- let locales = yield this.promiseLocales();
- let dir = locales.get(locale) || locale;
- let file = `_locales/${dir}/messages.json`;
-
- try {
- let messages = yield this.readJSON(file);
- return this.localeData.addLocale(locale, messages, this);
- } catch (e) {
- this.packagingError(`Loading locale file ${file}: ${e}`);
- return new Map();
- }
- }.bind(this));
- }
-
- // Reads the list of locales available in the extension, and returns a
- // Promise which resolves to a Map upon completion.
- // Each map key is a Gecko-compatible locale code, and each value is the
- // "_locales" subdirectory containing that locale:
- //
- // Map(gecko-locale-code -> locale-directory-name)
- promiseLocales() {
- if (!this._promiseLocales) {
- this._promiseLocales = Task.spawn(function* () {
- let locales = new Map();
-
- let entries = yield this.readDirectory("_locales");
- for (let file of entries) {
- if (file.isDir) {
- let locale = this.normalizeLocaleCode(file.name);
- locales.set(locale, file.name);
- }
- }
-
- this.localeData = new LocaleData({
- defaultLocale: this.defaultLocale,
- locales,
- builtinMessages: this.builtinMessages,
- });
-
- return locales;
- }.bind(this));
- }
-
- return this._promiseLocales;
- }
-
- // Reads the locale messages for all locales, and returns a promise which
- // resolves to a Map of locale messages upon completion. Each key in the map
- // is a Gecko-compatible locale code, and each value is a locale data object
- // as returned by |readLocaleFile|.
- initAllLocales() {
- return Task.spawn(function* () {
- let locales = yield this.promiseLocales();
-
- yield Promise.all(Array.from(locales.keys(),
- locale => this.readLocaleFile(locale)));
-
- let defaultLocale = this.defaultLocale;
- if (defaultLocale) {
- if (!locales.has(defaultLocale)) {
- this.manifestError('Value for "default_locale" property must correspond to ' +
- 'a directory in "_locales/". Not found: ' +
- JSON.stringify(`_locales/${this.manifest.default_locale}/`));
- }
- } else if (locales.size) {
- this.manifestError('The "default_locale" property is required when a ' +
- '"_locales/" directory is present.');
- }
-
- return this.localeData.messages;
- }.bind(this));
- }
-
- // Reads the locale file for the given Gecko-compatible locale code, or the
- // default locale if no locale code is given, and sets it as the currently
- // selected locale on success.
- //
- // Pre-loads the default locale for fallback message processing, regardless
- // of the locale specified.
- //
- // If no locales are unavailable, resolves to |null|.
- initLocale(locale = this.defaultLocale) {
- return Task.spawn(function* () {
- if (locale == null) {
- return null;
- }
-
- let promises = [this.readLocaleFile(locale)];
-
- let {defaultLocale} = this;
- if (locale != defaultLocale && !this.localeData.has(defaultLocale)) {
- promises.push(this.readLocaleFile(defaultLocale));
- }
-
- let results = yield Promise.all(promises);
-
- this.localeData.selectedLocale = locale;
- return results[0];
- }.bind(this));
- }
+const ExtensionParent = {
+ GlobalManager,
+ ParentAPIManager,
+ apiManager,
};
-
-
-/**
- * A skeleton Extension-like object, used for testing, which installs an
- * add-on via the add-on manager when startup() is called, and
- * uninstalles it on shutdown().
- *
- * @param {string} id
- * @param {nsIFile} file
- * @param {nsIURI} rootURI
- * @param {string} installType
- */
-class MockExtension {
- constructor(file, rootURI, installType) {
- this.id = null;
- this.file = file;
- this.rootURI = rootURI;
- this.installType = installType;
- this.addon = null;
-
- let promiseEvent = eventName => new Promise(resolve => {
- let onstartup = (msg, extension) => {
- if (this.addon && extension.id == this.addon.id) {
- Management.off(eventName, onstartup);
-
- this.id = extension.id;
- this._extension = extension;
- resolve(extension);
- }
- };
- Management.on(eventName, onstartup);
- });
-
- this._extension = null;
- this._extensionPromise = promiseEvent("startup");
- this._readyPromise = promiseEvent("ready");
- }
-
- testMessage(...args) {
- return this._extension.testMessage(...args);
- }
-
- on(...args) {
- this._extensionPromise.then(extension => {
- extension.on(...args);
- });
- }
-
- off(...args) {
- this._extensionPromise.then(extension => {
- extension.off(...args);
- });
- }
-
- startup() {
- if (this.installType == "temporary") {
- return AddonManager.installTemporaryAddon(this.file).then(addon => {
- this.addon = addon;
- return this._readyPromise;
- });
- } else if (this.installType == "permanent") {
- return new Promise((resolve, reject) => {
- AddonManager.getInstallForFile(this.file, install => {
- let listener = {
- onInstallFailed: reject,
- onInstallEnded: (install, newAddon) => {
- this.addon = newAddon;
- resolve(this._readyPromise);
- },
- };
-
- install.addListener(listener);
- install.install();
- });
- });
- }
- throw new Error("installType must be one of: temporary, permanent");
- }
-
- shutdown() {
- this.addon.uninstall();
- return this.cleanupGeneratedFile();
- }
-
- cleanupGeneratedFile() {
- flushJarCache(this.file);
- return OS.File.remove(this.file.path);
- }
-}
-
-let _browserUpdated = false;
-
-// We create one instance of this class per extension. |addonData|
-// comes directly from bootstrap.js when initializing.
-this.Extension = class extends ExtensionData {
- constructor(addonData, startupReason) {
- super(addonData.resourceURI);
-
- this.uuid = UUIDMap.get(addonData.id);
-
- if (addonData.cleanupFile) {
- Services.obs.addObserver(this, "xpcom-shutdown", false);
- this.cleanupFile = addonData.cleanupFile || null;
- delete addonData.cleanupFile;
- }
-
- this.addonData = addonData;
- this.startupReason = startupReason;
-
- this.id = addonData.id;
- this.baseURI = NetUtil.newURI(this.getURL("")).QueryInterface(Ci.nsIURL);
- this.principal = this.createPrincipal();
-
- this.onStartup = null;
-
- this.hasShutdown = false;
- this.onShutdown = new Set();
-
- this.uninstallURL = null;
-
- this.apis = [];
- this.whiteListedHosts = null;
- this.webAccessibleResources = null;
-
- this.emitter = new EventEmitter();
- }
-
- static set browserUpdated(updated) {
- _browserUpdated = updated;
- }
-
- static get browserUpdated() {
- return _browserUpdated;
- }
-
- /**
- * This code is designed to make it easy to test a WebExtension
- * without creating a bunch of files. Everything is contained in a
- * single JSON blob.
- *
- * Properties:
- * "background": "<JS code>"
- * A script to be loaded as the background script.
- * The "background" section of the "manifest" property is overwritten
- * if this is provided.
- * "manifest": {...}
- * Contents of manifest.json
- * "files": {"filename1": "contents1", ...}
- * Data to be included as files. Can be referenced from the manifest.
- * If a manifest file is provided here, it takes precedence over
- * a generated one. Always use "/" as a directory separator.
- * Directories should appear here only implicitly (as a prefix
- * to file names)
- *
- * To make things easier, the value of "background" and "files"[] can
- * be a function, which is converted to source that is run.
- *
- * The generated extension is stored in the system temporary directory,
- * and an nsIFile object pointing to it is returned.
- *
- * @param {object} data
- * @returns {nsIFile}
- */
- static generateXPI(data) {
- let manifest = data.manifest;
- if (!manifest) {
- manifest = {};
- }
-
- let files = data.files;
- if (!files) {
- files = {};
- }
-
- function provide(obj, keys, value, override = false) {
- if (keys.length == 1) {
- if (!(keys[0] in obj) || override) {
- obj[keys[0]] = value;
- }
- } else {
- if (!(keys[0] in obj)) {
- obj[keys[0]] = {};
- }
- provide(obj[keys[0]], keys.slice(1), value, override);
- }
- }
-
- provide(manifest, ["name"], "Generated extension");
- provide(manifest, ["manifest_version"], 2);
- provide(manifest, ["version"], "1.0");
-
- if (data.background) {
- let bgScript = uuidGen.generateUUID().number + ".js";
-
- provide(manifest, ["background", "scripts"], [bgScript], true);
- files[bgScript] = data.background;
- }
-
- provide(files, ["manifest.json"], manifest);
-
- if (data.embedded) {
- // Package this as a webextension embedded inside a legacy
- // extension.
-
- let xpiFiles = {
- "install.rdf": `<?xml version="1.0" encoding="UTF-8"?>
- <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:em="http://www.mozilla.org/2004/em-rdf#">
- <Description about="urn:mozilla:install-manifest"
- em:id="${manifest.applications.gecko.id}"
- em:name="${manifest.name}"
- em:type="2"
- em:version="${manifest.version}"
- em:description=""
- em:hasEmbeddedWebExtension="true"
- em:bootstrap="true">
-
- <!-- Firefox -->
- <em:targetApplication>
- <Description
- em:id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"
- em:minVersion="51.0a1"
- em:maxVersion="*"/>
- </em:targetApplication>
- </Description>
- </RDF>
- `,
-
- "bootstrap.js": `
- function install() {}
- function uninstall() {}
- function shutdown() {}
-
- function startup(data) {
- data.webExtension.startup();
- }
- `,
- };
-
- for (let [path, data] of Object.entries(files)) {
- xpiFiles[`webextension/${path}`] = data;
- }
-
- files = xpiFiles;
- }
-
- return this.generateZipFile(files);
- }
-
- static generateZipFile(files, baseName = "generated-extension.xpi") {
- let ZipWriter = Components.Constructor("@mozilla.org/zipwriter;1", "nsIZipWriter");
- let zipW = new ZipWriter();
-
- let file = FileUtils.getFile("TmpD", [baseName]);
- file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
-
- const MODE_WRONLY = 0x02;
- const MODE_TRUNCATE = 0x20;
- zipW.open(file, MODE_WRONLY | MODE_TRUNCATE);
-
- // Needs to be in microseconds for some reason.
- let time = Date.now() * 1000;
-
- function generateFile(filename) {
- let components = filename.split("/");
- let path = "";
- for (let component of components.slice(0, -1)) {
- path += component + "/";
- if (!zipW.hasEntry(path)) {
- zipW.addEntryDirectory(path, time, false);
- }
- }
- }
-
- for (let filename in files) {
- let script = files[filename];
- if (typeof(script) == "function") {
- script = "(" + script.toString() + ")()";
- } else if (instanceOf(script, "Object") || instanceOf(script, "Array")) {
- script = JSON.stringify(script);
- }
-
- if (!instanceOf(script, "ArrayBuffer")) {
- script = new TextEncoder("utf-8").encode(script).buffer;
- }
-
- let stream = Cc["@mozilla.org/io/arraybuffer-input-stream;1"].createInstance(Ci.nsIArrayBufferInputStream);
- stream.setData(script, 0, script.byteLength);
-
- generateFile(filename);
- zipW.addEntryStream(filename, time, 0, stream, false);
- }
-
- zipW.close();
-
- return file;
- }
-
- /**
- * Generates a new extension using |Extension.generateXPI|, and initializes a
- * new |Extension| instance which will execute it.
- *
- * @param {object} data
- * @returns {Extension}
- */
- static generate(data) {
- let file = this.generateXPI(data);
-
- flushJarCache(file);
- Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", {path: file.path});
-
- let fileURI = Services.io.newFileURI(file);
- let jarURI = Services.io.newURI("jar:" + fileURI.spec + "!/", null, null);
-
- // This may be "temporary" or "permanent".
- if (data.useAddonManager) {
- return new MockExtension(file, jarURI, data.useAddonManager);
- }
-
- let id;
- if (data.manifest) {
- if (data.manifest.applications && data.manifest.applications.gecko) {
- id = data.manifest.applications.gecko.id;
- } else if (data.manifest.browser_specific_settings && data.manifest.browser_specific_settings.gecko) {
- id = data.manifest.browser_specific_settings.gecko.id;
- }
- }
- if (!id) {
- id = uuidGen.generateUUID().number;
- }
-
- return new Extension({
- id,
- resourceURI: jarURI,
- cleanupFile: file,
- });
- }
-
- on(hook, f) {
- return this.emitter.on(hook, f);
- }
-
- off(hook, f) {
- return this.emitter.off(hook, f);
- }
-
- emit(...args) {
- return this.emitter.emit(...args);
- }
-
- testMessage(...args) {
- Management.emit("test-message", this, ...args);
- }
-
- createPrincipal(uri = this.baseURI) {
- return Services.scriptSecurityManager.createCodebasePrincipal(
- uri, {addonId: this.id});
- }
-
- // Checks that the given URL is a child of our baseURI.
- isExtensionURL(url) {
- let uri = Services.io.newURI(url, null, null);
-
- let common = this.baseURI.getCommonBaseSpec(uri);
- return common == this.baseURI.spec;
- }
-
- readManifest() {
- return super.readManifest().then(manifest => {
- if (AppConstants.RELEASE_OR_BETA) {
- return manifest;
- }
-
- // Load Experiments APIs that this extension depends on.
- return Promise.all(
- Array.from(this.apiNames, api => ExtensionAPIs.load(api))
- ).then(apis => {
- for (let API of apis) {
- this.apis.push(new API(this));
- }
-
- return manifest;
- });
- });
- }
-
- // Representation of the extension to send to content
- // processes. This should include anything the content process might
- // need.
- serialize() {
- return {
- id: this.id,
- uuid: this.uuid,
- manifest: this.manifest,
- resourceURL: this.addonData.resourceURI.spec,
- baseURL: this.baseURI.spec,
- content_scripts: this.manifest.content_scripts || [], // eslint-disable-line camelcase
- webAccessibleResources: this.webAccessibleResources.serialize(),
- whiteListedHosts: this.whiteListedHosts.serialize(),
- localeData: this.localeData.serialize(),
- permissions: this.permissions,
- principal: this.principal,
- };
- }
-
- broadcast(msg, data) {
- return new Promise(resolve => {
- let count = Services.ppmm.childCount;
- Services.ppmm.addMessageListener(msg + "Complete", function listener() {
- count--;
- if (count == 0) {
- Services.ppmm.removeMessageListener(msg + "Complete", listener);
- resolve();
- }
- });
- Services.ppmm.broadcastAsyncMessage(msg, data);
- });
- }
-
- runManifest(manifest) {
- // Strip leading slashes from web_accessible_resources.
- let strippedWebAccessibleResources = [];
- if (manifest.web_accessible_resources) {
- strippedWebAccessibleResources = manifest.web_accessible_resources.map(path => path.replace(/^\/+/, ""));
- }
-
- this.webAccessibleResources = new MatchGlobs(strippedWebAccessibleResources);
-
- let promises = [];
- for (let directive in manifest) {
- if (manifest[directive] !== null) {
- promises.push(Management.emit(`manifest_${directive}`, directive, this, manifest));
- }
- }
-
- let data = Services.ppmm.initialProcessData;
- if (!data["Extension:Extensions"]) {
- data["Extension:Extensions"] = [];
- }
- let serial = this.serialize();
- data["Extension:Extensions"].push(serial);
-
- return this.broadcast("Extension:Startup", serial).then(() => {
- return Promise.all(promises);
- });
- }
-
- callOnClose(obj) {
- this.onShutdown.add(obj);
- }
-
- forgetOnClose(obj) {
- this.onShutdown.delete(obj);
- }
-
- get builtinMessages() {
- return new Map([
- ["@@extension_id", this.uuid],
- ]);
- }
-
- // Reads the locale file for the given Gecko-compatible locale code, or if
- // no locale is given, the available locale closest to the UI locale.
- // Sets the currently selected locale on success.
- initLocale(locale = undefined) {
- // Ugh.
- let super_ = super.initLocale.bind(this);
-
- return Task.spawn(function* () {
- if (locale === undefined) {
- let locales = yield this.promiseLocales();
-
- let localeList = Array.from(locales.keys(), locale => {
- return {name: locale, locales: [locale]};
- });
-
- let match = Locale.findClosestLocale(localeList);
- locale = match ? match.name : this.defaultLocale;
- }
-
- return super_(locale);
- }.bind(this));
- }
-
- startup() {
- let started = false;
- return this.readManifest().then(() => {
- ExtensionManagement.startupExtension(this.uuid, this.addonData.resourceURI, this);
- started = true;
-
- if (!this.hasShutdown) {
- return this.initLocale();
- }
- }).then(() => {
- if (this.errors.length) {
- return Promise.reject({errors: this.errors});
- }
-
- if (this.hasShutdown) {
- return;
- }
-
- GlobalManager.init(this);
-
- // The "startup" Management event sent on the extension instance itself
- // is emitted just before the Management "startup" event,
- // and it is used to run code that needs to be executed before
- // any of the "startup" listeners.
- this.emit("startup", this);
- Management.emit("startup", this);
-
- return this.runManifest(this.manifest);
- }).then(() => {
- Management.emit("ready", this);
- }).catch(e => {
- dump(`Extension error: ${e.message} ${e.filename || e.fileName}:${e.lineNumber} :: ${e.stack || new Error().stack}\n`);
- Cu.reportError(e);
-
- if (started) {
- ExtensionManagement.shutdownExtension(this.uuid);
- }
-
- this.cleanupGeneratedFile();
-
- throw e;
- });
- }
-
- cleanupGeneratedFile() {
- if (!this.cleanupFile) {
- return;
- }
-
- let file = this.cleanupFile;
- this.cleanupFile = null;
-
- Services.obs.removeObserver(this, "xpcom-shutdown");
-
- this.broadcast("Extension:FlushJarCache", {path: file.path}).then(() => {
- // We can't delete this file until everyone using it has
- // closed it (because Windows is dumb). So we wait for all the
- // child processes (including the parent) to flush their JAR
- // caches. These caches may keep the file open.
- file.remove(false);
- });
- }
-
- shutdown() {
- this.hasShutdown = true;
- if (!this.manifest) {
- ExtensionManagement.shutdownExtension(this.uuid);
-
- this.cleanupGeneratedFile();
- return;
- }
-
- GlobalManager.uninit(this);
-
- for (let obj of this.onShutdown) {
- obj.close();
- }
-
- for (let api of this.apis) {
- api.destroy();
- }
-
- ParentAPIManager.shutdownExtension(this.id);
-
- Management.emit("shutdown", this);
-
- Services.ppmm.broadcastAsyncMessage("Extension:Shutdown", {id: this.id});
-
- MessageChannel.abortResponses({extensionId: this.id});
-
- ExtensionManagement.shutdownExtension(this.uuid);
-
- this.cleanupGeneratedFile();
- }
-
- observe(subject, topic, data) {
- if (topic == "xpcom-shutdown") {
- this.cleanupGeneratedFile();
- }
- }
-
- hasPermission(perm) {
- let match = /^manifest:(.*)/.exec(perm);
- if (match) {
- return this.manifest[match[1]] != null;
- }
-
- return this.permissions.has(perm);
- }
-
- get name() {
- return this.manifest.name;
- }
-};
copy from toolkit/components/extensions/Extension.jsm
copy to toolkit/components/extensions/ExtensionTestCommon.jsm
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/ExtensionTestCommon.jsm
@@ -1,1097 +1,54 @@
/* 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 = ["Extension", "ExtensionData"];
-
-/* globals Extension ExtensionData */
+/* exported ExtensionTestCommon, MockExtension */
-/*
- * This file is the main entry point for extensions. When an extension
- * loads, its bootstrap.js file creates a Extension instance
- * and calls .startup() on it. It calls .shutdown() when the extension
- * unloads. Extension manages any extension-specific state in
- * the chrome process.
- */
+this.EXPORTED_SYMBOLS = ["ExtensionTestCommon", "MockExtension"];
-const Ci = Components.interfaces;
-const Cc = Components.classes;
-const Cu = Components.utils;
-const Cr = Components.results;
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.importGlobalProperties(["TextEncoder"]);
+Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-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, "ExtensionStorage",
- "resource://gre/modules/ExtensionStorage.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Extension",
+ "resource://gre/modules/Extension.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent",
+ "resource://gre/modules/ExtensionParent.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");
-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, "NetUtil",
- "resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
- "resource://gre/modules/PrivateBrowsingUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
- "resource://gre/modules/Preferences.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "require",
- "resource://devtools/shared/Loader.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
- "resource://gre/modules/Schemas.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Task",
- "resource://gre/modules/Task.jsm");
-Cu.import("resource://gre/modules/ExtensionContent.jsm");
-Cu.import("resource://gre/modules/ExtensionManagement.jsm");
+XPCOMUtils.defineLazyGetter(this, "apiManager",
+ () => ExtensionParent.apiManager);
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator");
-const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
-const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
-const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
-
-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,
- EventEmitter,
- LocaleData,
- SchemaAPIManager,
- SpreadArgs,
- defineLazyGetter,
+const {
flushJarCache,
instanceOf,
} = ExtensionUtils;
XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
-const LOGGER_ID_BASE = "addons.webextension.";
-const UUID_MAP_PREF = "extensions.webextensions.uuids";
-const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall";
-const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall";
-
-const COMMENT_REGEXP = new RegExp(String.raw`
- ^
- (
- (?:
- [^"\n] |
- " (?:[^"\\\n] | \\.)* "
- )*?
- )
-
- //.*
- `.replace(/\s+/g, ""), "gm");
-
-var GlobalManager;
-var ParentAPIManager;
-
-// This object loads the ext-*.js scripts that define the extension API.
-var Management = new class extends SchemaAPIManager {
- constructor() {
- super("main");
- this.initialized = null;
- }
-
- // Loads all the ext-*.js scripts currently registered.
- lazyInit() {
- if (this.initialized) {
- return this.initialized;
- }
-
- // Load order matters here. The base manifest defines types which are
- // extended by other schemas, so needs to be loaded first.
- let promise = Schemas.load(BASE_SCHEMA).then(() => {
- let promises = [];
- for (let [/* name */, url] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCHEMAS)) {
- promises.push(Schemas.load(url));
- }
- for (let url of schemaURLs) {
- promises.push(Schemas.load(url));
- }
- return Promise.all(promises);
- });
-
- for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS)) {
- this.loadScript(value);
- }
-
- this.initialized = promise;
- return this.initialized;
- }
-
- registerSchemaAPI(namespace, envType, getAPI) {
- if (envType == "addon_parent" || envType == "content_parent") {
- super.registerSchemaAPI(namespace, envType, getAPI);
- }
- }
-}();
-
-// Subscribes to messages related to the extension messaging API and forwards it
-// to the relevant message manager. The "sender" field for the `onMessage` and
-// `onConnect` events are updated if needed.
-let ProxyMessenger = {
- _initialized: false,
- init() {
- if (this._initialized) {
- return;
- }
- this._initialized = true;
-
- // TODO(robwu): When addons move to a separate process, we should use the
- // parent process manager(s) of the addon process(es) instead of the
- // in-process one.
- let pipmm = Services.ppmm.getChildAt(0);
- // Listen on the global frame message manager because content scripts send
- // and receive extension messages via their frame.
- // Listen on the parent process message manager because `runtime.connect`
- // and `runtime.sendMessage` requests must be delivered to all frames in an
- // addon process (by the API contract).
- // And legacy addons are not associated with a frame, so that is another
- // reason for having a parent process manager here.
- let messageManagers = [Services.mm, pipmm];
-
- MessageChannel.addListener(messageManagers, "Extension:Connect", this);
- MessageChannel.addListener(messageManagers, "Extension:Message", this);
- MessageChannel.addListener(messageManagers, "Extension:Port:Disconnect", this);
- MessageChannel.addListener(messageManagers, "Extension:Port:PostMessage", this);
- },
-
- receiveMessage({target, messageName, channelId, sender, recipient, data, responseType}) {
- if (recipient.toNativeApp) {
- let {childId, toNativeApp} = recipient;
- if (messageName == "Extension:Message") {
- let context = ParentAPIManager.getContextById(childId);
- return new NativeApp(context, toNativeApp).sendMessage(data);
- }
- if (messageName == "Extension:Connect") {
- let context = ParentAPIManager.getContextById(childId);
- NativeApp.onConnectNative(context, target.messageManager, data.portId, sender, toNativeApp);
- return true;
- }
- // "Extension:Port:Disconnect" and "Extension:Port:PostMessage" for
- // native messages are handled by NativeApp.
- return;
- }
- let extension = GlobalManager.extensionMap.get(sender.extensionId);
- let receiverMM = this._getMessageManagerForRecipient(recipient);
- if (!extension || !receiverMM) {
- return Promise.reject({
- result: MessageChannel.RESULT_NO_HANDLER,
- message: "No matching message handler for the given recipient.",
- });
- }
-
- if ((messageName == "Extension:Message" ||
- messageName == "Extension:Connect") &&
- Management.global.tabGetSender) {
- // From ext-tabs.js, undefined on Android.
- Management.global.tabGetSender(extension, target, sender);
- }
- return MessageChannel.sendMessage(receiverMM, messageName, data, {
- sender,
- recipient,
- responseType,
- });
- },
-
- /**
- * @param {object} recipient An object that was passed to
- * `MessageChannel.sendMessage`.
- * @returns {object|null} The message manager matching the recipient if found.
- */
- _getMessageManagerForRecipient(recipient) {
- let {extensionId, tabId} = recipient;
- // tabs.sendMessage / tabs.connect
- if (tabId) {
- // `tabId` being set implies that the tabs API is supported, so we don't
- // need to check whether `TabManager` exists.
- let tab = Management.global.TabManager.getTab(tabId, null, null);
- return tab && tab.linkedBrowser.messageManager;
- }
-
- // runtime.sendMessage / runtime.connect
- if (extensionId) {
- // TODO(robwu): map the extensionId to the addon parent process's message
- // manager when they run in a separate process.
- return Services.ppmm.getChildAt(0);
- }
-
- return null;
- },
-};
-
-class BrowserDocshellFollower {
- /**
- * Follows the <browser> belonging to the `xulBrowser`'s current docshell.
- *
- * @param {XULElement} xulBrowser A <browser> tag.
- * @param {function} onBrowserChange Called when the <browser> changes.
- */
- constructor(xulBrowser, onBrowserChange) {
- this.xulBrowser = xulBrowser;
- this.onBrowserChange = onBrowserChange;
-
- xulBrowser.addEventListener("SwapDocShells", this);
- }
-
- destroy() {
- this.xulBrowser.removeEventListener("SwapDocShells", this);
- this.xulBrowser = null;
- }
-
- handleEvent({detail: otherBrowser}) {
- this.xulBrowser.removeEventListener("SwapDocShells", this);
- this.xulBrowser = otherBrowser;
- this.xulBrowser.addEventListener("SwapDocShells", this);
- this.onBrowserChange(otherBrowser);
- }
-}
-
-class ProxyContext extends BaseContext {
- constructor(envType, extension, params, xulBrowser, principal) {
- super(envType, extension);
-
- this.uri = NetUtil.newURI(params.url);
-
- this.incognito = params.incognito;
-
- // 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));
-
- Object.defineProperty(this, "principal", {
- value: principal, enumerable: true, configurable: true,
- });
-
- this.listenerProxies = new Map();
-
- Management.emit("proxy-context-load", this);
- }
-
- 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.
- if (!browser.messageManager) {
- throw new Error("BrowserDocshellFollower: The new browser has no message manager");
- }
-
- this.currentMessageManager = browser.messageManager;
- }
-
- shutdown() {
- this.unload();
- }
-
- unload() {
- if (this.unloaded) {
- 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.
- this.xulBrowser = xulBrowser;
- }
-
- // The window that contains this context. This may change due to moving tabs.
- get xulWindow() {
- return this.xulBrowser.ownerGlobal;
- }
-
- get windowId() {
- if (!Management.global.WindowManager || this.viewType == "background") {
- return;
- }
- // viewType popup or tab:
- return Management.global.WindowManager.getId(this.xulWindow);
- }
-
- get tabId() {
- if (!Management.global.TabManager) {
- return; // Not yet supported on Android.
- }
- let {gBrowser} = this.xulBrowser.ownerGlobal;
- let tab = gBrowser && gBrowser.getTabForBrowser(this.xulBrowser);
- return tab && Management.global.TabManager.getId(tab);
- }
-
- onBrowserChange(browser) {
- super.onBrowserChange(browser);
- this.xulBrowser = browser;
- }
-
- shutdown() {
- Management.emit("page-shutdown", this);
- super.shutdown();
- }
-}
-
-function findPathInObject(obj, path, printErrors = true) {
- let parent;
- for (let elt of path.split(".")) {
- if (!obj || !(elt in obj)) {
- if (printErrors) {
- Cu.reportError(`WebExtension API ${path} not found (it may be unimplemented by Firefox).`);
- }
- return null;
- }
-
- parent = obj;
- obj = obj[elt];
- }
-
- if (typeof obj === "function") {
- return obj.bind(parent);
- }
- return obj;
-}
-
-ParentAPIManager = {
- proxyContexts: new Map(),
-
- init() {
- Services.obs.addObserver(this, "message-manager-close", false);
-
- Services.mm.addMessageListener("API:CreateProxyContext", this);
- Services.mm.addMessageListener("API:CloseProxyContext", this, true);
- Services.mm.addMessageListener("API:Call", this);
- Services.mm.addMessageListener("API:AddListener", this);
- Services.mm.addMessageListener("API:RemoveListener", this);
- },
-
- // "message-manager-close" observer.
- observe(subject, topic, data) {
- let mm = subject;
- for (let [childId, context] of this.proxyContexts) {
- if (context.currentMessageManager == mm) {
- this.closeProxyContext(childId);
- }
- }
- },
-
- shutdownExtension(extensionId) {
- for (let [childId, context] of this.proxyContexts) {
- if (context.extension.id == extensionId) {
- context.shutdown();
- this.proxyContexts.delete(childId);
- }
- }
- },
-
- receiveMessage({name, data, target}) {
- switch (name) {
- case "API:CreateProxyContext":
- this.createProxyContext(data, target);
- break;
-
- case "API:CloseProxyContext":
- this.closeProxyContext(data.childId);
- break;
-
- case "API:Call":
- this.call(data, target);
- break;
-
- case "API:AddListener":
- this.addListener(data, target);
- break;
-
- case "API:RemoveListener":
- this.removeListener(data);
- break;
- }
- },
-
- createProxyContext(data, target) {
- let {envType, extensionId, childId, principal} = data;
- if (this.proxyContexts.has(childId)) {
- throw new Error("A WebExtension context with the given ID already exists!");
- }
-
- let extension = GlobalManager.getExtension(extensionId);
- if (!extension) {
- throw new Error(`No WebExtension found with ID ${extensionId}`);
- }
-
- let context;
- if (envType == "addon_parent") {
- // Privileged addon contexts can only be loaded in documents whose main
- // frame is also the same addon.
- if (principal.URI.prePath != extension.baseURI.prePath ||
- !target.contentPrincipal.subsumes(principal)) {
- throw new Error(`Refused to create privileged WebExtension context for ${principal.URI.spec}`);
- }
- context = new ExtensionChildProxyContext(envType, extension, data, target);
- } else if (envType == "content_parent") {
- context = new ProxyContext(envType, extension, data, target, principal);
- } else {
- throw new Error(`Invalid WebExtension context envType: ${envType}`);
- }
- this.proxyContexts.set(childId, context);
- },
-
- closeProxyContext(childId) {
- let context = this.proxyContexts.get(childId);
- if (!context) {
- return;
- }
- context.unload();
- this.proxyContexts.delete(childId);
- },
-
- call(data, target) {
- let context = this.getContextById(data.childId);
- if (context.currentMessageManager !== target.messageManager) {
- Cu.reportError("WebExtension warning: Message manager unexpectedly changed");
- }
-
- try {
- let args = Cu.cloneInto(data.args, context.sandbox);
- let result = findPathInObject(context.apiObj, data.path)(...args);
-
- if (data.callId) {
- result = result || Promise.resolve();
-
- result.then(result => {
- result = result instanceof SpreadArgs ? [...result] : [result];
-
- context.currentMessageManager.sendAsyncMessage("API:CallResult", {
- childId: data.childId,
- callId: data.callId,
- result,
- });
- }, error => {
- error = context.normalizeError(error);
- context.currentMessageManager.sendAsyncMessage("API:CallResult", {
- childId: data.childId,
- callId: data.callId,
- error: {message: error.message},
- });
- });
- }
- } catch (e) {
- if (data.callId) {
- let error = context.normalizeError(e);
- context.currentMessageManager.sendAsyncMessage("API:CallResult", {
- childId: data.childId,
- callId: data.callId,
- error: {message: error.message},
- });
- } else {
- Cu.reportError(e);
- }
- }
- },
-
- addListener(data, target) {
- let context = this.getContextById(data.childId);
- if (context.currentMessageManager !== target.messageManager) {
- Cu.reportError("WebExtension warning: Message manager unexpectedly changed");
- }
-
- function listener(...listenerArgs) {
- context.currentMessageManager.sendAsyncMessage("API:RunListener", {
- childId: data.childId,
- path: data.path,
- args: listenerArgs,
- });
- }
-
- context.listenerProxies.set(data.path, listener);
-
- let args = Cu.cloneInto(data.args, context.sandbox);
- findPathInObject(context.apiObj, data.path).addListener(listener, ...args);
- },
-
- removeListener(data) {
- let context = this.getContextById(data.childId);
- let listener = context.listenerProxies.get(data.path);
- findPathInObject(context.apiObj, data.path).removeListener(listener);
- },
-
- getContextById(childId) {
- let context = this.proxyContexts.get(childId);
- if (!context) {
- let error = new Error("WebExtension context not found!");
- Cu.reportError(error);
- throw error;
- }
- return context;
- },
-};
-
-ParentAPIManager.init();
-
-// All moz-extension URIs use a machine-specific UUID rather than the
-// extension's own ID in the host component. This makes it more
-// difficult for web pages to detect whether a user has a given add-on
-// installed (by trying to load a moz-extension URI referring to a
-// web_accessible_resource from the extension). UUIDMap.get()
-// returns the UUID for a given add-on ID.
-var UUIDMap = {
- _read() {
- let pref = Preferences.get(UUID_MAP_PREF, "{}");
- try {
- return JSON.parse(pref);
- } catch (e) {
- Cu.reportError(`Error parsing ${UUID_MAP_PREF}.`);
- return {};
- }
- },
-
- _write(map) {
- Preferences.set(UUID_MAP_PREF, JSON.stringify(map));
- },
-
- get(id, create = true) {
- let map = this._read();
-
- if (id in map) {
- return map[id];
- }
-
- let uuid = null;
- if (create) {
- uuid = uuidGen.generateUUID().number;
- uuid = uuid.slice(1, -1); // Strip { and } off the UUID.
-
- map[id] = uuid;
- this._write(map);
- }
- return uuid;
- },
-
- remove(id) {
- let map = this._read();
- delete map[id];
- this._write(map);
- },
-};
-
-// This is the old interface that UUIDMap replaced, to be removed when
-// the references listed in bug 1291399 are updated.
-/* exported getExtensionUUID */
-function getExtensionUUID(id) {
- return UUIDMap.get(id, true);
-}
-
-// For extensions that have called setUninstallURL(), send an event
-// so the browser can display the URL.
-var UninstallObserver = {
- initialized: false,
-
- init() {
- if (!this.initialized) {
- AddonManager.addAddonListener(this);
- XPCOMUtils.defineLazyPreferenceGetter(this, "leaveStorage", LEAVE_STORAGE_PREF, false);
- XPCOMUtils.defineLazyPreferenceGetter(this, "leaveUuid", LEAVE_UUID_PREF, false);
- this.initialized = true;
- }
- },
-
- onUninstalling(addon) {
- let extension = GlobalManager.extensionMap.get(addon.id);
- if (extension) {
- // Let any other interested listeners respond
- // (e.g., display the uninstall URL)
- Management.emit("uninstall", extension);
- }
- },
-
- onUninstalled(addon) {
- let uuid = UUIDMap.get(addon.id, false);
- if (!uuid) {
- return;
- }
-
- if (!this.leaveStorage) {
- // Clear browser.local.storage
- ExtensionStorage.clear(addon.id);
-
- // Clear any IndexedDB storage created by the extension
- let baseURI = NetUtil.newURI(`moz-extension://${uuid}/`);
- let principal = Services.scriptSecurityManager.createCodebasePrincipal(
- baseURI, {addonId: addon.id}
- );
- Services.qms.clearStoragesForPrincipal(principal);
-
- // Clear localStorage created by the extension
- let attrs = JSON.stringify({addonId: addon.id});
- Services.obs.notifyObservers(null, "clear-origin-attributes-data", attrs);
- }
-
- if (!this.leaveUuid) {
- // Clear the entry in the UUID map
- UUIDMap.remove(addon.id);
- }
- },
-};
-
-// Responsible for loading extension APIs into the right globals.
-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) {
- UninstallObserver.init();
- ProxyMessenger.init();
- 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) {
- Management.off("extension-browser-inserted", this._onExtensionBrowser);
- this.initialized = false;
- }
- },
-
- _onExtensionBrowser(type, browser) {
- browser.messageManager.loadFrameScript(`data:,
- Components.utils.import("resource://gre/modules/ExtensionContent.jsm");
- ExtensionContent.init(this);
- addEventListener("unload", function() {
- ExtensionContent.uninit(this);
- });
- `, false);
- },
-
- getExtension(extensionId) {
- return this.extensionMap.get(extensionId);
- },
-
- injectInObject(context, isChromeCompat, dest) {
- Management.generateAPIs(context, dest);
- SchemaAPIManager.generateAPIs(context, context.extension.apis, dest);
- },
-};
-
-// 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.
-//
-// No functionality of this class is guaranteed to work before
-// |readManifest| has been called, and completed.
-this.ExtensionData = class {
- constructor(rootURI) {
- this.rootURI = rootURI;
-
- this.manifest = null;
- this.id = null;
- this.uuid = null;
- this.localeData = null;
- this._promiseLocales = null;
-
- this.apiNames = new Set();
- this.dependencies = new Set();
- this.permissions = new Set();
-
- this.errors = [];
- }
-
- get builtinMessages() {
- return null;
- }
-
- get logger() {
- let id = this.id || "<unknown>";
- return Log.repository.getLogger(LOGGER_ID_BASE + id);
- }
-
- // Report an error about the extension's manifest file.
- manifestError(message) {
- this.packagingError(`Reading manifest: ${message}`);
- }
-
- // Report an error about the extension's general packaging.
- packagingError(message) {
- this.errors.push(message);
- this.logger.error(`Loading extension '${this.id}': ${message}`);
- }
-
- /**
- * Returns the moz-extension: URL for the given path within this
- * extension.
- *
- * Must not be called unless either the `id` or `uuid` property has
- * already been set.
- *
- * @param {string} path The path portion of the URL.
- * @returns {string}
- */
- getURL(path = "") {
- if (!(this.id || this.uuid)) {
- throw new Error("getURL may not be called before an `id` or `uuid` has been set");
- }
- if (!this.uuid) {
- this.uuid = UUIDMap.get(this.id);
- }
- return `moz-extension://${this.uuid}/${path}`;
- }
-
- readDirectory(path) {
- return Task.spawn(function* () {
- if (this.rootURI instanceof Ci.nsIFileURL) {
- let uri = NetUtil.newURI(this.rootURI.resolve("./" + path));
- let fullPath = uri.QueryInterface(Ci.nsIFileURL).file.path;
-
- let iter = new OS.File.DirectoryIterator(fullPath);
- let results = [];
-
- try {
- yield iter.forEach(entry => {
- results.push(entry);
- });
- } catch (e) {
- // Always return a list, even if the directory does not exist (or is
- // not a directory) for symmetry with the ZipReader behavior.
- }
- iter.close();
-
- return results;
- }
-
- // FIXME: We need a way to do this without main thread IO.
-
- let uri = this.rootURI.QueryInterface(Ci.nsIJARURI);
-
- let file = uri.JARFile.QueryInterface(Ci.nsIFileURL).file;
- let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader);
- zipReader.open(file);
- try {
- let results = [];
-
- // Normalize the directory path.
- path = `${uri.JAREntry}/${path}`;
- path = path.replace(/\/\/+/g, "/").replace(/^\/|\/$/g, "") + "/";
-
- // Escape pattern metacharacters.
- let pattern = path.replace(/[[\]()?*~|$\\]/g, "\\$&");
-
- let enumerator = zipReader.findEntries(pattern + "*");
- while (enumerator.hasMore()) {
- let name = enumerator.getNext();
- if (!name.startsWith(path)) {
- throw new Error("Unexpected ZipReader entry");
- }
-
- // The enumerator returns the full path of all entries.
- // Trim off the leading path, and filter out entries from
- // subdirectories.
- name = name.slice(path.length);
- if (name && !/\/./.test(name)) {
- results.push({
- name: name.replace("/", ""),
- isDir: name.endsWith("/"),
- });
- }
- }
-
- return results;
- } finally {
- zipReader.close();
- }
- }.bind(this));
- }
-
- readJSON(path) {
- return new Promise((resolve, reject) => {
- let uri = this.rootURI.resolve(`./${path}`);
-
- NetUtil.asyncFetch({uri, loadUsingSystemPrincipal: true}, (inputStream, status) => {
- if (!Components.isSuccessCode(status)) {
- // Convert status code to a string
- let e = Components.Exception("", status);
- reject(new Error(`Error while loading '${uri}' (${e.name})`));
- return;
- }
- try {
- let text = NetUtil.readInputStreamToString(inputStream, inputStream.available(),
- {charset: "utf-8"});
-
- text = text.replace(COMMENT_REGEXP, "$1");
-
- resolve(JSON.parse(text));
- } catch (e) {
- reject(e);
- }
- });
- });
- }
-
- // Reads the extension's |manifest.json| file, and stores its
- // parsed contents in |this.manifest|.
- readManifest() {
- return Promise.all([
- this.readJSON("manifest.json"),
- Management.lazyInit(),
- ]).then(([manifest]) => {
- this.manifest = manifest;
- this.rawManifest = manifest;
-
- if (manifest && manifest.default_locale) {
- return this.initLocale();
- }
- }).then(() => {
- let context = {
- url: this.baseURI && this.baseURI.spec,
-
- principal: this.principal,
-
- logError: error => {
- this.logger.warn(`Loading extension '${this.id}': Reading manifest: ${error}`);
- },
-
- preprocessors: {},
- };
-
- if (this.localeData) {
- context.preprocessors.localize = (value, context) => this.localize(value);
- }
-
- let normalized = Schemas.normalize(this.manifest, "manifest.WebExtensionManifest", context);
- if (normalized.error) {
- this.manifestError(normalized.error);
- } else {
- this.manifest = normalized.value;
- }
-
- try {
- // Do not override the add-on id that has been already assigned.
- if (!this.id && this.manifest.applications.gecko.id) {
- this.id = this.manifest.applications.gecko.id;
- }
- } catch (e) {
- // Errors are handled by the type checks above.
- }
-
- let permissions = this.manifest.permissions || [];
-
- let whitelist = [];
- for (let perm of permissions) {
- this.permissions.add(perm);
-
- let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm);
- if (!match) {
- whitelist.push(perm);
- } else if (match[1] == "experiments" && match[2]) {
- this.apiNames.add(match[2]);
- }
- }
- this.whiteListedHosts = new MatchPattern(whitelist);
-
- for (let api of this.apiNames) {
- this.dependencies.add(`${api}@experiments.addons.mozilla.org`);
- }
-
- return this.manifest;
- });
- }
-
- localizeMessage(...args) {
- return this.localeData.localizeMessage(...args);
- }
-
- localize(...args) {
- return this.localeData.localize(...args);
- }
-
- // If a "default_locale" is specified in that manifest, returns it
- // as a Gecko-compatible locale string. Otherwise, returns null.
- get defaultLocale() {
- if (this.manifest.default_locale != null) {
- return this.normalizeLocaleCode(this.manifest.default_locale);
- }
-
- return null;
- }
-
- // Normalizes a Chrome-compatible locale code to the appropriate
- // Gecko-compatible variant. Currently, this means simply
- // replacing underscores with hyphens.
- normalizeLocaleCode(locale) {
- return String.replace(locale, /_/g, "-");
- }
-
- // Reads the locale file for the given Gecko-compatible locale code, and
- // stores its parsed contents in |this.localeMessages.get(locale)|.
- readLocaleFile(locale) {
- return Task.spawn(function* () {
- let locales = yield this.promiseLocales();
- let dir = locales.get(locale) || locale;
- let file = `_locales/${dir}/messages.json`;
-
- try {
- let messages = yield this.readJSON(file);
- return this.localeData.addLocale(locale, messages, this);
- } catch (e) {
- this.packagingError(`Loading locale file ${file}: ${e}`);
- return new Map();
- }
- }.bind(this));
- }
-
- // Reads the list of locales available in the extension, and returns a
- // Promise which resolves to a Map upon completion.
- // Each map key is a Gecko-compatible locale code, and each value is the
- // "_locales" subdirectory containing that locale:
- //
- // Map(gecko-locale-code -> locale-directory-name)
- promiseLocales() {
- if (!this._promiseLocales) {
- this._promiseLocales = Task.spawn(function* () {
- let locales = new Map();
-
- let entries = yield this.readDirectory("_locales");
- for (let file of entries) {
- if (file.isDir) {
- let locale = this.normalizeLocaleCode(file.name);
- locales.set(locale, file.name);
- }
- }
-
- this.localeData = new LocaleData({
- defaultLocale: this.defaultLocale,
- locales,
- builtinMessages: this.builtinMessages,
- });
-
- return locales;
- }.bind(this));
- }
-
- return this._promiseLocales;
- }
-
- // Reads the locale messages for all locales, and returns a promise which
- // resolves to a Map of locale messages upon completion. Each key in the map
- // is a Gecko-compatible locale code, and each value is a locale data object
- // as returned by |readLocaleFile|.
- initAllLocales() {
- return Task.spawn(function* () {
- let locales = yield this.promiseLocales();
-
- yield Promise.all(Array.from(locales.keys(),
- locale => this.readLocaleFile(locale)));
-
- let defaultLocale = this.defaultLocale;
- if (defaultLocale) {
- if (!locales.has(defaultLocale)) {
- this.manifestError('Value for "default_locale" property must correspond to ' +
- 'a directory in "_locales/". Not found: ' +
- JSON.stringify(`_locales/${this.manifest.default_locale}/`));
- }
- } else if (locales.size) {
- this.manifestError('The "default_locale" property is required when a ' +
- '"_locales/" directory is present.');
- }
-
- return this.localeData.messages;
- }.bind(this));
- }
-
- // Reads the locale file for the given Gecko-compatible locale code, or the
- // default locale if no locale code is given, and sets it as the currently
- // selected locale on success.
- //
- // Pre-loads the default locale for fallback message processing, regardless
- // of the locale specified.
- //
- // If no locales are unavailable, resolves to |null|.
- initLocale(locale = this.defaultLocale) {
- return Task.spawn(function* () {
- if (locale == null) {
- return null;
- }
-
- let promises = [this.readLocaleFile(locale)];
-
- let {defaultLocale} = this;
- if (locale != defaultLocale && !this.localeData.has(defaultLocale)) {
- promises.push(this.readLocaleFile(defaultLocale));
- }
-
- let results = yield Promise.all(promises);
-
- this.localeData.selectedLocale = locale;
- return results[0];
- }.bind(this));
- }
-};
-
/**
* A skeleton Extension-like object, used for testing, which installs an
* add-on via the add-on manager when startup() is called, and
* uninstalles it on shutdown().
*
* @param {string} id
* @param {nsIFile} file
@@ -1104,24 +61,24 @@ class MockExtension {
this.file = file;
this.rootURI = rootURI;
this.installType = installType;
this.addon = null;
let promiseEvent = eventName => new Promise(resolve => {
let onstartup = (msg, extension) => {
if (this.addon && extension.id == this.addon.id) {
- Management.off(eventName, onstartup);
+ apiManager.off(eventName, onstartup);
this.id = extension.id;
this._extension = extension;
resolve(extension);
}
};
- Management.on(eventName, onstartup);
+ apiManager.on(eventName, onstartup);
});
this._extension = null;
this._extensionPromise = promiseEvent("startup");
this._readyPromise = promiseEvent("ready");
}
testMessage(...args) {
@@ -1171,61 +128,17 @@ class MockExtension {
}
cleanupGeneratedFile() {
flushJarCache(this.file);
return OS.File.remove(this.file.path);
}
}
-let _browserUpdated = false;
-
-// We create one instance of this class per extension. |addonData|
-// comes directly from bootstrap.js when initializing.
-this.Extension = class extends ExtensionData {
- constructor(addonData, startupReason) {
- super(addonData.resourceURI);
-
- this.uuid = UUIDMap.get(addonData.id);
-
- if (addonData.cleanupFile) {
- Services.obs.addObserver(this, "xpcom-shutdown", false);
- this.cleanupFile = addonData.cleanupFile || null;
- delete addonData.cleanupFile;
- }
-
- this.addonData = addonData;
- this.startupReason = startupReason;
-
- this.id = addonData.id;
- this.baseURI = NetUtil.newURI(this.getURL("")).QueryInterface(Ci.nsIURL);
- this.principal = this.createPrincipal();
-
- this.onStartup = null;
-
- this.hasShutdown = false;
- this.onShutdown = new Set();
-
- this.uninstallURL = null;
-
- this.apis = [];
- this.whiteListedHosts = null;
- this.webAccessibleResources = null;
-
- this.emitter = new EventEmitter();
- }
-
- static set browserUpdated(updated) {
- _browserUpdated = updated;
- }
-
- static get browserUpdated() {
- return _browserUpdated;
- }
-
+class ExtensionTestCommon {
/**
* This code is designed to make it easy to test a WebExtension
* without creating a bunch of files. Everything is contained in a
* single JSON blob.
*
* Properties:
* "background": "<JS code>"
* A script to be loaded as the background script.
@@ -1418,269 +331,9 @@ this.Extension = class extends Extension
}
return new Extension({
id,
resourceURI: jarURI,
cleanupFile: file,
});
}
-
- on(hook, f) {
- return this.emitter.on(hook, f);
- }
-
- off(hook, f) {
- return this.emitter.off(hook, f);
- }
-
- emit(...args) {
- return this.emitter.emit(...args);
- }
-
- testMessage(...args) {
- Management.emit("test-message", this, ...args);
- }
-
- createPrincipal(uri = this.baseURI) {
- return Services.scriptSecurityManager.createCodebasePrincipal(
- uri, {addonId: this.id});
- }
-
- // Checks that the given URL is a child of our baseURI.
- isExtensionURL(url) {
- let uri = Services.io.newURI(url, null, null);
-
- let common = this.baseURI.getCommonBaseSpec(uri);
- return common == this.baseURI.spec;
- }
-
- readManifest() {
- return super.readManifest().then(manifest => {
- if (AppConstants.RELEASE_OR_BETA) {
- return manifest;
- }
-
- // Load Experiments APIs that this extension depends on.
- return Promise.all(
- Array.from(this.apiNames, api => ExtensionAPIs.load(api))
- ).then(apis => {
- for (let API of apis) {
- this.apis.push(new API(this));
- }
-
- return manifest;
- });
- });
- }
-
- // Representation of the extension to send to content
- // processes. This should include anything the content process might
- // need.
- serialize() {
- return {
- id: this.id,
- uuid: this.uuid,
- manifest: this.manifest,
- resourceURL: this.addonData.resourceURI.spec,
- baseURL: this.baseURI.spec,
- content_scripts: this.manifest.content_scripts || [], // eslint-disable-line camelcase
- webAccessibleResources: this.webAccessibleResources.serialize(),
- whiteListedHosts: this.whiteListedHosts.serialize(),
- localeData: this.localeData.serialize(),
- permissions: this.permissions,
- principal: this.principal,
- };
- }
-
- broadcast(msg, data) {
- return new Promise(resolve => {
- let count = Services.ppmm.childCount;
- Services.ppmm.addMessageListener(msg + "Complete", function listener() {
- count--;
- if (count == 0) {
- Services.ppmm.removeMessageListener(msg + "Complete", listener);
- resolve();
- }
- });
- Services.ppmm.broadcastAsyncMessage(msg, data);
- });
- }
-
- runManifest(manifest) {
- // Strip leading slashes from web_accessible_resources.
- let strippedWebAccessibleResources = [];
- if (manifest.web_accessible_resources) {
- strippedWebAccessibleResources = manifest.web_accessible_resources.map(path => path.replace(/^\/+/, ""));
- }
-
- this.webAccessibleResources = new MatchGlobs(strippedWebAccessibleResources);
-
- let promises = [];
- for (let directive in manifest) {
- if (manifest[directive] !== null) {
- promises.push(Management.emit(`manifest_${directive}`, directive, this, manifest));
- }
- }
-
- let data = Services.ppmm.initialProcessData;
- if (!data["Extension:Extensions"]) {
- data["Extension:Extensions"] = [];
- }
- let serial = this.serialize();
- data["Extension:Extensions"].push(serial);
-
- return this.broadcast("Extension:Startup", serial).then(() => {
- return Promise.all(promises);
- });
- }
-
- callOnClose(obj) {
- this.onShutdown.add(obj);
- }
-
- forgetOnClose(obj) {
- this.onShutdown.delete(obj);
- }
-
- get builtinMessages() {
- return new Map([
- ["@@extension_id", this.uuid],
- ]);
- }
-
- // Reads the locale file for the given Gecko-compatible locale code, or if
- // no locale is given, the available locale closest to the UI locale.
- // Sets the currently selected locale on success.
- initLocale(locale = undefined) {
- // Ugh.
- let super_ = super.initLocale.bind(this);
-
- return Task.spawn(function* () {
- if (locale === undefined) {
- let locales = yield this.promiseLocales();
-
- let localeList = Array.from(locales.keys(), locale => {
- return {name: locale, locales: [locale]};
- });
-
- let match = Locale.findClosestLocale(localeList);
- locale = match ? match.name : this.defaultLocale;
- }
-
- return super_(locale);
- }.bind(this));
- }
-
- startup() {
- let started = false;
- return this.readManifest().then(() => {
- ExtensionManagement.startupExtension(this.uuid, this.addonData.resourceURI, this);
- started = true;
-
- if (!this.hasShutdown) {
- return this.initLocale();
- }
- }).then(() => {
- if (this.errors.length) {
- return Promise.reject({errors: this.errors});
- }
-
- if (this.hasShutdown) {
- return;
- }
-
- GlobalManager.init(this);
-
- // The "startup" Management event sent on the extension instance itself
- // is emitted just before the Management "startup" event,
- // and it is used to run code that needs to be executed before
- // any of the "startup" listeners.
- this.emit("startup", this);
- Management.emit("startup", this);
-
- return this.runManifest(this.manifest);
- }).then(() => {
- Management.emit("ready", this);
- }).catch(e => {
- dump(`Extension error: ${e.message} ${e.filename || e.fileName}:${e.lineNumber} :: ${e.stack || new Error().stack}\n`);
- Cu.reportError(e);
-
- if (started) {
- ExtensionManagement.shutdownExtension(this.uuid);
- }
-
- this.cleanupGeneratedFile();
-
- throw e;
- });
- }
-
- cleanupGeneratedFile() {
- if (!this.cleanupFile) {
- return;
- }
-
- let file = this.cleanupFile;
- this.cleanupFile = null;
-
- Services.obs.removeObserver(this, "xpcom-shutdown");
-
- this.broadcast("Extension:FlushJarCache", {path: file.path}).then(() => {
- // We can't delete this file until everyone using it has
- // closed it (because Windows is dumb). So we wait for all the
- // child processes (including the parent) to flush their JAR
- // caches. These caches may keep the file open.
- file.remove(false);
- });
- }
-
- shutdown() {
- this.hasShutdown = true;
- if (!this.manifest) {
- ExtensionManagement.shutdownExtension(this.uuid);
-
- this.cleanupGeneratedFile();
- return;
- }
-
- GlobalManager.uninit(this);
-
- for (let obj of this.onShutdown) {
- obj.close();
- }
-
- for (let api of this.apis) {
- api.destroy();
- }
-
- ParentAPIManager.shutdownExtension(this.id);
-
- Management.emit("shutdown", this);
-
- Services.ppmm.broadcastAsyncMessage("Extension:Shutdown", {id: this.id});
-
- MessageChannel.abortResponses({extensionId: this.id});
-
- ExtensionManagement.shutdownExtension(this.uuid);
-
- this.cleanupGeneratedFile();
- }
-
- observe(subject, topic, data) {
- if (topic == "xpcom-shutdown") {
- this.cleanupGeneratedFile();
- }
- }
-
- hasPermission(perm) {
- let match = /^manifest:(.*)/.exec(perm);
- if (match) {
- return this.manifest[match[1]] != null;
- }
-
- return this.permissions.has(perm);
- }
-
- get name() {
- return this.manifest.name;
- }
-};
+}
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -27,20 +27,16 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyModuleGetter(this, "Locale",
"resource://gre/modules/Locale.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
"resource://gre/modules/MessageChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
"resource://gre/modules/Preferences.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
- "resource://gre/modules/PrivateBrowsingUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
- "resource://gre/modules/PromiseUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
"@mozilla.org/content/style-sheet-service;1",
"nsIStyleSheetService");
function getConsole() {
@@ -178,350 +174,16 @@ class DefaultMap extends Map {
class SpreadArgs extends Array {
constructor(args) {
super();
this.push(...args);
}
}
-let gContextId = 0;
-
-class BaseContext {
- constructor(envType, extension) {
- this.envType = envType;
- this.onClose = new Set();
- this.checkedLastError = false;
- this._lastError = null;
- this.contextId = `${++gContextId}-${Services.appinfo.uniqueProcessID}`;
- this.unloaded = false;
- this.extension = extension;
- this.jsonSandbox = null;
- this.active = true;
- this.incognito = null;
- this.messageManager = null;
- this.docShell = null;
- this.contentWindow = null;
- this.innerWindowID = 0;
- }
-
- setContentWindow(contentWindow) {
- 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);
-
- if (this.incognito == null) {
- this.incognito = PrivateBrowsingUtils.isContentWindowPrivate(contentWindow);
- }
-
- MessageChannel.setupMessageManagers([this.messageManager]);
-
- let onPageShow = event => {
- if (!event || event.target === document) {
- this.docShell = docShell;
- this.contentWindow = contentWindow;
- this.active = true;
- }
- };
- let onPageHide = event => {
- if (!event || event.target === document) {
- // Put this off until the next tick.
- Promise.resolve().then(() => {
- this.docShell = null;
- this.contentWindow = null;
- this.active = false;
- });
- }
- };
-
- onPageShow();
- contentWindow.addEventListener("pagehide", onPageHide, true);
- contentWindow.addEventListener("pageshow", onPageShow, true);
- this.callOnClose({
- close: () => {
- onPageHide();
- if (this.active) {
- contentWindow.removeEventListener("pagehide", onPageHide, true);
- contentWindow.removeEventListener("pageshow", onPageShow, true);
- }
- },
- });
- }
-
- get cloneScope() {
- throw new Error("Not implemented");
- }
-
- get principal() {
- throw new Error("Not implemented");
- }
-
- runSafe(...args) {
- if (this.unloaded) {
- Cu.reportError("context.runSafe called after context unloaded");
- } else if (!this.active) {
- Cu.reportError("context.runSafe called while context is inactive");
- } else {
- return runSafeSync(this, ...args);
- }
- }
-
- runSafeWithoutClone(...args) {
- if (this.unloaded) {
- Cu.reportError("context.runSafeWithoutClone called after context unloaded");
- } else if (!this.active) {
- Cu.reportError("context.runSafeWithoutClone called while context is inactive");
- } else {
- return runSafeSyncWithoutClone(...args);
- }
- }
-
- checkLoadURL(url, options = {}) {
- let ssm = Services.scriptSecurityManager;
-
- let flags = ssm.STANDARD;
- if (!options.allowScript) {
- flags |= ssm.DISALLOW_SCRIPT;
- }
- if (!options.allowInheritsPrincipal) {
- flags |= ssm.DISALLOW_INHERIT_PRINCIPAL;
- }
- if (options.dontReportErrors) {
- flags |= ssm.DONT_REPORT_ERRORS;
- }
-
- try {
- ssm.checkLoadURIStrWithPrincipal(this.principal, url, flags);
- } catch (e) {
- return false;
- }
- return true;
- }
-
- /**
- * Safely call JSON.stringify() on an object that comes from an
- * extension.
- *
- * @param {array<any>} args Arguments for JSON.stringify()
- * @returns {string} The stringified representation of obj
- */
- jsonStringify(...args) {
- if (!this.jsonSandbox) {
- this.jsonSandbox = Cu.Sandbox(this.principal, {
- sameZoneAs: this.cloneScope,
- wantXrays: false,
- });
- }
-
- return Cu.waiveXrays(this.jsonSandbox.JSON).stringify(...args);
- }
-
- callOnClose(obj) {
- this.onClose.add(obj);
- }
-
- forgetOnClose(obj) {
- this.onClose.delete(obj);
- }
-
- /**
- * A wrapper around MessageChannel.sendMessage which adds the extension ID
- * to the recipient object, and ensures replies are not processed after the
- * context has been unloaded.
- *
- * @param {nsIMessageManager} target
- * @param {string} messageName
- * @param {object} data
- * @param {object} [options]
- * @param {object} [options.sender]
- * @param {object} [options.recipient]
- *
- * @returns {Promise}
- */
- sendMessage(target, messageName, data, options = {}) {
- options.recipient = options.recipient || {};
- options.sender = options.sender || {};
-
- // TODO(robwu): This should not unconditionally be overwritten once we
- // support onMessageExternal / onConnectExternal (bugzil.la/1258360).
- options.recipient.extensionId = this.extension.id;
- options.sender.extensionId = this.extension.id;
- options.sender.contextId = this.contextId;
-
- return MessageChannel.sendMessage(target, messageName, data, options);
- }
-
- get lastError() {
- this.checkedLastError = true;
- return this._lastError;
- }
-
- set lastError(val) {
- this.checkedLastError = false;
- this._lastError = val;
- }
-
- /**
- * Normalizes the given error object for use by the target scope. If
- * the target is an error object which belongs to that scope, it is
- * returned as-is. If it is an ordinary object with a `message`
- * property, it is converted into an error belonging to the target
- * scope. If it is an Error object which does *not* belong to the
- * clone scope, it is reported, and converted to an unexpected
- * exception error.
- *
- * @param {Error|object} error
- * @returns {Error}
- */
- normalizeError(error) {
- if (error instanceof this.cloneScope.Error) {
- return error;
- }
- let message;
- if (instanceOf(error, "Object") || error instanceof ExtensionError) {
- message = error.message;
- } else if (typeof error == "object" &&
- this.principal.subsumes(Cu.getObjectPrincipal(error))) {
- message = error.message;
- } else {
- Cu.reportError(error);
- }
- message = message || "An unexpected error occurred";
- return new this.cloneScope.Error(message);
- }
-
- /**
- * Sets the value of `.lastError` to `error`, calls the given
- * callback, and reports an error if the value has not been checked
- * when the callback returns.
- *
- * @param {object} error An object with a `message` property. May
- * optionally be an `Error` object belonging to the target scope.
- * @param {function} callback The callback to call.
- * @returns {*} The return value of callback.
- */
- withLastError(error, callback) {
- this.lastError = this.normalizeError(error);
- try {
- return callback();
- } finally {
- if (!this.checkedLastError) {
- Cu.reportError(`Unchecked lastError value: ${this.lastError}`);
- }
- this.lastError = null;
- }
- }
-
- /**
- * Wraps the given promise so it can be safely returned to extension
- * code in this context.
- *
- * If `callback` is provided, however, it is used as a completion
- * function for the promise, and no promise is returned. In this case,
- * the callback is called when the promise resolves or rejects. In the
- * latter case, `lastError` is set to the rejection value, and the
- * callback function must check `browser.runtime.lastError` or
- * `extension.runtime.lastError` in order to prevent it being reported
- * to the console.
- *
- * @param {Promise} promise The promise with which to wrap the
- * callback. May resolve to a `SpreadArgs` instance, in which case
- * each element will be used as a separate argument.
- *
- * Unless the promise object belongs to the cloneScope global, its
- * resolution value is cloned into cloneScope prior to calling the
- * `callback` function or resolving the wrapped promise.
- *
- * @param {function} [callback] The callback function to wrap
- *
- * @returns {Promise|undefined} If callback is null, a promise object
- * belonging to the target scope. Otherwise, undefined.
- */
- wrapPromise(promise, callback = null) {
- let runSafe = this.runSafe.bind(this);
- if (promise instanceof this.cloneScope.Promise) {
- runSafe = this.runSafeWithoutClone.bind(this);
- }
-
- if (callback) {
- promise.then(
- args => {
- if (this.unloaded) {
- dump(`Promise resolved after context unloaded\n`);
- } else if (!this.active) {
- dump(`Promise resolved while context is inactive\n`);
- } else if (args instanceof SpreadArgs) {
- runSafe(callback, ...args);
- } else {
- runSafe(callback, args);
- }
- },
- error => {
- this.withLastError(error, () => {
- if (this.unloaded) {
- dump(`Promise rejected after context unloaded\n`);
- } else if (!this.active) {
- dump(`Promise rejected while context is inactive\n`);
- } else {
- this.runSafeWithoutClone(callback);
- }
- });
- });
- } else {
- return new this.cloneScope.Promise((resolve, reject) => {
- promise.then(
- value => {
- if (this.unloaded) {
- dump(`Promise resolved after context unloaded\n`);
- } else if (!this.active) {
- dump(`Promise resolved while context is inactive\n`);
- } else if (value instanceof SpreadArgs) {
- runSafe(resolve, value.length == 1 ? value[0] : value);
- } else {
- runSafe(resolve, value);
- }
- },
- value => {
- if (this.unloaded) {
- dump(`Promise rejected after context unloaded: ${value && value.message}\n`);
- } else if (!this.active) {
- dump(`Promise rejected while context is inactive: ${value && value.message}\n`);
- } else {
- this.runSafeWithoutClone(reject, this.normalizeError(value));
- }
- });
- });
- }
- }
-
- unload() {
- this.unloaded = true;
-
- MessageChannel.abortResponses({
- extensionId: this.extension.id,
- contextId: this.contextId,
- });
-
- for (let obj of this.onClose) {
- obj.close();
- }
- }
-
- /**
- * A simple proxy for unload(), for use with callOnClose().
- */
- close() {
- this.unload();
- }
-}
-
// Manages icon details for toolbar buttons in the |pageAction| and
// |browserAction| APIs.
let IconDetails = {
// Normalizes the various acceptable input formats into an object
// with icon size as key and icon URL as value.
//
// If a context is specified (function is called from an extension):
// Throws an error if an invalid icon size was provided or the
@@ -1204,384 +866,23 @@ function promiseObserved(topic, test = (
Services.obs.removeObserver(observer, topic);
resolve({subject, data});
}
};
Services.obs.addObserver(observer, topic, false);
});
}
-
-/*
- * Messaging primitives.
- */
-
-let gNextPortId = 1;
-
-/**
- * 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.
- */
-function Port(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) => {
- if (!sender.contextId) {
- Cu.reportError("Missing sender.contextId in message to Port");
- return false;
- }
- 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);
-}
-
-Port.prototype = {
- 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 EventManager(this.context, "Port.onDisconnect", fire => {
- return this.registerOnDisconnect(error => {
- portError = error && this.context.normalizeError(error);
- fire.withoutClone(portObj);
- });
- }).api(),
- onMessage: new EventManager(this.context, "Port.onMessage", fire => {
- return this.registerOnMessage(msg => {
- msg = Cu.cloneInto(msg, this.context.cloneScope);
- fire.withoutClone(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);
- },
-
- close() {
- this.disconnect();
- },
-};
-
function getMessageManager(target) {
if (target instanceof Ci.nsIFrameLoaderOwner) {
return target.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager;
}
return target.QueryInterface(Ci.nsIMessageSender);
}
-/**
- * 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.
- */
-function Messenger(context, messageManagers, sender, filter, optionalFilter) {
- this.context = context;
- this.messageManagers = messageManagers;
- this.sender = sender;
- this.filter = filter;
- this.optionalFilter = optionalFilter;
-}
-
-Messenger.prototype = {
- _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);
- },
-
- onMessage(name) {
- return new SingletonEventManager(this.context, name, callback => {
- 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;
- },
-
- 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 = callback(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();
- },
-
- connectGetRawPort(messageManager, name, recipient) {
- let portId = `${gNextPortId++}-${Services.appinfo.uniqueProcessID}`;
- let port = new Port(this.context, messageManager, this.messageManagers, name, portId, null, recipient);
- let msg = {name, portId};
- this._sendMessage(messageManager, "Extension:Connect", msg, recipient)
- .catch(e => {
- if (e.result === MessageChannel.RESULT_NO_HANDLER) {
- e = {message: "Could not establish connection. Receiving end does not exist."};
- } else if (e.result === MessageChannel.RESULT_DISCONNECTED) {
- e = null;
- }
- port.disconnectByOtherEnd(e);
- });
- return port;
- },
-
- connect(messageManager, name, recipient) {
- let port = this.connectGetRawPort(messageManager, name, recipient);
- return port.api();
- },
-
- onConnect(name) {
- return new SingletonEventManager(this.context, name, callback => {
- 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;
- },
-
- 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);
- this.context.runSafeWithoutClone(callback, port.api());
- return true;
- },
- };
-
- MessageChannel.addListener(this.messageManagers, "Extension:Connect", listener);
- return () => {
- MessageChannel.removeListener(this.messageManagers, "Extension:Connect", listener);
- };
- }).api();
- },
-};
-
function flushJarCache(jarFile) {
Services.obs.notifyObservers(jarFile, "flush-cache-entry", null);
}
const PlatformInfo = Object.freeze({
os: (function() {
let os = AppConstants.platform;
if (os == "macosx") {
@@ -1609,562 +910,16 @@ function detectLanguage(text) {
language: lang.languageCode,
percentage: lang.percent,
};
}),
}));
}
/**
- * An object that runs the implementation of a schema API. Instantiations of
- * this interfaces are used by Schemas.jsm.
- *
- * @interface
- */
-class SchemaAPIInterface {
- /**
- * Calls this as a function that returns its return value.
- *
- * @abstract
- * @param {Array} args The parameters for the function.
- * @returns {*} The return value of the invoked function.
- */
- callFunction(args) {
- throw new Error("Not implemented");
- }
-
- /**
- * Calls this as a function and ignores its return value.
- *
- * @abstract
- * @param {Array} args The parameters for the function.
- */
- callFunctionNoReturn(args) {
- throw new Error("Not implemented");
- }
-
- /**
- * Calls this as a function that completes asynchronously.
- *
- * @abstract
- * @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.
- */
- callAsyncFunction(args, callback) {
- throw new Error("Not implemented");
- }
-
- /**
- * Retrieves the value of this as a property.
- *
- * @abstract
- * @returns {*} The value of the property.
- */
- getProperty() {
- throw new Error("Not implemented");
- }
-
- /**
- * Assigns the value to this as property.
- *
- * @abstract
- * @param {string} value The new value of the property.
- */
- setProperty(value) {
- throw new Error("Not implemented");
- }
-
- /**
- * Registers a `listener` to this as an event.
- *
- * @abstract
- * @param {function} listener The callback to be called when the event fires.
- * @param {Array} args Extra parameters for EventManager.addListener.
- * @see EventManager.addListener
- */
- addListener(listener, args) {
- throw new Error("Not implemented");
- }
-
- /**
- * Checks whether `listener` is listening to this as an event.
- *
- * @abstract
- * @param {function} listener The event listener.
- * @returns {boolean} Whether `listener` is registered with this as an event.
- * @see EventManager.hasListener
- */
- hasListener(listener) {
- throw new Error("Not implemented");
- }
-
- /**
- * Unregisters `listener` from this as an event.
- *
- * @abstract
- * @param {function} listener The event listener.
- * @see EventManager.removeListener
- */
- removeListener(listener) {
- throw new Error("Not implemented");
- }
-}
-
-/**
- * An object that runs a locally implemented API.
- */
-class LocalAPIImplementation extends SchemaAPIInterface {
- /**
- * Constructs an implementation of the `name` method or property of `pathObj`.
- *
- * @param {object} pathObj The object containing the member with name `name`.
- * @param {string} name The name of the implemented member.
- * @param {BaseContext} context The context in which the schema is injected.
- */
- constructor(pathObj, name, context) {
- super();
- this.pathObj = pathObj;
- this.name = name;
- this.context = context;
- }
-
- callFunction(args) {
- return this.pathObj[this.name](...args);
- }
-
- callFunctionNoReturn(args) {
- this.pathObj[this.name](...args);
- }
-
- callAsyncFunction(args, callback) {
- let promise;
- try {
- promise = this.pathObj[this.name](...args) || Promise.resolve();
- } catch (e) {
- promise = Promise.reject(e);
- }
- return this.context.wrapPromise(promise, callback);
- }
-
- getProperty() {
- return this.pathObj[this.name];
- }
-
- setProperty(value) {
- this.pathObj[this.name] = value;
- }
-
- addListener(listener, args) {
- try {
- this.pathObj[this.name].addListener.call(null, listener, ...args);
- } catch (e) {
- throw this.context.normalizeError(e);
- }
- }
-
- hasListener(listener) {
- return this.pathObj[this.name].hasListener.call(null, listener);
- }
-
- removeListener(listener) {
- this.pathObj[this.name].removeListener.call(null, listener);
- }
-}
-
-let nextId = 1;
-
-/**
- * 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;
- }
-
- callFunctionNoReturn(args) {
- this.childApiManager.callParentFunctionNoReturn(this.path, args);
- }
-
- callAsyncFunction(args, callback) {
- return this.childApiManager.callParentAsyncFunction(this.path, args, callback);
- }
-
- addListener(listener, args) {
- let set = this.childApiManager.listeners.get(this.path);
- if (!set) {
- set = new Set();
- this.childApiManager.listeners.set(this.path, set);
- }
-
- set.add(listener);
-
- if (set.size == 1) {
- args = args.slice(1);
-
- this.childApiManager.messageManager.sendAsyncMessage("API:AddListener", {
- childId: this.childApiManager.id,
- path: this.path,
- args,
- });
- }
- }
-
- removeListener(listener) {
- let set = this.childApiManager.listeners.get(this.path);
- if (!set) {
- return;
- }
- set.delete(listener);
-
- if (set.size == 0) {
- this.childApiManager.messageManager.sendAsyncMessage("API:RemoveListener", {
- childId: this.childApiManager.id,
- path: this.path,
- });
- }
- }
-
- hasListener(listener) {
- let set = this.childApiManager.listeners.get(this.path);
- return set ? set.has(listener) : false;
- }
-}
-
-// 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
-// Extension.jsm. It handles asynchronous function calls as well as
-// event listeners.
-class ChildAPIManager {
- constructor(context, messageManager, localApis, contextData) {
- this.context = context;
- this.messageManager = messageManager;
-
- // 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 = localApis;
-
- let id = String(context.extension.id) + "." + String(context.contextId);
- this.id = id;
-
- let data = {childId: id, extensionId: context.extension.id, principal: context.principal};
- Object.assign(data, contextData);
-
- messageManager.addMessageListener("API:RunListener", this);
- messageManager.addMessageListener("API:CallResult", this);
-
- // Map[path -> Set[listener]]
- // path is, e.g., "runtime.onMessage".
- this.listeners = new Map();
-
- // Map[callId -> Deferred]
- this.callPromises = new Map();
-
- this.createProxyContextInConstructor(data);
- }
-
- createProxyContextInConstructor(data) {
- this.messageManager.sendAsyncMessage("API:CreateProxyContext", data);
- }
-
- receiveMessage({name, data}) {
- if (data.childId != this.id) {
- return;
- }
-
- switch (name) {
- case "API:RunListener":
- let listeners = this.listeners.get(data.path);
- for (let callback of listeners) {
- runSafe(this.context, callback, ...data.args);
- }
- 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 = nextId++;
- 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) {
- let parsed = /^(.+)\.(on[A-Z][^.]+)$/.exec(path);
- if (!parsed) {
- throw new Error("getParentEvent: Invalid event name: " + path);
- }
- let [, namespace, name] = parsed;
- 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});
- }
-
- 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;
- }
- return true;
- }
-
- getImplementation(namespace, name) {
- let pathObj = this.localApis;
- if (pathObj) {
- for (let part of namespace.split(".")) {
- pathObj = pathObj[part];
- if (!pathObj) {
- break;
- }
- }
- if (pathObj && name in pathObj) {
- return new LocalAPIImplementation(pathObj, 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);
- }
-}
-
-/**
- * This object loads the ext-*.js scripts that define the extension API.
- *
- * This class instance is shared with the scripts that it loads, so that the
- * ext-*.js scripts and the instantiator can communicate with each other.
- */
-class SchemaAPIManager extends EventEmitter {
- /**
- * @param {string} processType
- * "main" - The main, one and only chrome browser process.
- * "addon" - An addon process.
- * "content" - A content process.
- */
- constructor(processType) {
- super();
- this.processType = processType;
- this.global = this._createExtGlobal();
- this._scriptScopes = [];
- this._schemaApis = {
- addon_parent: [],
- addon_child: [],
- content_parent: [],
- content_child: [],
- };
- }
-
- /**
- * Create a global object that is used as the shared global for all ext-*.js
- * scripts that are loaded via `loadScript`.
- *
- * @returns {object} A sandbox that is used as the global by `loadScript`.
- */
- _createExtGlobal() {
- let global = Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), {
- wantXrays: false,
- sandboxName: `Namespace of ext-*.js scripts for ${this.processType}`,
- });
-
- Object.assign(global, {global, Cc, Ci, Cu, Cr, XPCOMUtils, extensions: this});
-
- XPCOMUtils.defineLazyGetter(global, "console", getConsole);
-
- XPCOMUtils.defineLazyModuleGetter(global, "require",
- "resource://devtools/shared/Loader.jsm");
-
- return global;
- }
-
- /**
- * Load an ext-*.js script. The script runs in its own scope, if it wishes to
- * share state with another script it can assign to the `global` variable. If
- * it wishes to communicate with this API manager, use `extensions`.
- *
- * @param {string} scriptUrl The URL of the ext-*.js script.
- */
- loadScript(scriptUrl) {
- // Create the object in the context of the sandbox so that the script runs
- // in the sandbox's context instead of here.
- let scope = Cu.createObjectIn(this.global);
-
- Services.scriptloader.loadSubScript(scriptUrl, scope, "UTF-8");
-
- // Save the scope to avoid it being garbage collected.
- this._scriptScopes.push(scope);
- }
-
- /**
- * Called by an ext-*.js script to register an API.
- *
- * @param {string} namespace The API namespace.
- * Intended to match the namespace of the generated API, but not used at
- * the moment - see bugzil.la/1295774.
- * @param {string} envType Restricts the API to contexts that run in the
- * given environment. Must be one of the following:
- * - "addon_parent" - addon APIs that runs in the main process.
- * - "addon_child" - addon APIs that runs in an addon process.
- * - "content_parent" - content script APIs that runs in the main process.
- * - "content_child" - content script APIs that runs in a content process.
- * @param {function(BaseContext)} getAPI A function that returns an object
- * that will be merged with |chrome| and |browser|. The next example adds
- * the create, update and remove methods to the tabs API.
- *
- * registerSchemaAPI("tabs", "addon_parent", (context) => ({
- * tabs: { create, update },
- * }));
- * registerSchemaAPI("tabs", "addon_parent", (context) => ({
- * tabs: { remove },
- * }));
- */
- registerSchemaAPI(namespace, envType, getAPI) {
- this._schemaApis[envType].push({namespace, getAPI});
- }
-
- /**
- * Exports all registered scripts to `obj`.
- *
- * @param {BaseContext} context The context for which the API bindings are
- * generated.
- * @param {object} obj The destination of the API.
- */
- generateAPIs(context, obj) {
- let apis = this._schemaApis[context.envType];
- if (!apis) {
- Cu.reportError(`No APIs have been registered for ${context.envType}`);
- return;
- }
- SchemaAPIManager.generateAPIs(context, apis, obj);
- }
-
- /**
- * Mash together all the APIs from `apis` into `obj`.
- *
- * @param {BaseContext} context The context for which the API bindings are
- * generated.
- * @param {Array} apis A list of objects, see `registerSchemaAPI`.
- * @param {object} obj The destination of the API.
- */
- static generateAPIs(context, apis, obj) {
- // Recursively copy properties from source to dest.
- function copy(dest, source) {
- for (let prop in source) {
- let desc = Object.getOwnPropertyDescriptor(source, prop);
- if (typeof(desc.value) == "object") {
- if (!(prop in dest)) {
- dest[prop] = {};
- }
- copy(dest[prop], source[prop]);
- } else {
- Object.defineProperty(dest, prop, desc);
- }
- }
- }
-
- for (let api of apis) {
- if (Schemas.checkPermissions(api.namespace, context.extension)) {
- api = api.getAPI(context);
- copy(obj, api);
- }
- }
- }
-}
-
-/**
* Convert any of several different representations of a date/time to a Date object.
* Accepts several formats:
* a Date object, an ISO8601 string, or a number of milliseconds since the epoch as
* either a number or a string.
*
* @param {Date|string|number} date
* The date to convert.
* @returns {Date}
@@ -2218,46 +973,61 @@ function defineLazyGetter(object, prop,
},
set(value) {
redefine(this, value);
},
});
}
+function findPathInObject(obj, path, printErrors = true) {
+ let parent;
+ for (let elt of path.split(".")) {
+ if (!obj || !(elt in obj)) {
+ if (printErrors) {
+ Cu.reportError(`WebExtension API ${path} not found (it may be unimplemented by Firefox).`);
+ }
+ return null;
+ }
+
+ parent = obj;
+ obj = obj[elt];
+ }
+
+ if (typeof obj === "function") {
+ return obj.bind(parent);
+ }
+ return obj;
+}
+
this.ExtensionUtils = {
defineLazyGetter,
detectLanguage,
extend,
+ findPathInObject,
flushJarCache,
getConsole,
getInnerWindowID,
+ getMessageManager,
ignoreEvent,
injectAPI,
instanceOf,
normalizeTime,
promiseDocumentLoaded,
promiseDocumentReady,
promiseEvent,
promiseObserved,
runSafe,
runSafeSync,
runSafeSyncWithoutClone,
runSafeWithoutClone,
stylesheetMap,
- BaseContext,
DefaultMap,
DefaultWeakMap,
EventEmitter,
EventManager,
ExtensionError,
IconDetails,
- LocalAPIImplementation,
LocaleData,
- Messenger,
- Port,
PlatformInfo,
- SchemaAPIInterface,
SingletonEventManager,
SpreadArgs,
- ChildAPIManager,
- SchemaAPIManager,
};
--- a/toolkit/components/extensions/LegacyExtensionsUtils.jsm
+++ b/toolkit/components/extensions/LegacyExtensionsUtils.jsm
@@ -17,22 +17,26 @@ const {classes: Cc, interfaces: Ci, util
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Extension",
"resource://gre/modules/Extension.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/ExtensionChild.jsm");
+Cu.import("resource://gre/modules/ExtensionCommon.jsm");
var {
BaseContext,
+} = ExtensionCommon;
+
+var {
Messenger,
-} = ExtensionUtils;
+} = 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.
--- a/toolkit/components/extensions/NativeMessaging.jsm
+++ b/toolkit/components/extensions/NativeMessaging.jsm
@@ -12,18 +12,18 @@ const {classes: Cc, interfaces: Ci, util
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {});
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
"resource://gre/modules/AsyncShutdown.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
- "resource://gre/modules/ExtensionUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionChild",
+ "resource://gre/modules/ExtensionChild.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Subprocess",
"resource://gre/modules/Subprocess.jsm");
@@ -226,17 +226,17 @@ this.NativeApp = class extends EventEmit
* and receive messages from the port's creator.
* @param {string} portId A unique internal ID that identifies the port.
* @param {object} sender The object describing the creator of the connection
* request.
* @param {string} application The name of the native messaging host.
*/
static onConnectNative(context, messageManager, portId, sender, application) {
let app = new NativeApp(context, application);
- let port = new ExtensionUtils.Port(context, messageManager, [messageManager], "", portId, sender, sender);
+ let port = new ExtensionChild.Port(context, messageManager, [messageManager], "", portId, sender, sender);
app.once("disconnect", (what, err) => port.disconnect(err));
/* eslint-disable mozilla/balanced-listeners */
app.on("message", (what, msg) => port.postMessage(msg));
/* eslint-enable mozilla/balanced-listeners */
port.registerOnMessage(msg => app.send(msg));
port.registerOnDisconnect(msg => app.close());
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -3,31 +3,34 @@
# 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/.
EXTRA_JS_MODULES += [
'Extension.jsm',
'ExtensionAPI.jsm',
'ExtensionChild.jsm',
+ 'ExtensionCommon.jsm',
'ExtensionContent.jsm',
'ExtensionManagement.jsm',
+ 'ExtensionParent.jsm',
'ExtensionStorage.jsm',
'ExtensionUtils.jsm',
'LegacyExtensionsUtils.jsm',
'MessageChannel.jsm',
'NativeMessaging.jsm',
'Schemas.jsm',
]
EXTRA_COMPONENTS += [
'extensions-toolkit.manifest',
]
TESTING_JS_MODULES += [
+ 'ExtensionTestCommon.jsm',
'ExtensionXPCShellUtils.jsm',
]
DIRS += ['schemas']
JAR_MANIFESTS += ['jar.mn']
MOCHITEST_MANIFESTS += ['test/mochitest/mochitest.ini']
--- a/toolkit/components/extensions/test/xpcshell/test_ext_apimanager.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_apimanager.js
@@ -1,15 +1,19 @@
/* 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";
-let {ExtensionUtils: {SchemaAPIManager}} = Cu.import("resource://gre/modules/ExtensionUtils.jsm", {});
+Cu.import("resource://gre/modules/ExtensionCommon.jsm");
+
+const {
+ SchemaAPIManager,
+} = ExtensionCommon;
this.unknownvar = "Some module-global var";
var gUniqueId = 0;
// SchemaAPIManager's loadScript uses loadSubScript to load a script. This
// requires a local (resource://) URL. So create such a temporary URL for
// testing.
--- a/toolkit/components/extensions/test/xpcshell/test_ext_contexts.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contexts.js
@@ -1,17 +1,22 @@
"use strict";
const global = this;
Cu.import("resource://gre/modules/Timer.jsm");
+Cu.import("resource://gre/modules/ExtensionCommon.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
var {
BaseContext,
+} = ExtensionCommon;
+
+var {
EventManager,
SingletonEventManager,
} = ExtensionUtils;
class StubContext extends BaseContext {
constructor() {
let fakeExtension = {id: "test@web.extension"};
super("testEnv", fakeExtension);
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
@@ -1,15 +1,15 @@
"use strict";
Components.utils.import("resource://gre/modules/Schemas.jsm");
Components.utils.import("resource://gre/modules/BrowserUtils.jsm");
-Components.utils.import("resource://gre/modules/ExtensionUtils.jsm");
+Components.utils.import("resource://gre/modules/ExtensionCommon.jsm");
-let {LocalAPIImplementation, SchemaAPIInterface} = ExtensionUtils;
+let {LocalAPIImplementation, SchemaAPIInterface} = ExtensionCommon;
let json = [
{namespace: "testing",
properties: {
PROP1: {value: 20},
prop2: {type: "string"},
prop3: {
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_api_injection.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_api_injection.js
@@ -1,17 +1,17 @@
"use strict";
-Components.utils.import("resource://gre/modules/ExtensionUtils.jsm");
+Components.utils.import("resource://gre/modules/ExtensionCommon.jsm");
Components.utils.import("resource://gre/modules/Schemas.jsm");
let {
BaseContext,
SchemaAPIManager,
-} = ExtensionUtils;
+} = ExtensionCommon;
let nestedNamespaceJson = [
{
"namespace": "backgroundAPI.testnamespace",
"functions": [
{
"name": "create",
"type": "function",
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_async.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_async.js
@@ -1,14 +1,14 @@
"use strict";
-Components.utils.import("resource://gre/modules/ExtensionUtils.jsm");
+Components.utils.import("resource://gre/modules/ExtensionCommon.jsm");
Components.utils.import("resource://gre/modules/Schemas.jsm");
-let {BaseContext, LocalAPIImplementation} = ExtensionUtils;
+let {BaseContext, LocalAPIImplementation} = ExtensionCommon;
let schemaJson = [
{
namespace: "testnamespace",
functions: [{
name: "one_required",
type: "function",
parameters: [{
--- a/toolkit/components/extensions/test/xpcshell/test_native_messaging.js
+++ b/toolkit/components/extensions/test/xpcshell/test_native_messaging.js
@@ -1,14 +1,14 @@
"use strict";
/* global OS, HostManifestManager, NativeApp */
Cu.import("resource://gre/modules/AppConstants.jsm");
Cu.import("resource://gre/modules/AsyncShutdown.jsm");
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/ExtensionCommon.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/Schemas.jsm");
Cu.import("resource://gre/modules/Services.jsm");
const {Subprocess, SubprocessImpl} = Cu.import("resource://gre/modules/Subprocess.jsm");
Cu.import("resource://gre/modules/NativeMessaging.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
let registry = null;
@@ -79,17 +79,17 @@ let context = {
jsonStringify(...args) { return JSON.stringify(...args); },
cloneScope: global,
logError() {},
preprocessors: {},
callOnClose: () => {},
forgetOnClose: () => {},
};
-class MockContext extends ExtensionUtils.BaseContext {
+class MockContext extends ExtensionCommon.BaseContext {
constructor(extensionId) {
let fakeExtension = {id: extensionId};
super("testEnv", fakeExtension);
this.sandbox = Cu.Sandbox(global);
}
get cloneScope() {
return global;