477 lines
14 KiB
JavaScript
477 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/. */
|
|
|
|
"use strict";
|
|
|
|
let EXPORTED_SYMBOLS = [];
|
|
|
|
const { XPCOMUtils } = ChromeUtils.import(
|
|
"resource://gre/modules/XPCOMUtils.jsm"
|
|
);
|
|
const Services =
|
|
globalThis.Services ||
|
|
ChromeUtils.import("resource://gre/modules/Services.jsm").Services;
|
|
|
|
XPCOMUtils.defineLazyModuleGetters(this, {
|
|
Blocklist: "resource://gre/modules/Blocklist.jsm",
|
|
ConsoleAPI: "resource://gre/modules/Console.jsm",
|
|
InstallRDF: "chrome://userchromejs/content/RDFManifestConverter.jsm",
|
|
});
|
|
|
|
Services.obs.addObserver((doc) => {
|
|
if (
|
|
doc.location.protocol + doc.location.pathname === "about:addons" ||
|
|
doc.location.protocol + doc.location.pathname ===
|
|
"chrome:/content/extensions/aboutaddons.html"
|
|
) {
|
|
const win = doc.defaultView;
|
|
let handleEvent_orig =
|
|
win.customElements.get("addon-card").prototype.handleEvent;
|
|
win.customElements.get("addon-card").prototype.handleEvent = function (e) {
|
|
if (
|
|
e.type === "click" &&
|
|
e.target.getAttribute("action") === "preferences" &&
|
|
this.addon.optionsType == 1 /*AddonManager.OPTIONS_TYPE_DIALOG*/
|
|
) {
|
|
var windows = Services.wm.getEnumerator(null);
|
|
while (windows.hasMoreElements()) {
|
|
var win2 = windows.getNext();
|
|
if (win2.closed) {
|
|
continue;
|
|
}
|
|
if (win2.document.documentURI == this.addon.optionsURL) {
|
|
win2.focus();
|
|
return;
|
|
}
|
|
}
|
|
var features = "chrome,titlebar,toolbar,centerscreen";
|
|
win.docShell.rootTreeItem.domWindow.openDialog(
|
|
this.addon.optionsURL,
|
|
this.addon.id,
|
|
features
|
|
);
|
|
} else {
|
|
handleEvent_orig.apply(this, arguments);
|
|
}
|
|
};
|
|
let update_orig = win.customElements.get("addon-options").prototype.update;
|
|
win.customElements.get("addon-options").prototype.update = function (
|
|
card,
|
|
addon
|
|
) {
|
|
update_orig.apply(this, arguments);
|
|
if (addon.optionsType == 1 /*AddonManager.OPTIONS_TYPE_DIALOG*/)
|
|
this.querySelector(
|
|
'panel-item[data-l10n-id="preferences-addon-button"]'
|
|
).hidden = false;
|
|
};
|
|
}
|
|
}, "chrome-document-loaded");
|
|
|
|
const { AddonManager } = ChromeUtils.import(
|
|
"resource://gre/modules/AddonManager.jsm"
|
|
);
|
|
const { XPIDatabase, AddonInternal } = ChromeUtils.import(
|
|
"resource://gre/modules/addons/XPIDatabase.jsm"
|
|
);
|
|
|
|
const { defineAddonWrapperProperty } = Cu.import(
|
|
"resource://gre/modules/addons/XPIDatabase.jsm"
|
|
);
|
|
defineAddonWrapperProperty("optionsType", function optionsType() {
|
|
if (!this.isActive) {
|
|
return null;
|
|
}
|
|
|
|
let addon = this.__AddonInternal__;
|
|
let hasOptionsURL = !!this.optionsURL;
|
|
|
|
if (addon.optionsType) {
|
|
switch (parseInt(addon.optionsType, 10)) {
|
|
case 1 /*AddonManager.OPTIONS_TYPE_DIALOG*/:
|
|
case AddonManager.OPTIONS_TYPE_TAB:
|
|
case AddonManager.OPTIONS_TYPE_INLINE_BROWSER:
|
|
return hasOptionsURL ? addon.optionsType : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
return null;
|
|
});
|
|
|
|
XPIDatabase.isDisabledLegacy = () => false;
|
|
|
|
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");
|
|
|
|
/**
|
|
* Valid IDs fit this pattern.
|
|
*/
|
|
var gIDTest =
|
|
/^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}|[a-z0-9-\._]*\@[a-z0-9-\._]+)$/i;
|
|
|
|
// Properties that exist in the install manifest
|
|
const PROP_METADATA = [
|
|
"id",
|
|
"version",
|
|
"type",
|
|
"internalName",
|
|
"updateURL",
|
|
"optionsURL",
|
|
"optionsType",
|
|
"aboutURL",
|
|
"iconURL",
|
|
];
|
|
const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"];
|
|
const PROP_LOCALE_MULTI = ["developers", "translators", "contributors"];
|
|
|
|
// Map new string type identifiers to old style nsIUpdateItem types.
|
|
// Retired values:
|
|
// 32 = multipackage xpi file
|
|
// 8 = locale
|
|
// 256 = apiextension
|
|
// 128 = experiment
|
|
// theme = 4
|
|
const TYPES = {
|
|
extension: 2,
|
|
dictionary: 64,
|
|
};
|
|
|
|
const COMPATIBLE_BY_DEFAULT_TYPES = {
|
|
extension: true,
|
|
dictionary: true,
|
|
};
|
|
|
|
const hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty);
|
|
|
|
function isXPI(filename) {
|
|
let ext = filename.slice(-4).toLowerCase();
|
|
return ext === ".xpi" || ext === ".zip";
|
|
}
|
|
|
|
/**
|
|
* Gets an nsIURI for a file within another file, either a directory or an XPI
|
|
* file. If aFile is a directory then this will return a file: URI, if it is an
|
|
* XPI file then it will return a jar: URI.
|
|
*
|
|
* @param {nsIFile} aFile
|
|
* The file containing the resources, must be either a directory or an
|
|
* XPI file
|
|
* @param {string} aPath
|
|
* The path to find the resource at, '/' separated. If aPath is empty
|
|
* then the uri to the root of the contained files will be returned
|
|
* @returns {nsIURI}
|
|
* An nsIURI pointing at the resource
|
|
*/
|
|
function getURIForResourceInFile(aFile, aPath) {
|
|
if (!isXPI(aFile.leafName)) {
|
|
let resource = aFile.clone();
|
|
if (aPath) aPath.split("/").forEach((part) => resource.append(part));
|
|
|
|
return Services.io.newFileURI(resource);
|
|
}
|
|
|
|
return buildJarURI(aFile, aPath);
|
|
}
|
|
|
|
/**
|
|
* Creates a jar: URI for a file inside a ZIP file.
|
|
*
|
|
* @param {nsIFile} aJarfile
|
|
* The ZIP file as an nsIFile
|
|
* @param {string} aPath
|
|
* The path inside the ZIP file
|
|
* @returns {nsIURI}
|
|
* An nsIURI for the file
|
|
*/
|
|
function buildJarURI(aJarfile, aPath) {
|
|
let uri = Services.io.newFileURI(aJarfile);
|
|
uri = "jar:" + uri.spec + "!/" + aPath;
|
|
return Services.io.newURI(uri);
|
|
}
|
|
|
|
var BootstrapLoader = {
|
|
name: "bootstrap",
|
|
manifestFile: "install.rdf",
|
|
async loadManifest(pkg) {
|
|
/**
|
|
* Reads locale properties from either the main install manifest root or
|
|
* an em:localized section in the install manifest.
|
|
*
|
|
* @param {Object} aSource
|
|
* The resource to read the properties from.
|
|
* @param {boolean} isDefault
|
|
* True if the locale is to be read from the main install manifest
|
|
* root
|
|
* @param {string[]} aSeenLocales
|
|
* An array of locale names already seen for this install manifest.
|
|
* Any locale names seen as a part of this function will be added to
|
|
* this array
|
|
* @returns {Object}
|
|
* an object containing the locale properties
|
|
*/
|
|
function readLocale(aSource, isDefault, aSeenLocales) {
|
|
let locale = {};
|
|
if (!isDefault) {
|
|
locale.locales = [];
|
|
for (let localeName of aSource.locales || []) {
|
|
if (!localeName) {
|
|
logger.warn("Ignoring empty locale in localized properties");
|
|
continue;
|
|
}
|
|
if (aSeenLocales.includes(localeName)) {
|
|
logger.warn("Ignoring duplicate locale in localized properties");
|
|
continue;
|
|
}
|
|
aSeenLocales.push(localeName);
|
|
locale.locales.push(localeName);
|
|
}
|
|
|
|
if (locale.locales.length == 0) {
|
|
logger.warn("Ignoring localized properties with no listed locales");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
for (let prop of [...PROP_LOCALE_SINGLE, ...PROP_LOCALE_MULTI]) {
|
|
if (hasOwnProperty(aSource, prop)) {
|
|
locale[prop] = aSource[prop];
|
|
}
|
|
}
|
|
|
|
return locale;
|
|
}
|
|
|
|
let manifestData = await pkg.readString("install.rdf");
|
|
let manifest = InstallRDF.loadFromString(manifestData).decode();
|
|
|
|
let addon = new AddonInternal();
|
|
for (let prop of PROP_METADATA) {
|
|
if (hasOwnProperty(manifest, prop)) {
|
|
addon[prop] = manifest[prop];
|
|
}
|
|
}
|
|
|
|
if (!addon.type) {
|
|
addon.type = "extension";
|
|
} else {
|
|
let type = addon.type;
|
|
addon.type = null;
|
|
for (let name in TYPES) {
|
|
if (TYPES[name] == type) {
|
|
addon.type = name;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!(addon.type in TYPES))
|
|
throw new Error("Install manifest specifies unknown type: " + addon.type);
|
|
|
|
if (!addon.id) throw new Error("No ID in install manifest");
|
|
if (!gIDTest.test(addon.id))
|
|
throw new Error("Illegal add-on ID " + addon.id);
|
|
if (!addon.version) throw new Error("No version in install manifest");
|
|
|
|
addon.strictCompatibility =
|
|
!(addon.type in COMPATIBLE_BY_DEFAULT_TYPES) ||
|
|
manifest.strictCompatibility == "true";
|
|
|
|
// Only read these properties for extensions.
|
|
if (addon.type == "extension") {
|
|
if (manifest.bootstrap != "true") {
|
|
throw new Error("Non-restartless extensions no longer supported");
|
|
}
|
|
|
|
if (
|
|
addon.optionsType &&
|
|
addon.optionsType != 1 /*AddonManager.OPTIONS_TYPE_DIALOG*/ &&
|
|
addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE_BROWSER &&
|
|
addon.optionsType != AddonManager.OPTIONS_TYPE_TAB
|
|
) {
|
|
throw new Error(
|
|
"Install manifest specifies unknown optionsType: " + addon.optionsType
|
|
);
|
|
}
|
|
|
|
if (addon.optionsType) addon.optionsType = parseInt(addon.optionsType);
|
|
}
|
|
|
|
addon.defaultLocale = readLocale(manifest, true);
|
|
|
|
let seenLocales = [];
|
|
addon.locales = [];
|
|
for (let localeData of manifest.localized || []) {
|
|
let locale = readLocale(localeData, false, seenLocales);
|
|
if (locale) addon.locales.push(locale);
|
|
}
|
|
|
|
let dependencies = new Set(manifest.dependencies);
|
|
addon.dependencies = Object.freeze(Array.from(dependencies));
|
|
|
|
let seenApplications = [];
|
|
addon.targetApplications = [];
|
|
for (let targetApp of manifest.targetApplications || []) {
|
|
if (!targetApp.id || !targetApp.minVersion || !targetApp.maxVersion) {
|
|
logger.warn(
|
|
"Ignoring invalid targetApplication entry in install manifest"
|
|
);
|
|
continue;
|
|
}
|
|
if (seenApplications.includes(targetApp.id)) {
|
|
logger.warn(
|
|
"Ignoring duplicate targetApplication entry for " +
|
|
targetApp.id +
|
|
" in install manifest"
|
|
);
|
|
continue;
|
|
}
|
|
seenApplications.push(targetApp.id);
|
|
addon.targetApplications.push(targetApp);
|
|
}
|
|
|
|
// Note that we don't need to check for duplicate targetPlatform entries since
|
|
// the RDF service coalesces them for us.
|
|
addon.targetPlatforms = [];
|
|
for (let targetPlatform of manifest.targetPlatforms || []) {
|
|
let platform = {
|
|
os: null,
|
|
abi: null,
|
|
};
|
|
|
|
let pos = targetPlatform.indexOf("_");
|
|
if (pos != -1) {
|
|
platform.os = targetPlatform.substring(0, pos);
|
|
platform.abi = targetPlatform.substring(pos + 1);
|
|
} else {
|
|
platform.os = targetPlatform;
|
|
}
|
|
|
|
addon.targetPlatforms.push(platform);
|
|
}
|
|
|
|
addon.userDisabled = false;
|
|
addon.softDisabled = addon.blocklistState == Blocklist.STATE_SOFTBLOCKED;
|
|
addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
|
|
|
|
addon.userPermissions = null;
|
|
|
|
addon.icons = {};
|
|
if (await pkg.hasResource("icon.png")) {
|
|
addon.icons[32] = "icon.png";
|
|
addon.icons[48] = "icon.png";
|
|
}
|
|
|
|
if (await pkg.hasResource("icon64.png")) {
|
|
addon.icons[64] = "icon64.png";
|
|
}
|
|
|
|
return addon;
|
|
},
|
|
|
|
loadScope(addon) {
|
|
let file = addon.file || addon._sourceBundle;
|
|
let uri = getURIForResourceInFile(file, "bootstrap.js").spec;
|
|
let principal = Services.scriptSecurityManager.getSystemPrincipal();
|
|
|
|
let sandbox = new Cu.Sandbox(principal, {
|
|
sandboxName: uri,
|
|
addonId: addon.id,
|
|
wantGlobalProperties: ["ChromeUtils"],
|
|
metadata: { addonID: addon.id, URI: uri },
|
|
});
|
|
|
|
try {
|
|
Object.assign(sandbox, BOOTSTRAP_REASONS);
|
|
|
|
XPCOMUtils.defineLazyGetter(
|
|
sandbox,
|
|
"console",
|
|
() => new ConsoleAPI({ consoleID: `addon/${addon.id}` })
|
|
);
|
|
|
|
Services.scriptloader.loadSubScript(uri, sandbox);
|
|
} catch (e) {
|
|
logger.warn(`Error loading bootstrap.js for ${addon.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 ${addon.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) {
|
|
install(...args);
|
|
// Forget any cached files we might've had from this extension.
|
|
Services.obs.notifyObservers(null, "startupcache-invalidate");
|
|
},
|
|
|
|
uninstall(...args) {
|
|
uninstall(...args);
|
|
// Forget any cached files we might've had from this extension.
|
|
Services.obs.notifyObservers(null, "startupcache-invalidate");
|
|
},
|
|
|
|
startup(...args) {
|
|
if (addon.type == "extension") {
|
|
logger.debug(`Registering manifest for ${file.path}\n`);
|
|
Components.manager.addBootstrappedManifestLocation(file);
|
|
}
|
|
return startup(...args);
|
|
},
|
|
|
|
shutdown(data, reason) {
|
|
try {
|
|
return shutdown(data, reason);
|
|
} catch (err) {
|
|
throw err;
|
|
} finally {
|
|
if (reason != BOOTSTRAP_REASONS.APP_SHUTDOWN) {
|
|
logger.debug(`Removing manifest for ${file.path}\n`);
|
|
Components.manager.removeBootstrappedManifestLocation(file);
|
|
}
|
|
}
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
AddonManager.addExternalExtensionLoader(BootstrapLoader);
|
|
|
|
if (AddonManager.isReady) {
|
|
AddonManager.getAllAddons().then((addons) => {
|
|
addons.forEach((addon) => {
|
|
if (
|
|
addon.type == "extension" &&
|
|
!addon.isWebExtension &&
|
|
!addon.userDisabled
|
|
) {
|
|
addon.reload();
|
|
}
|
|
});
|
|
});
|
|
}
|