Mypal68/toolkit/components/extensions/ExtensionProcessScript.jsm
2025-04-19 19:15:41 +03:00

408 lines
12 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,
isPrivileged: extension.isPrivileged,
permissions: extension.permissions,
allowedOrigins: extension.whiteListedHosts,
webAccessibleResources: extension.webAccessibleResources,
extensionPageCSP: extension.extensionPageCSP,
contentScriptCSP: extension.contentScriptCSP,
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();