Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
/**
* 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.
*/
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
/** @type {Lazy} */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ExtensionChild: "resource://gre/modules/ExtensionChild.sys.mjs",
ExtensionCommon: "resource://gre/modules/ExtensionCommon.sys.mjs",
ExtensionContent: "resource://gre/modules/ExtensionContent.sys.mjs",
ExtensionPageChild: "resource://gre/modules/ExtensionPageChild.sys.mjs",
ExtensionUserScriptsContent:
"resource://gre/modules/ExtensionUserScriptsContent.sys.mjs",
ExtensionWorkerChild: "resource://gre/modules/ExtensionWorkerChild.sys.mjs",
Schemas: "resource://gre/modules/Schemas.sys.mjs",
});
import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
const { DefaultWeakMap } = 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
ChromeUtils.defineLazyGetter(lazy, "isContentProcess", () => {
return Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
});
ChromeUtils.defineLazyGetter(lazy, "isContentScriptProcess", () => {
return (
lazy.isContentProcess ||
!WebExtensionPolicy.useRemoteWebExtensions ||
// Thunderbird still loads some content in the parent process.
AppConstants.MOZ_APP_NAME == "thunderbird"
);
});
var extensions = new DefaultWeakMap(policy => {
return new lazy.ExtensionChild(policy);
});
var pendingExtensions = new Map();
var ExtensionManager;
ExtensionManager = {
// WeakMap<WebExtensionPolicy, Map<number, WebExtensionContentScript>>
registeredContentScripts: new DefaultWeakMap(() => new Map()),
init() {
Services.cpmm.addMessageListener("Extension:Startup", this);
Services.cpmm.addMessageListener("Extension:Shutdown", this);
Services.cpmm.addMessageListener("Extension:FlushJarCache", this);
Services.cpmm.addMessageListener("Extension:RegisterContentScripts", this);
Services.cpmm.addMessageListener(
"Extension:UnregisterContentScripts",
this
);
Services.cpmm.addMessageListener("Extension:UpdateContentScripts", this);
Services.cpmm.addMessageListener("Extension:UpdateUserScriptWorlds", this);
Services.cpmm.addMessageListener("Extension:UpdatePermissions", this);
Services.cpmm.addMessageListener("Extension:UpdateIgnoreQuarantine", this);
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") || {});
}
let { backgroundWorkerScript } = extension;
if (!backgroundWorkerScript && WebExtensionPolicy.isExtensionProcess) {
({ backgroundWorkerScript } = getData(extension, "extendedData") || {});
}
let { backgroundTypeModule } = extension;
if (
backgroundTypeModule == null &&
WebExtensionPolicy.isExtensionProcess
) {
({ backgroundTypeModule } = getData(extension, "extendedData") || {});
}
policy = new WebExtensionPolicy({
id: extension.id,
mozExtensionHostname: extension.uuid,
name: extension.name,
type: extension.type,
baseURL: extension.resourceURL,
isPrivileged: extension.isPrivileged,
ignoreQuarantine: extension.ignoreQuarantine,
temporarilyInstalled: extension.temporarilyInstalled,
permissions: extension.permissions,
allowedOrigins: extension.allowedOrigins,
webAccessibleResources: extension.webAccessibleResources,
manifestVersion: extension.manifestVersion,
extensionPageCSP: extension.extensionPageCSP,
localizeCallback,
backgroundScripts,
backgroundWorkerScript,
backgroundTypeModule,
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 (lazy.isContentProcess) {
policy.active = false;
}
}
break;
}
case "Extension:FlushJarCache":
ExtensionUtils.flushJarCache(data.path);
break;
case "Extension:RegisterContentScripts": {
let policy = WebExtensionPolicy.getByID(data.id);
if (policy) {
const registeredContentScripts =
this.registeredContentScripts.get(policy);
for (const { scriptId, options } of data.scripts) {
const type =
"userScriptOptions" in options ? "userScript" : "contentScript";
if (registeredContentScripts.has(scriptId)) {
Cu.reportError(
new Error(
`Registering ${type} ${scriptId} on ${data.id} more than once`
)
);
} else {
const script = new WebExtensionContentScript(policy, options);
// If the script is a userScript, add the additional
// userScriptOptions property to the WebExtensionContentScript
// instance.
if (type === "userScript") {
script.userScriptOptions = options.userScriptOptions;
}
policy.registerContentScript(script);
registeredContentScripts.set(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;
}
case "Extension:UpdateContentScripts": {
let policy = WebExtensionPolicy.getByID(data.id);
if (policy) {
const registeredContentScripts =
this.registeredContentScripts.get(policy);
for (const { scriptId, options } of data.scripts) {
const oldScript = registeredContentScripts.get(scriptId);
const newScript = new WebExtensionContentScript(policy, options);
policy.unregisterContentScript(oldScript);
policy.registerContentScript(newScript);
registeredContentScripts.set(scriptId, newScript);
}
}
break;
}
case "Extension:UpdateUserScriptWorlds": {
let policy = WebExtensionPolicy.getByID(data.id);
if (policy) {
lazy.ExtensionUserScriptsContent.updateWorldConfig(
extensions.get(policy),
data.reset,
data.update
);
}
break;
}
case "Extension:UpdatePermissions": {
let policy = WebExtensionPolicy.getByID(data.id);
if (!policy) {
break;
}
// In the parent process, Extension.sys.mjs updates the policy.
if (lazy.isContentProcess) {
lazy.ExtensionCommon.updateAllowedOrigins(
policy,
data.origins,
data.add
);
if (data.permissions.length) {
let perms = new Set(policy.permissions);
for (let perm of data.permissions) {
if (data.add) {
perms.add(perm);
} else {
perms.delete(perm);
}
}
policy.permissions = Array.from(perms);
}
}
if (data.permissions.length && extensions.has(policy)) {
// Notify ChildApiManager of permission changes.
extensions.get(policy).emit("update-permissions");
}
break;
}
case "Extension:UpdateIgnoreQuarantine": {
let policy = WebExtensionPolicy.getByID(data.id);
if (policy?.active) {
policy.ignoreQuarantine = data.ignoreQuarantine;
}
break;
}
}
} catch (e) {
Cu.reportError(e);
}
Services.cpmm.sendAsyncMessage(`${name}Complete`);
},
};
export var ExtensionProcessScript = {
extensions,
initExtension(extension) {
return ExtensionManager.initExtensionPolicy(extension);
},
initExtensionDocument(policy, doc, privileged) {
let extension = extensions.get(policy);
if (privileged) {
lazy.ExtensionPageChild.initExtensionContext(extension, doc.defaultView);
} else {
lazy.ExtensionContent.initExtensionContext(extension, doc.defaultView);
}
},
getExtensionChild(id) {
let policy = WebExtensionPolicy.getByID(id);
if (policy) {
return extensions.get(policy);
}
},
preloadContentScript(contentScript) {
if (lazy.isContentScriptProcess) {
lazy.ExtensionContent.contentScripts.get(contentScript).preload();
}
},
loadContentScript(contentScript, window) {
return lazy.ExtensionContent.contentScripts
.get(contentScript)
.injectInto(window);
},
};
export var ExtensionAPIRequestHandler = {
initExtensionWorker(policy, serviceWorkerInfo) {
let extension = extensions.get(policy);
if (!extension) {
throw new Error(`Extension instance not found for addon ${policy.id}`);
}
lazy.ExtensionWorkerChild.initExtensionWorkerContext(
extension,
serviceWorkerInfo
);
},
onExtensionWorkerLoaded(policy, serviceWorkerDescriptorId) {
lazy.ExtensionWorkerChild.notifyExtensionWorkerContextLoaded(
serviceWorkerDescriptorId,
policy
);
},
onExtensionWorkerDestroyed(policy, serviceWorkerDescriptorId) {
lazy.ExtensionWorkerChild.destroyExtensionWorkerContext(
serviceWorkerDescriptorId
);
},
handleAPIRequest(policy, request) {
let context;
try {
let extension = extensions.get(policy);
if (!extension) {
throw new Error(`Extension instance not found for addon ${policy.id}`);
}
context = this.getExtensionContextForAPIRequest({
extension,
request,
});
if (!context) {
throw new Error(
`Extension context not found for API request: ${request}`
);
}
// Add a property to the request object for the normalizedArgs.
request.normalizedArgs = this.validateAndNormalizeRequestArgs({
context,
request,
});
return context.childManager.handleWebIDLAPIRequest(request);
} catch (error) {
// Propagate errors related to parameter validation when the error object
// belongs to the extension context that initiated the call.
if (context?.Error && error instanceof context.Error) {
return {
type: Ci.mozIExtensionAPIRequestResult.EXTENSION_ERROR,
value: error,
};
}
// Do not propagate errors that are not meant to be accessible to the
// extension, report it to the console and just throw the generic
// "An unexpected error occurred".
Cu.reportError(error);
return {
type: Ci.mozIExtensionAPIRequestResult.EXTENSION_ERROR,
value: new Error("An unexpected error occurred"),
};
}
},
getExtensionContextForAPIRequest({ extension, request }) {
if (request.serviceWorkerInfo) {
return lazy.ExtensionWorkerChild.getExtensionWorkerContext(
extension,
request.serviceWorkerInfo
);
}
return null;
},
validateAndNormalizeRequestArgs({ context, request }) {
if (
!lazy.Schemas.checkPermissions(request.apiNamespace, context.extension)
) {
throw new context.Error(
`Not enough privileges to access ${request.apiNamespace}`
);
}
if (request.requestType === "getProperty") {
return [];
}
if (request.apiObjectType) {
// skip parameter validation on request targeting an api object,
// even the JS-based implementation of the API objects are not
// going through the same kind of Schema based validation that
// the API namespaces methods and events go through.
//
// TODO(Bug 1728535): validate and normalize also this request arguments
// as a low priority follow up.
return request.args;
}
// Validate and normalize parameters, set the normalized args on the
// mozIExtensionAPIRequest normalizedArgs property.
return lazy.Schemas.checkWebIDLRequestParameters(
context.childManager,
request
);
},
};
ExtensionManager.init();