Files
tubestation/browser/components/extensions/parent/ext-legacy.js
Alex Kontos 8252e45d0f feat: implement privileged extension support
Also includes:

* feat: improve support for bootstrapped extensions
Added support for custom preference pages.
* fix: incorrect loading order for bootstrap loader
* fix: BootstrapLoader

(cherry picked from commit eb40811e464688c7d2fc58a4330272dde1ec7937)
2025-11-03 16:04:54 +00:00

470 lines
14 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/. */
ChromeUtils.defineModuleGetter(
this,
"ChromeManifest",
"resource:///modules/ChromeManifest.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"ExtensionSupport",
"resource:///modules/ExtensionSupport.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"Overlays",
"resource:///modules/Overlays.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"XPIInternal",
"resource://gre/modules/addons/XPIProvider.jsm"
);
Cu.importGlobalProperties(["fetch"]);
var { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
var { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
XPCOMUtils.defineLazyGetter(this, "BOOTSTRAP_REASONS", () => {
const { XPIProvider } = ChromeUtils.import(
"resource://gre/modules/addons/XPIProvider.jsm"
);
return XPIProvider.BOOTSTRAP_REASONS;
});
const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm");
var logger = Log.repository.getLogger("addons.bootstrap");
let bootstrapScopes = new Map();
let cachedParams = new Map();
Services.obs.addObserver(() => {
for (let [id, scope] of bootstrapScopes.entries()) {
if (ExtensionSupport.loadedBootstrapExtensions.has(id)) {
scope.shutdown(
{ ...cachedParams.get(id) },
BOOTSTRAP_REASONS.APP_SHUTDOWN
);
}
}
}, "quit-application-granted");
var { ExtensionError } = ExtensionUtils;
this.legacy = class extends ExtensionAPI {
async onManifestEntry(entryName) {
if (this.extension.manifest.legacy) {
if (this.extension.manifest.legacy.type == "bootstrap") {
await this.registerBootstrapped();
} else {
await this.registerNonBootstrapped();
}
}
}
// This function is for non-bootstrapped add-ons.
async registerNonBootstrapped() {
this.extension.legacyLoaded = true;
let state = {
id: this.extension.id,
pendingOperation: null,
version: this.extension.version,
};
if (ExtensionSupport.loadedLegacyExtensions.has(this.extension.id)) {
state = ExtensionSupport.loadedLegacyExtensions.get(this.extension.id);
let versionComparison = Services.vc.compare(
this.extension.version,
state.version
);
if (versionComparison != 0) {
if (versionComparison > 0) {
state.pendingOperation = "upgrade";
ExtensionSupport.loadedLegacyExtensions.notifyObservers(state);
} else if (versionComparison < 0) {
state.pendingOperation = "downgrade";
ExtensionSupport.loadedLegacyExtensions.notifyObservers(state);
}
// Forget any cached files we might've had from another version of this extension.
Services.obs.notifyObservers(null, "startupcache-invalidate");
}
console.log(
`Legacy WebExtension ${
this.extension.id
} has already been loaded in this run, refusing to do so again. Please restart.`
);
return;
}
ExtensionSupport.loadedLegacyExtensions.set(this.extension.id, state);
if (this.extension.startupReason == "ADDON_INSTALL") {
// Usually, sideloaded extensions are disabled when they first appear,
// but to run calendar tests, we disable this.
let scope = XPIInternal.XPIStates.findAddon(this.extension.id).location
.scope;
let autoDisableScopes = Services.prefs.getIntPref(
"extensions.autoDisableScopes"
);
// If the extension was just installed from the distribution folder,
// it's in the profile extensions folder. We don't want to disable it.
let isDistroAddon = Services.prefs.getBoolPref(
"extensions.installedDistroAddon." + this.extension.id,
false
);
if (!isDistroAddon && scope & autoDisableScopes) {
state.pendingOperation = "install";
console.log(
`Legacy WebExtension ${
this.extension.id
} loading for other reason than startup (${
this.extension.startupReason
}), refusing to load immediately.`
);
ExtensionSupport.loadedLegacyExtensions.notifyObservers(state);
// Forget any cached files we might've had if this extension was previously installed.
Services.obs.notifyObservers(null, "startupcache-invalidate");
return;
}
}
if (this.extension.startupReason == "ADDON_ENABLE") {
state.pendingOperation = "enable";
console.log(
`Legacy WebExtension ${
this.extension.id
} loading for other reason than startup (${
this.extension.startupReason
}), refusing to load immediately.`
);
ExtensionSupport.loadedLegacyExtensions.notifyObservers(state);
return;
}
let extensionRoot;
if (this.extension.rootURI instanceof Ci.nsIJARURI) {
extensionRoot = this.extension.rootURI.JARFile.QueryInterface(
Ci.nsIFileURL
).file;
console.log("Loading packed extension from", extensionRoot.path);
} else {
extensionRoot = this.extension.rootURI.QueryInterface(Ci.nsIFileURL).file;
console.log("Loading unpacked extension from", extensionRoot.path);
}
// Have Gecko do as much loading as is still possible
try {
Cc["@mozilla.org/component-manager-extra;1"]
.getService(Ci.nsIComponentManagerExtra)
.addLegacyExtensionManifestLocation(extensionRoot);
} catch (e) {
throw new ExtensionError(e.message, e.fileName, e.lineNumber);
}
// Load chrome.manifest
let appinfo = Services.appinfo;
let options = {
application: appinfo.ID,
appversion: appinfo.version,
platformversion: appinfo.platformVersion,
os: appinfo.OS,
osversion: Services.sysinfo.getProperty("version"),
abi: appinfo.XPCOMABI,
};
let loader = async filename => {
let url = this.extension.getURL(filename);
return fetch(url).then(response => response.text());
};
let chromeManifest = new ChromeManifest(loader, options);
await chromeManifest.parse("chrome.manifest");
// Load preference files
console.log("Loading add-on preferences from ", extensionRoot.path);
ExtensionSupport.loadAddonPrefs(extensionRoot);
// Fire profile-after-change notifications, because we are past that event by now
console.log("Firing profile-after-change listeners for", this.extension.id);
let profileAfterChange = chromeManifest.category.get(
"profile-after-change"
);
for (let contractid of profileAfterChange.values()) {
let service = contractid.startsWith("service,");
let instance;
try {
if (service) {
instance = Cc[contractid.substr(8)].getService(Ci.nsIObserver);
} else {
instance = Cc[contractid].createInstance(Ci.nsIObserver);
}
instance.observe(null, "profile-after-change", null);
} catch (e) {
console.error(
"Error firing profile-after-change listener for",
contractid
);
}
}
// Overlays.load must only be called once per window per extension.
// We use this WeakSet to remember all windows we've already seen.
let seenDocuments = new WeakSet();
// Listen for new windows to overlay.
let documentObserver = {
observe(doc) {
if (
ExtensionCommon.instanceOf(doc, "HTMLDocument") &&
!seenDocuments.has(doc)
) {
seenDocuments.add(doc);
Overlays.load(chromeManifest, doc.defaultView);
}
},
};
Services.obs.addObserver(documentObserver, "chrome-document-interactive");
// Add overlays to all existing windows.
getAllWindows().forEach(win => {
if (
["interactive", "complete"].includes(win.document.readyState) &&
!seenDocuments.has(win.document)
) {
seenDocuments.add(win.document);
Overlays.load(chromeManifest, win);
}
});
this.extension.callOnClose({
close: () => {
Services.obs.removeObserver(
documentObserver,
"chrome-document-interactive"
);
},
});
}
// The following functions are for bootstrapped add-ons.
async registerBootstrapped() {
let oldParams = cachedParams.get(this.extension.id);
let params = {
id: this.extension.id,
version: this.extension.version,
resourceURI: Services.io.newURI(this.extension.resourceURL),
installPath: this.extensionFile.path,
};
cachedParams.set(this.extension.id, { ...params });
if (
oldParams &&
["ADDON_UPGRADE", "ADDON_DOWNGRADE"].includes(
this.extension.startupReason
)
) {
params.oldVersion = oldParams.version;
}
let scope = await this.loadScope();
bootstrapScopes.set(this.extension.id, scope);
if (
["ADDON_INSTALL", "ADDON_UPGRADE", "ADDON_DOWNGRADE"].includes(
this.extension.startupReason
)
) {
scope.install(params, BOOTSTRAP_REASONS[this.extension.startupReason]);
}
scope.startup(params, BOOTSTRAP_REASONS[this.extension.startupReason]);
ExtensionSupport.loadedBootstrapExtensions.add(this.extension.id);
}
static onDisable(id) {
if (bootstrapScopes.has(id)) {
bootstrapScopes
.get(id)
.shutdown({ ...cachedParams.get(id) }, BOOTSTRAP_REASONS.ADDON_DISABLE);
ExtensionSupport.loadedBootstrapExtensions.delete(id);
}
}
static onUpdate(id, manifest) {
if (bootstrapScopes.has(id)) {
let params = {
...cachedParams.get(id),
newVersion: manifest.version,
};
let reason = BOOTSTRAP_REASONS.ADDON_UPGRADE;
if (Services.vc.compare(params.newVersion, params.version) < 0) {
reason = BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
}
let scope = bootstrapScopes.get(id);
scope.shutdown(params, reason);
scope.uninstall(params, reason);
ExtensionSupport.loadedBootstrapExtensions.delete(id);
bootstrapScopes.delete(id);
}
}
static onUninstall(id) {
if (bootstrapScopes.has(id)) {
bootstrapScopes
.get(id)
.uninstall(
{ ...cachedParams.get(id) },
BOOTSTRAP_REASONS.ADDON_UNINSTALL
);
bootstrapScopes.delete(id);
}
}
get extensionFile() {
let uri = Services.io.newURI(this.extension.resourceURL);
if (uri instanceof Ci.nsIJARURI) {
uri = uri.QueryInterface(Ci.nsIJARURI).JARFile;
}
return uri.QueryInterface(Ci.nsIFileURL).file;
}
loadScope() {
let { extension } = this;
let file = this.extensionFile;
let uri = this.extension.getURL("bootstrap.js");
let principal = Services.scriptSecurityManager.getSystemPrincipal();
let sandbox = new Cu.Sandbox(principal, {
sandboxName: uri,
addonId: this.extension.id,
wantGlobalProperties: ["ChromeUtils"],
metadata: { addonID: this.extension.id, URI: uri },
});
try {
Object.assign(sandbox, BOOTSTRAP_REASONS);
XPCOMUtils.defineLazyGetter(
sandbox,
"console",
() => new ConsoleAPI({ consoleID: `addon/${this.extension.id}` })
);
Services.scriptloader.loadSubScript(uri, sandbox);
} catch (e) {
logger.warn(`Error loading bootstrap.js for ${this.extension.id}`, e);
}
function findMethod(name) {
if (sandbox.name) {
return sandbox.name;
}
try {
let method = Cu.evalInSandbox(name, sandbox);
return method;
} catch (err) {}
return () => {
logger.warn(
`Add-on ${extension.id} is missing bootstrap method ${name}`
);
};
}
let install = findMethod("install");
let uninstall = findMethod("uninstall");
let startup = findMethod("startup");
let shutdown = findMethod("shutdown");
return {
install(...args) {
try {
install(...args);
} catch (ex) {
logger.warn(
`Exception running bootstrap method install on ${extension.id}`,
ex
);
}
},
uninstall(...args) {
try {
uninstall(...args);
} catch (ex) {
logger.warn(
`Exception running bootstrap method uninstall on ${extension.id}`,
ex
);
} finally {
// Forget any cached files we might've had from this extension.
Services.obs.notifyObservers(null, "startupcache-invalidate");
}
},
startup(...args) {
logger.debug(`Registering manifest for ${file.path}\n`);
Components.manager.addBootstrappedManifestLocation(file);
try {
startup(...args);
} catch (ex) {
logger.warn(
`Exception running bootstrap method startup on ${extension.id}`,
ex
);
}
},
shutdown(data, reason) {
try {
shutdown(data, reason);
} catch (ex) {
logger.warn(
`Exception running bootstrap method shutdown on ${extension.id}`,
ex
);
} finally {
if (reason != BOOTSTRAP_REASONS.APP_SHUTDOWN) {
logger.debug(`Removing manifest for ${file.path}\n`);
Components.manager.removeBootstrappedManifestLocation(file);
}
}
},
};
}
};
function getAllWindows() {
function getChildDocShells(parentDocShell) {
let docShells = parentDocShell.getAllDocShellsInSubtree(
Ci.nsIDocShellTreeItem.typeAll,
Ci.nsIDocShell.ENUMERATE_FORWARDS
);
for (let docShell of docShells) {
docShell
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
domWindows.push(docShell.domWindow);
}
}
let domWindows = [];
for (let win of Services.ww.getWindowEnumerator()) {
let parentDocShell = win
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
getChildDocShells(parentDocShell);
}
return domWindows;
}