mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-10 21:28:04 +02:00
We already create early stub policy objects in the parent process during extension startup, so that early load attempts and policy queries can succeed during startup and extension installation. This patch does the same for child processes, which likewise may be asked to load extension URLs early during startup when they override the homepage or have pages loaded from session restore. Differential Revision: https://phabricator.services.mozilla.com/D21448 --HG-- extra : rebase_source : 189cecff691630336f38c16568f64350a5b2637c
363 lines
11 KiB
JavaScript
363 lines
11 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 script contains the minimum, skeleton content process code that we need
|
|
* in order to lazily load other extension modules when they are first
|
|
* necessary. Anything which is not likely to be needed immediately, or shortly
|
|
* after startup, in *every* browser process live outside of this file.
|
|
*/
|
|
|
|
var EXPORTED_SYMBOLS = ["ExtensionProcessScript"];
|
|
|
|
const {MessageChannel} = ChromeUtils.import("resource://gre/modules/MessageChannel.jsm");
|
|
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetters(this, {
|
|
ExtensionChild: "resource://gre/modules/ExtensionChild.jsm",
|
|
ExtensionCommon: "resource://gre/modules/ExtensionCommon.jsm",
|
|
ExtensionContent: "resource://gre/modules/ExtensionContent.jsm",
|
|
ExtensionPageChild: "resource://gre/modules/ExtensionPageChild.jsm",
|
|
});
|
|
|
|
const {ExtensionUtils} = ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionCommon.getConsole());
|
|
|
|
const {
|
|
DefaultWeakMap,
|
|
getInnerWindowID,
|
|
} = ExtensionUtils;
|
|
|
|
const {sharedData} = Services.cpmm;
|
|
|
|
function getData(extension, key = "") {
|
|
return sharedData.get(`extension/${extension.id}/${key}`);
|
|
}
|
|
|
|
// We need to avoid touching Services.appinfo here in order to prevent
|
|
// the wrong version from being cached during xpcshell test startup.
|
|
// eslint-disable-next-line mozilla/use-services
|
|
const appinfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
|
|
const isContentProcess = appinfo.processType == appinfo.PROCESS_TYPE_CONTENT;
|
|
|
|
var extensions = new DefaultWeakMap(policy => {
|
|
return new ExtensionChild.BrowserExtensionContent(policy);
|
|
});
|
|
|
|
var pendingExtensions = new Map();
|
|
|
|
var ExtensionManager;
|
|
|
|
class ExtensionGlobal {
|
|
constructor(global) {
|
|
this.global = global;
|
|
this.global.addMessageListener("Extension:SetFrameData", this);
|
|
|
|
this.frameData = null;
|
|
|
|
MessageChannel.addListener(global, "Extension:Capture", this);
|
|
MessageChannel.addListener(global, "Extension:DetectLanguage", this);
|
|
MessageChannel.addListener(global, "Extension:Execute", this);
|
|
MessageChannel.addListener(global, "WebNavigation:GetFrame", this);
|
|
MessageChannel.addListener(global, "WebNavigation:GetAllFrames", this);
|
|
}
|
|
|
|
get messageFilterStrict() {
|
|
return {
|
|
innerWindowID: getInnerWindowID(this.global.content),
|
|
};
|
|
}
|
|
|
|
getFrameData(force = false) {
|
|
if (!this.frameData && force) {
|
|
this.frameData = this.global.sendSyncMessage("Extension:GetTabAndWindowId")[0];
|
|
}
|
|
return this.frameData;
|
|
}
|
|
|
|
receiveMessage({target, messageName, recipient, data, name}) {
|
|
switch (name) {
|
|
case "Extension:SetFrameData":
|
|
if (this.frameData) {
|
|
Object.assign(this.frameData, data);
|
|
} else {
|
|
this.frameData = data;
|
|
}
|
|
if (data.viewType && WebExtensionPolicy.isExtensionProcess) {
|
|
ExtensionPageChild.expectViewLoad(this.global, data.viewType);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// SetFrameData does not have a recipient extension, or it would be
|
|
// an extension process. Anything following this point must have
|
|
// a recipient extension, so check access to the window.
|
|
let policy = WebExtensionPolicy.getByID(recipient.extensionId);
|
|
if (!policy.canAccessWindow(this.global.content)) {
|
|
throw new Error("Extension cannot access window");
|
|
}
|
|
|
|
return ExtensionContent.receiveMessage(this.global, messageName, target, data, recipient);
|
|
}
|
|
}
|
|
|
|
ExtensionManager = {
|
|
// WeakMap<WebExtensionPolicy, Map<string, WebExtensionContentScript>>
|
|
registeredContentScripts: new DefaultWeakMap((policy) => new Map()),
|
|
|
|
globals: new WeakMap(),
|
|
|
|
init() {
|
|
MessageChannel.setupMessageManagers([Services.cpmm]);
|
|
|
|
Services.cpmm.addMessageListener("Extension:Startup", this);
|
|
Services.cpmm.addMessageListener("Extension:Shutdown", this);
|
|
Services.cpmm.addMessageListener("Extension:FlushJarCache", this);
|
|
Services.cpmm.addMessageListener("Extension:RegisterContentScript", this);
|
|
Services.cpmm.addMessageListener("Extension:UnregisterContentScripts", this);
|
|
|
|
// eslint-disable-next-line mozilla/balanced-listeners
|
|
Services.obs.addObserver(
|
|
global => this.globals.set(global, new ExtensionGlobal(global)),
|
|
"tab-content-frameloader-created");
|
|
|
|
this.updateStubExtensions();
|
|
|
|
for (let id of sharedData.get("extensions/activeIDs") || []) {
|
|
this.initExtension(getData({id}));
|
|
}
|
|
},
|
|
|
|
initStubPolicy(id, data) {
|
|
let resolveReadyPromise;
|
|
let readyPromise = new Promise(resolve => {
|
|
resolveReadyPromise = resolve;
|
|
});
|
|
|
|
let policy = new WebExtensionPolicy({
|
|
id,
|
|
localizeCallback() {},
|
|
readyPromise,
|
|
allowedOrigins: new MatchPatternSet([]),
|
|
...data,
|
|
});
|
|
|
|
try {
|
|
policy.active = true;
|
|
|
|
pendingExtensions.set(id, {policy, resolveReadyPromise});
|
|
} catch (e) {
|
|
Cu.reportError(e);
|
|
}
|
|
},
|
|
|
|
updateStubExtensions() {
|
|
for (let [id, data] of sharedData.get("extensions/pending") || []) {
|
|
if (!pendingExtensions.has(id)) {
|
|
this.initStubPolicy(id, data);
|
|
}
|
|
}
|
|
},
|
|
|
|
initExtensionPolicy(extension) {
|
|
let policy = WebExtensionPolicy.getByID(extension.id);
|
|
if (!policy || pendingExtensions.has(extension.id)) {
|
|
let localizeCallback;
|
|
if (extension.localize) {
|
|
// We have a real Extension object.
|
|
localizeCallback = extension.localize.bind(extension);
|
|
} else {
|
|
// We have serialized extension data;
|
|
localizeCallback = str => extensions.get(policy).localize(str);
|
|
}
|
|
|
|
let {backgroundScripts} = extension;
|
|
if (!backgroundScripts && WebExtensionPolicy.isExtensionProcess) {
|
|
({backgroundScripts} = getData(extension, "extendedData") || {});
|
|
}
|
|
|
|
policy = new WebExtensionPolicy({
|
|
id: extension.id,
|
|
mozExtensionHostname: extension.uuid,
|
|
name: extension.name,
|
|
baseURL: extension.resourceURL,
|
|
|
|
permissions: extension.permissions,
|
|
allowedOrigins: extension.whiteListedHosts,
|
|
webAccessibleResources: extension.webAccessibleResources,
|
|
|
|
contentSecurityPolicy: extension.contentSecurityPolicy,
|
|
|
|
localizeCallback,
|
|
|
|
backgroundScripts,
|
|
|
|
contentScripts: extension.contentScripts,
|
|
});
|
|
|
|
policy.debugName = `${JSON.stringify(policy.name)} (ID: ${policy.id}, ${policy.getURL()})`;
|
|
|
|
// Register any existent dynamically registered content script for the extension
|
|
// when a content process is started for the first time (which also cover
|
|
// a content process that crashed and it has been recreated).
|
|
const registeredContentScripts = this.registeredContentScripts.get(policy);
|
|
|
|
for (let [scriptId, options] of getData(extension, "contentScripts") || []) {
|
|
const script = new WebExtensionContentScript(policy, options);
|
|
|
|
// If the script is a userScript, add the additional userScriptOptions
|
|
// property to the WebExtensionContentScript instance.
|
|
if ("userScriptOptions" in options) {
|
|
script.userScriptOptions = options.userScriptOptions;
|
|
}
|
|
|
|
policy.registerContentScript(script);
|
|
registeredContentScripts.set(scriptId, script);
|
|
}
|
|
|
|
let stub = pendingExtensions.get(extension.id);
|
|
if (stub) {
|
|
pendingExtensions.delete(extension.id);
|
|
stub.policy.active = false;
|
|
stub.resolveReadyPromise(policy);
|
|
}
|
|
|
|
policy.active = true;
|
|
policy.instanceId = extension.instanceId;
|
|
policy.optionalPermissions = extension.optionalPermissions;
|
|
}
|
|
return policy;
|
|
},
|
|
|
|
initExtension(data) {
|
|
if (typeof data === "string") {
|
|
data = getData({id: data});
|
|
}
|
|
let policy = this.initExtensionPolicy(data);
|
|
|
|
policy.injectContentScripts();
|
|
},
|
|
|
|
handleEvent(event) {
|
|
if (event.type === "change" && event.changedKeys.includes("extensions/pending")) {
|
|
this.updateStubExtensions();
|
|
}
|
|
},
|
|
|
|
receiveMessage({name, data}) {
|
|
try {
|
|
switch (name) {
|
|
case "Extension:Startup":
|
|
this.initExtension(data);
|
|
break;
|
|
|
|
case "Extension:Shutdown": {
|
|
let policy = WebExtensionPolicy.getByID(data.id);
|
|
if (policy) {
|
|
if (extensions.has(policy)) {
|
|
extensions.get(policy).shutdown();
|
|
}
|
|
|
|
if (isContentProcess) {
|
|
policy.active = false;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case "Extension:FlushJarCache":
|
|
ExtensionUtils.flushJarCache(data.path);
|
|
break;
|
|
|
|
case "Extension:RegisterContentScript": {
|
|
let policy = WebExtensionPolicy.getByID(data.id);
|
|
|
|
if (policy) {
|
|
const registeredContentScripts = this.registeredContentScripts.get(policy);
|
|
const type = "userScriptOptions" in data.options ? "userScript" : "contentScript";
|
|
|
|
if (registeredContentScripts.has(data.scriptId)) {
|
|
Cu.reportError(new Error(
|
|
`Registering ${type} ${data.scriptId} on ${data.id} more than once`));
|
|
} else {
|
|
const script = new WebExtensionContentScript(policy, data.options);
|
|
|
|
// If the script is a userScript, add the additional userScriptOptions
|
|
// property to the WebExtensionContentScript instance.
|
|
if (type === "userScript") {
|
|
script.userScriptOptions = data.options.userScriptOptions;
|
|
}
|
|
|
|
policy.registerContentScript(script);
|
|
registeredContentScripts.set(data.scriptId, script);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case "Extension:UnregisterContentScripts": {
|
|
let policy = WebExtensionPolicy.getByID(data.id);
|
|
|
|
if (policy) {
|
|
const registeredContentScripts = this.registeredContentScripts.get(policy);
|
|
|
|
for (const scriptId of data.scriptIds) {
|
|
const script = registeredContentScripts.get(scriptId);
|
|
if (script) {
|
|
policy.unregisterContentScript(script);
|
|
registeredContentScripts.delete(scriptId);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
Cu.reportError(e);
|
|
}
|
|
Services.cpmm.sendAsyncMessage(`${name}Complete`);
|
|
},
|
|
};
|
|
|
|
var ExtensionProcessScript = {
|
|
extensions,
|
|
|
|
getFrameData(global, force) {
|
|
let extGlobal = ExtensionManager.globals.get(global);
|
|
return extGlobal && extGlobal.getFrameData(force);
|
|
},
|
|
|
|
initExtension(extension) {
|
|
return ExtensionManager.initExtensionPolicy(extension);
|
|
},
|
|
|
|
initExtensionDocument(policy, doc, privileged) {
|
|
let extension = extensions.get(policy);
|
|
if (privileged) {
|
|
ExtensionPageChild.initExtensionContext(extension, doc.defaultView);
|
|
} else {
|
|
ExtensionContent.initExtensionContext(extension, doc.defaultView);
|
|
}
|
|
},
|
|
|
|
getExtensionChild(id) {
|
|
let policy = WebExtensionPolicy.getByID(id);
|
|
if (policy) {
|
|
return extensions.get(policy);
|
|
}
|
|
},
|
|
|
|
preloadContentScript(contentScript) {
|
|
ExtensionContent.contentScripts.get(contentScript).preload();
|
|
},
|
|
|
|
loadContentScript(contentScript, window) {
|
|
return ExtensionContent.contentScripts.get(contentScript).injectInto(window);
|
|
},
|
|
};
|
|
|
|
ExtensionManager.init();
|