468 lines
14 KiB
JavaScript
468 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.defineESModuleGetters(this, {
|
|
ChromeManifest: "resource:///modules/ChromeManifest.sys.mjs",
|
|
ExtensionSupport: "resource:///modules/ExtensionSupport.sys.mjs",
|
|
Overlays: "resource:///modules/Overlays.sys.mjs",
|
|
XPIExports: "resource://gre/modules/addons/XPIExports.sys.mjs",
|
|
});
|
|
|
|
Cu.importGlobalProperties(["fetch"]);
|
|
|
|
var { XPCOMUtils } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/XPCOMUtils.sys.mjs"
|
|
);
|
|
var { ConsoleAPI } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/Console.sys.mjs"
|
|
);
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "BOOTSTRAP_REASONS", () => {
|
|
const { XPIExports } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/addons/XPIExports.sys.mjs"
|
|
);
|
|
return XPIExports.XPIProvider.BOOTSTRAP_REASONS;
|
|
});
|
|
|
|
const { Log } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/Log.sys.mjs"
|
|
);
|
|
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 = XPIExports.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 {
|
|
Components.manager.addBootstrappedManifestLocation(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);
|
|
try {
|
|
const response = await fetch(url);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch ${filename}: ${response.status}`);
|
|
}
|
|
return await response.text();
|
|
} catch (error) {
|
|
console.error(`Error loading ${filename}:`, error);
|
|
throw error;
|
|
}
|
|
};
|
|
let chromeManifest = new ChromeManifest(loader, options);
|
|
await chromeManifest.parse("chrome.manifest");
|
|
|
|
// Load preference files
|
|
console.log("Loading add-on preferences from ", extensionRoot.path);
|
|
await 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.slice(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,
|
|
e
|
|
);
|
|
}
|
|
}
|
|
|
|
// 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() {
|
|
try {
|
|
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);
|
|
} catch (error) {
|
|
logger.error(`Failed to register bootstrapped extension ${this.extension.id}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
ChromeUtils.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] && typeof sandbox[name] === 'function') {
|
|
return sandbox[name];
|
|
}
|
|
|
|
try {
|
|
let method = Cu.evalInSandbox(name, sandbox);
|
|
if (typeof method === 'function') {
|
|
return method;
|
|
}
|
|
} catch (err) {
|
|
// Method not found, will return default function below
|
|
}
|
|
|
|
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.error(
|
|
`Exception running bootstrap method install on ${extension.id}`,
|
|
ex
|
|
);
|
|
}
|
|
},
|
|
|
|
uninstall(...args) {
|
|
try {
|
|
uninstall(...args);
|
|
} catch (ex) {
|
|
logger.error(
|
|
`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.error(
|
|
`Exception running bootstrap method startup on ${extension.id}`,
|
|
ex
|
|
);
|
|
}
|
|
},
|
|
|
|
shutdown(data, reason) {
|
|
try {
|
|
shutdown(data, reason);
|
|
} catch (ex) {
|
|
logger.error(
|
|
`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() {
|
|
let domWindows = [];
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
for (let win of Services.ww.getWindowEnumerator()) {
|
|
let parentDocShell = win
|
|
.getInterface(Ci.nsIWebNavigation)
|
|
.QueryInterface(Ci.nsIDocShell);
|
|
getChildDocShells(parentDocShell);
|
|
}
|
|
return domWindows;
|
|
}
|