552 lines
17 KiB
JavaScript
552 lines
17 KiB
JavaScript
/* 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 module contains code for managing APIs that need to run in the
|
|
* parent process, and handles the parent side of operations that need
|
|
* to be proxied from ExtensionChild.jsm.
|
|
*/
|
|
|
|
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
|
|
|
/* exported ExtensionParent */
|
|
|
|
this.EXPORTED_SYMBOLS = ["ExtensionParent"];
|
|
|
|
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, "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, "Schemas",
|
|
"resource://gre/modules/Schemas.jsm");
|
|
|
|
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
|
|
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
|
|
|
var {
|
|
BaseContext,
|
|
SchemaAPIManager,
|
|
} = ExtensionCommon;
|
|
|
|
var {
|
|
MessageManagerProxy,
|
|
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");
|
|
}
|
|
|
|
let GlobalManager;
|
|
let ParentAPIManager;
|
|
let ProxyMessenger;
|
|
|
|
// This object loads the ext-*.js scripts that define the extension API.
|
|
let apiManager = 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.
|
|
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") &&
|
|
apiManager.global.tabGetSender) {
|
|
// From ext-tabs.js, undefined on Android.
|
|
apiManager.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 = 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);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* The proxied parent side of a context in ExtensionChild.jsm, for the
|
|
* parent side of a proxied API.
|
|
*/
|
|
class ProxyContextParent 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.messageManagerProxy = new MessageManagerProxy(xulBrowser);
|
|
|
|
Object.defineProperty(this, "principal", {
|
|
value: principal, enumerable: true, configurable: true,
|
|
});
|
|
|
|
this.listenerProxies = new Map();
|
|
|
|
apiManager.emit("proxy-context-load", this);
|
|
}
|
|
|
|
get cloneScope() {
|
|
return this.sandbox;
|
|
}
|
|
|
|
get xulBrowser() {
|
|
return this.messageManagerProxy.eventTarget;
|
|
}
|
|
|
|
get parentMessageManager() {
|
|
return this.messageManagerProxy.messageManager;
|
|
}
|
|
|
|
shutdown() {
|
|
this.unload();
|
|
}
|
|
|
|
unload() {
|
|
if (this.unloaded) {
|
|
return;
|
|
}
|
|
this.messageManagerProxy.dispose();
|
|
super.unload();
|
|
apiManager.emit("proxy-context-unload", this);
|
|
}
|
|
}
|
|
|
|
defineLazyGetter(ProxyContextParent.prototype, "apiObj", function() {
|
|
let obj = {};
|
|
GlobalManager.injectInObject(this, false, obj);
|
|
return obj;
|
|
});
|
|
|
|
defineLazyGetter(ProxyContextParent.prototype, "sandbox", function() {
|
|
return Cu.Sandbox(this.principal);
|
|
});
|
|
|
|
/**
|
|
* The parent side of proxied API context for extension content script
|
|
* running in ExtensionContent.jsm.
|
|
*/
|
|
class ContentScriptContextParent extends ProxyContextParent {
|
|
}
|
|
|
|
/**
|
|
* The parent side of proxied API context for extension page, such as a
|
|
* background script, a tab page, or a popup, running in
|
|
* ExtensionChild.jsm.
|
|
*/
|
|
class ExtensionPageContextParent extends ProxyContextParent {
|
|
constructor(envType, extension, params, xulBrowser) {
|
|
super(envType, extension, params, xulBrowser, extension.principal);
|
|
|
|
this.viewType = params.viewType;
|
|
}
|
|
|
|
// The window that contains this context. This may change due to moving tabs.
|
|
get xulWindow() {
|
|
return this.xulBrowser.ownerGlobal;
|
|
}
|
|
|
|
get windowId() {
|
|
if (!apiManager.global.WindowManager || this.viewType == "background") {
|
|
return;
|
|
}
|
|
// viewType popup or tab:
|
|
return apiManager.global.WindowManager.getId(this.xulWindow);
|
|
}
|
|
|
|
get tabId() {
|
|
if (!apiManager.global.TabManager) {
|
|
return; // Not yet supported on Android.
|
|
}
|
|
let {gBrowser} = this.xulBrowser.ownerGlobal;
|
|
let tab = gBrowser && gBrowser.getTabForBrowser(this.xulBrowser);
|
|
return tab && apiManager.global.TabManager.getId(tab);
|
|
}
|
|
|
|
onBrowserChange(browser) {
|
|
super.onBrowserChange(browser);
|
|
this.xulBrowser = browser;
|
|
}
|
|
|
|
shutdown() {
|
|
apiManager.emit("page-shutdown", this);
|
|
super.shutdown();
|
|
}
|
|
}
|
|
|
|
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);
|
|
},
|
|
|
|
observe(subject, topic, data) {
|
|
if (topic === "message-manager-close") {
|
|
let mm = subject;
|
|
for (let [childId, context] of this.proxyContexts) {
|
|
if (context.parentMessageManager === 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 ExtensionPageContextParent(envType, extension, data, target);
|
|
} else if (envType == "content_parent") {
|
|
context = new ContentScriptContextParent(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) {
|
|
context.unload();
|
|
this.proxyContexts.delete(childId);
|
|
}
|
|
},
|
|
|
|
call(data, target) {
|
|
let context = this.getContextById(data.childId);
|
|
if (context.parentMessageManager !== target.messageManager) {
|
|
throw new Error("Got message on unexpected message manager");
|
|
}
|
|
|
|
let reply = result => {
|
|
if (!context.parentMessageManager) {
|
|
Cu.reportError("Cannot send function call result: other side closed connection");
|
|
return;
|
|
}
|
|
|
|
context.parentMessageManager.sendAsyncMessage(
|
|
"API:CallResult",
|
|
Object.assign({
|
|
childId: data.childId,
|
|
callId: data.callId,
|
|
}, result));
|
|
};
|
|
|
|
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];
|
|
|
|
reply({result});
|
|
}, error => {
|
|
error = context.normalizeError(error);
|
|
reply({error: {message: error.message}});
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (data.callId) {
|
|
let error = context.normalizeError(e);
|
|
reply({error: {message: error.message}});
|
|
} else {
|
|
Cu.reportError(e);
|
|
}
|
|
}
|
|
},
|
|
|
|
addListener(data, target) {
|
|
let context = this.getContextById(data.childId);
|
|
if (context.parentMessageManager !== target.messageManager) {
|
|
throw new Error("Got message on unexpected message manager");
|
|
}
|
|
|
|
let {childId} = data;
|
|
|
|
function listener(...listenerArgs) {
|
|
return context.sendMessage(
|
|
context.parentMessageManager,
|
|
"API:RunListener",
|
|
{
|
|
childId,
|
|
listenerId: data.listenerId,
|
|
path: data.path,
|
|
args: listenerArgs,
|
|
},
|
|
{
|
|
recipient: {childId},
|
|
});
|
|
}
|
|
|
|
context.listenerProxies.set(data.listenerId, 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.listenerId);
|
|
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();
|
|
|
|
|
|
const ExtensionParent = {
|
|
GlobalManager,
|
|
ParentAPIManager,
|
|
apiManager,
|
|
};
|