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)
This commit is contained in:
Alex Kontos
2022-07-27 11:31:47 +01:00
parent 91a3ba5527
commit 8252e45d0f
29 changed files with 8737 additions and 3 deletions

View File

@@ -229,6 +229,12 @@ BrowserGlue.prototype = {
// nsIObserver implementation
observe: async function BG_observe(subject, topic, data) {
switch (topic) {
case "app-startup":
const { BootstrapLoader } = ChromeUtils.import(
"resource:///modules/BootstrapLoader.jsm"
);
AddonManager.addExternalExtensionLoader(BootstrapLoader);
break;
case "notifications-open-settings":
this._openPreferences("privacy-permissions");
break;

View File

@@ -94,6 +94,12 @@
"scopes": ["addon_parent"],
"paths": [["identity"]]
},
"legacy": {
"url": "chrome://browser/content/parent/ext-legacy.js",
"schema": "chrome://browser/content/schemas/legacy.json",
"scopes": ["addon_parent"],
"manifest": ["legacy"]
},
"menusChild": {
"schema": "chrome://browser/content/schemas/menus_child.json",
"scopes": ["addon_child", "content_child", "devtools_child"]

View File

@@ -17,6 +17,7 @@ browser.jar:
content/browser/parent/ext-devtools-panels.js (parent/ext-devtools-panels.js)
content/browser/parent/ext-find.js (parent/ext-find.js)
content/browser/parent/ext-history.js (parent/ext-history.js)
content/browser/parent/ext-legacy.js (parent/ext-legacy.js)
content/browser/parent/ext-menus.js (parent/ext-menus.js)
content/browser/parent/ext-normandyAddonStudy.js (parent/ext-normandyAddonStudy.js)
content/browser/parent/ext-omnibox.js (parent/ext-omnibox.js)

View File

@@ -0,0 +1,469 @@
/* 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;
}

View File

@@ -12,6 +12,7 @@ browser.jar:
content/browser/schemas/devtools_panels.json
content/browser/schemas/find.json
content/browser/schemas/history.json
content/browser/schemas/legacy.json
content/browser/schemas/menus.json
content/browser/schemas/menus_child.json
content/browser/schemas/normandyAddonStudy.json

View File

@@ -0,0 +1,43 @@
[
{
"namespace": "manifest",
"types": [
{
"$extend": "WebExtensionManifest",
"properties": {
"legacy": {
"optional": true,
"choices": [
{
"type": "boolean"
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["xul", "bootstrap"],
"optional": true
},
"options": {
"type": "object",
"properties": {
"page": {
"type": "string"
},
"open_in_tab": {
"type": "boolean",
"optional": true
}
},
"optional": true
}
}
}
]
}
}
}
]
}
]

View File

@@ -4171,7 +4171,9 @@ export var AddonManager = {
AUTOUPDATE_DEFAULT: 1,
// Indicates that the Addon should update automatically.
AUTOUPDATE_ENABLE: 2,
// Constants for how Addon options should be shown.
// Options will be opened in a new window
OPTIONS_TYPE_DIALOG: 1,
// Constants for how Addon options should be shown.
// Options will be displayed in a new tab, if possible
OPTIONS_TYPE_TAB: 3,

View File

@@ -990,6 +990,7 @@ export class AddonWrapper {
if (addon.optionsType) {
switch (parseInt(addon.optionsType, 10)) {
case lazy.AddonManager.OPTIONS_TYPE_DIALOG:
case lazy.AddonManager.OPTIONS_TYPE_TAB:
case lazy.AddonManager.OPTIONS_TYPE_INLINE_BROWSER:
return hasOptionsURL ? addon.optionsType : null;
@@ -2717,8 +2718,8 @@ export const XPIDatabase = {
}
if (this.isDisabledLegacy(aAddon)) {
logger.warn(`disabling legacy extension ${aAddon.id}`);
return false;
logger.warn(`enabling legacy extension ${aAddon.id}`);
return true;
}
if (lazy.AddonManager.checkCompatibility) {

View File

@@ -0,0 +1,458 @@
/* 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";
var EXPORTED_SYMBOLS = ["BootstrapLoader"];
const { AddonManager } = ChromeUtils.import(
"resource://gre/modules/AddonManager.jsm"
);
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
AddonInternal: "resource://gre/modules/addons/XPIDatabase.jsm",
Blocklist: "resource://gre/modules/Blocklist.jsm",
ConsoleAPI: "resource://gre/modules/Console.jsm",
InstallRDF: "resource:///modules/RDFManifestConverter.jsm",
Services: "resource://gre/modules/Services.jsm",
});
Services.obs.addObserver(doc => {
if (
doc.location.protocol + doc.location.pathname === "about:addons" ||
doc.location.protocol + doc.location.pathname ===
"chrome://mozapps/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 == AddonManager.OPTIONS_TYPE_DIALOG
) {
let windows = Services.wm.getEnumerator(null);
while (windows.hasMoreElements()) {
let win2 = windows.getNext();
if (win2.closed) {
continue;
}
if (win2.document.documentURI == this.addon.optionsURL) {
win2.focus();
return;
}
}
let features = "chrome,titlebar,toolbar,centerscreen";
let instantApply = Services.prefs.getBoolPref(
"browser.preferences.instantApply"
);
features += instantApply ? ",dialog=no" : "";
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 == AddonManager.OPTIONS_TYPE_DIALOG) {
this.querySelector('panel-item[action="preferences"]').hidden = false;
}
};
}
}, "chrome-document-loaded");
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";
}
/**
* 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);
}
/**
* 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);
}
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) {
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 != 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
);
}
} else {
// Convert legacy dictionaries into a format the WebExtension
// dictionary loader can process.
if (addon.type === "dictionary") {
addon.loader = null;
let dictionaries = {};
await pkg.iterFiles(({ path }) => {
let match = /^dictionaries\/([^\/]+)\.dic$/.exec(path);
if (match) {
let lang = match[1].replace(/_/g, "-");
dictionaries[lang] = match[0];
}
});
addon.startupData = { dictionaries };
}
// Only extensions are allowed to provide an optionsURL, optionsType,
// optionsBrowserStyle, or aboutURL. For all other types they are silently ignored
addon.aboutURL = null;
addon.optionsBrowserStyle = null;
addon.optionsType = null;
addon.optionsURL = null;
}
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),
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);
}
}
},
};
},
};

View File

@@ -0,0 +1,363 @@
/* 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.EXPORTED_SYMBOLS = ["ChromeManifest"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
/**
* A parser for chrome.manifest files. Implements a subset of
* https://developer.mozilla.org/en-US/docs/Mozilla/Chrome_Registration
*/
class ChromeManifest {
/**
* Constructs the chrome.manifest parser
*
* @param {Function} loader An asynchronous function that will load further files, e.g.
* those included via the |manifest| instruction. The
* function will take the file as an argument and should
* resolve with the string contents of that file
* @param {Object} options Object describing the current system. The keys are manifest
* instructions
*/
constructor(loader, options) {
this.loader = loader;
this.options = options;
this.overlay = new DefaultMap(() => []);
this.locales = new DefaultMap(() => new Map());
this.style = new DefaultMap(() => new Set());
this.category = new DefaultMap(() => new Map());
this.component = new Map();
this.contract = new Map();
this.content = new Map();
this.skin = new Map();
this.resource = new Map();
this.override = new Map();
}
/**
* Parse the given file.
*
* @param {string} filename The filename to load
* @param {string} base The relative directory this file is expected to be in.
* @returns {Promise} Resolved when loading completes
*/
async parse(filename = "chrome.manifest", base = "") {
await this.parseString(await this.loader(filename), base);
}
/**
* Parse the given string.
*
* @param {string} data The file data to load
* @param {string} base The relative directory this file is expected to be in.
* @returns {Promise} Resolved when loading completes
*/
async parseString(data, base = "") {
let lines = data.split("\n");
let extraManifests = [];
for (let line of lines) {
let parts = line.trim().split(/\s+/);
let directive = parts.shift();
switch (directive) {
case "manifest":
extraManifests.push(this._parseManifest(base, ...parts));
break;
case "component":
this._parseComponent(...parts);
break;
case "contract":
this._parseContract(...parts);
break;
case "category":
this._parseCategory(...parts);
break;
case "content":
this._parseContent(...parts);
break;
case "locale":
this._parseLocale(...parts);
break;
case "skin":
this._parseSkin(...parts);
break;
case "resource":
this._parseResource(...parts);
break;
case "overlay":
this._parseOverlay(...parts);
break;
case "style":
this._parseStyle(...parts);
break;
case "override":
this._parseOverride(...parts);
break;
}
}
await Promise.all(extraManifests);
}
/**
* Ensure the flags provided for the instruction match our options
*
* @param {string[]} flags An array of raw flag values in the form key=value.
* @returns {boolean} True, if the flags match the options provided in the constructor
*/
_parseFlags(flags) {
if (!flags.length) {
return true;
}
let matchString = (a, sign, b) => {
if (sign != "=") {
console.warn(
`Invalid sign ${sign} in ${a}${sign}${b}, dropping manifest instruction`
);
return false;
}
return a == b;
};
let matchVersion = (a, sign, b) => {
switch (sign) {
case "=":
return Services.vc.compare(a, b) == 0;
case ">":
return Services.vc.compare(a, b) > 0;
case "<":
return Services.vc.compare(a, b) < 0;
case ">=":
return Services.vc.compare(a, b) >= 0;
case "<=":
return Services.vc.compare(a, b) <= 0;
default:
console.warn(
`Invalid sign ${sign} in ${a}${sign}${b}, dropping manifest instruction`
);
return false;
}
};
let flagMatches = (key, typeMatch) => {
return (
!flagdata.has(key) ||
flagdata.get(key).some(val => typeMatch(this.options[key], ...val))
);
};
let flagdata = new DefaultMap(() => []);
for (let flag of flags) {
let match = flag.match(/(\w+)(>=|<=|<|>|=)(.*)/);
if (match) {
flagdata.get(match[1]).push([match[2], match[3]]);
} else {
console.warn(`Invalid flag ${flag}, dropping manifest instruction`);
}
}
return (
flagMatches("application", matchString) &&
flagMatches("appversion", matchVersion) &&
flagMatches("platformversion", matchVersion) &&
flagMatches("os", matchString) &&
flagMatches("osversion", matchVersion) &&
flagMatches("abi", matchString)
);
}
/**
* Parse the manifest instruction, to load other files
*
* @param {string} base The base directory the manifest file is in
* @param {string} filename The file and path to load
* @param {...string} flags The flags for this instruction
* @returns {Promise} Promise resolved when the manifest is loaded
*/
async _parseManifest(base, filename, ...flags) {
if (this._parseFlags(flags)) {
let dirparts = filename.split("/");
dirparts.pop();
try {
await this.parse(filename, base + "/" + dirparts.join("/"));
} catch (e) {
console.log(`Could not read manifest '${base}/${filename}'.`);
}
}
return null;
}
/**
* Parse the component instruction, to load xpcom components
*
* @param {string} classid The xpcom class id to load
* @param {string} loction The file location of this component
* @param {...string} flags The flags for this instruction
*/
_parseComponent(classid, location, ...flags) {
if (this._parseFlags(flags)) {
this.component.set(classid, location);
}
}
/**
* Parse the contract instruction, to load xpcom contract ids
*
* @param {string} contractid The xpcom contract id to load
* @param {string} location The file location of this component
* @param {...string} flags The flags for this instruction
*/
_parseContract(contractid, location, ...flags) {
if (this._parseFlags(flags)) {
this.contract.set(contractid, location);
}
}
/**
* Parse the category instruction, to set up xpcom categories
*
* @param {string} category The name of the category
* @param {string} entryName The category entry name
* @param {string} value The category entry value
* @param {...string} flags The flags for this instruction
*/
_parseCategory(category, entryName, value, ...flags) {
if (this._parseFlags(flags)) {
this.category.get(category).set(entryName, value);
}
}
/**
* Parse the content instruction, to set chrome content locations
*
* @param {string} shortname The content short name, e.g. chrome://shortname/content/
* @param {string} location The location for this content registration
* @param {...string} flags The flags for this instruction
*/
_parseContent(shortname, location, ...flags) {
if (this._parseFlags(flags)) {
this.content.set(shortname, location);
}
}
/**
* Parse the locale instruction, to set chrome locale locations
*
* @param {string} shortname The locale short name, e.g. chrome://shortname/locale/
* @param {string} location The location for this locale registration
* @param {...string} flags The flags for this instruction
*/
_parseLocale(shortname, locale, location, ...flags) {
if (this._parseFlags(flags)) {
this.locales.get(shortname).set(locale, location);
}
}
/**
* Parse the skin instruction, to set chrome skin locations
*
* @param {string} shortname The skin short name, e.g. chrome://shortname/skin/
* @param {string} location The location for this skin registration
* @param {...string} flags The flags for this instruction
*/
_parseSkin(packagename, skinname, location, ...flags) {
if (this._parseFlags(flags)) {
this.skin.set(packagename, location);
}
}
/**
* Parse the resource instruction, to set up resource uri substitutions
*
* @param {string} packagename The resource package name, e.g. resource://packagename/
* @param {string} url The location for this content registration
* @param {...string} flags The flags for this instruction
*/
_parseResource(packagename, location, ...flags) {
if (this._parseFlags(flags)) {
this.resource.set(packagename, location);
}
}
/**
* Parse the overlay instruction, to set up xul overlays
*
* @param {string} targetUrl The chrome target url
* @param {string} overlayUrl The url of the xul overlay
* @param {...string} flags The flags for this instruction
*/
_parseOverlay(targetUrl, overlayUrl, ...flags) {
if (this._parseFlags(flags)) {
this.overlay.get(targetUrl).push(overlayUrl);
}
}
/**
* Parse the style instruction, to add stylesheets into chrome windows
*
* @param {string} uri The uri of the chrome window
* @param {string} sheet The uri of the css sheet
* @param {...string} flags The flags for this instruction
*/
_parseStyle(uri, sheet, ...flags) {
if (this._parseFlags(flags)) {
this.style.get(uri).add(sheet);
}
}
/**
* Parse the override instruction, to set chrome uri overrides
*
* @param {string} uri The uri being overridden
* @param {string} newuri The replacement uri for the original location
* @param {...string} flags The flags for this instruction
*/
_parseOverride(uri, newuri, ...flags) {
if (this._parseFlags(flags)) {
this.override.set(uri, newuri);
}
}
}
/**
* A default map, which assumes a default value on get() if the key doesn't exist
*/
class DefaultMap extends Map {
/**
* Constructs the default map
*
* @param {Function} _default A function that returns the default value for this map
* @param {*} iterable An iterable to initialize the map with
*/
constructor(_default, iterable) {
super(iterable);
this._default = _default;
}
/**
* Get the given key, creating if necessary
*
* @param {string} key The key of the map to get
* @param {boolean} create True, if the key should be created in case it doesn't exist.
*/
get(key, create = true) {
if (this.has(key)) {
return super.get(key);
} else if (create) {
this.set(key, this._default());
return super.get(key);
}
return this._default();
}
}

View File

@@ -0,0 +1,410 @@
/* 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";
/**
* Helper functions for use by extensions that should ease them plug
* into the application.
*/
this.EXPORTED_SYMBOLS = ["ExtensionSupport"];
const { AddonManager } = ChromeUtils.import(
"resource://gre/modules/AddonManager.jsm"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
// ChromeUtils.import("resource://gre/modules/Deprecated.jsm") - needed for warning.
const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
var { fixIterator } = ChromeUtils.import(
"resource:///modules/iteratorUtils.jsm"
);
const { IOUtils } = ChromeUtils.import("resource:///modules/IOUtils.jsm");
var extensionHooks = new Map();
var legacyExtensions = new Map();
var openWindowList;
var ExtensionSupport = {
/**
* A Map-like object which tracks legacy extension status. The "has" method
* returns only active extensions for compatibility with existing code.
*/
loadedLegacyExtensions: {
set(id, state) {
legacyExtensions.set(id, state);
},
get(id) {
return legacyExtensions.get(id);
},
has(id) {
if (!legacyExtensions.has(id)) {
return false;
}
let state = legacyExtensions.get(id);
return !["install", "enable"].includes(state.pendingOperation);
},
hasAnyState(id) {
return legacyExtensions.has(id);
},
_maybeDelete(id, newPendingOperation) {
if (!legacyExtensions.has(id)) {
return;
}
let state = legacyExtensions.get(id);
if (
state.pendingOperation == "enable" &&
newPendingOperation == "disable"
) {
legacyExtensions.delete(id);
this.notifyObservers(state);
} else if (
state.pendingOperation == "install" &&
newPendingOperation == "uninstall"
) {
legacyExtensions.delete(id);
this.notifyObservers(state);
}
},
notifyObservers(state) {
let wrappedState = { wrappedJSObject: state };
Services.obs.notifyObservers(wrappedState, "legacy-addon-status-changed");
},
// AddonListener
onDisabled(ev) {
this._maybeDelete(ev.id, "disable");
},
onUninstalled(ev) {
this._maybeDelete(ev.id, "uninstall");
},
},
loadAddonPrefs(addonFile) {
function setPref(preferDefault, name, value) {
let branch = preferDefault
? Services.prefs.getDefaultBranch("")
: Services.prefs.getBranch("");
if (typeof value == "boolean") {
branch.setBoolPref(name, value);
} else if (typeof value == "string") {
if (value.startsWith("chrome://") && value.endsWith(".properties")) {
let valueLocal = Cc[
"@mozilla.org/pref-localizedstring;1"
].createInstance(Ci.nsIPrefLocalizedString);
valueLocal.data = value;
branch.setComplexValue(name, Ci.nsIPrefLocalizedString, valueLocal);
} else {
branch.setStringPref(name, value);
}
} else if (typeof value == "number" && Number.isInteger(value)) {
branch.setIntPref(name, value);
} else if (typeof value == "number" && Number.isFloat(value)) {
// Floats are set as char prefs, then retrieved using getFloatPref
branch.setCharPref(name, value);
}
}
function walkExtensionPrefs(extensionRoot) {
let prefFile = extensionRoot.clone();
let foundPrefStrings = [];
if (!prefFile.exists()) {
return [];
}
if (prefFile.isDirectory()) {
prefFile.append("defaults");
prefFile.append("preferences");
if (!prefFile.exists() || !prefFile.isDirectory()) {
return [];
}
let unsortedFiles = [];
for (let file of fixIterator(prefFile.directoryEntries, Ci.nsIFile)) {
if (file.isFile() && file.leafName.toLowerCase().endsWith(".js")) {
unsortedFiles.push(file);
}
}
for (let file of unsortedFiles.sort((a, b) =>
a.path < b.path ? 1 : -1
)) {
foundPrefStrings.push(IOUtils.loadFileToString(file));
}
} else if (prefFile.isFile() && prefFile.leafName.endsWith("xpi")) {
let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(
Ci.nsIZipReader
);
zipReader.open(prefFile);
let entries = zipReader.findEntries("defaults/preferences/*.js");
let unsortedEntries = [];
while (entries.hasMore()) {
unsortedEntries.push(entries.getNext());
}
for (let entryName of unsortedEntries.sort().reverse()) {
let stream = zipReader.getInputStream(entryName);
let entrySize = zipReader.getEntry(entryName).realSize;
if (entrySize > 0) {
let content = NetUtil.readInputStreamToString(stream, entrySize, {
charset: "utf-8",
replacement: "?",
});
foundPrefStrings.push(content);
}
}
}
return foundPrefStrings;
}
let sandbox = new Cu.Sandbox(null);
sandbox.pref = setPref.bind(undefined, true);
sandbox.user_pref = setPref.bind(undefined, false);
let prefDataStrings = walkExtensionPrefs(addonFile);
for (let prefDataString of prefDataStrings) {
try {
Cu.evalInSandbox(prefDataString, sandbox);
} catch (e) {
Cu.reportError(
"Error reading default prefs of addon " +
addonFile.leafName +
": " +
e
);
}
}
/*
TODO: decide whether we need to warn the user/make addon authors to migrate away from these pref files.
if (prefDataStrings.length > 0) {
Deprecated.warning(addon.defaultLocale.name + " uses defaults/preferences/*.js files to load prefs",
"https://bugzilla.mozilla.org/show_bug.cgi?id=1414398");
}
*/
},
/**
* Register listening for windows getting opened that will run the specified callback function
* when a matching window is loaded.
*
* @param aID {String} Some identification of the caller, usually the extension ID.
* @param aExtensionHook {Object} The object describing the hook the caller wants to register.
* Members of the object can be (all optional, but one callback must be supplied):
* chromeURLs {Array} An array of strings of document URLs on which
* the given callback should run. If not specified,
* run on all windows.
* onLoadWindow {function} The callback function to run when window loads
* the matching document.
* onUnloadWindow {function} The callback function to run when window
* unloads the matching document.
* Both callbacks receive the matching window object as argument.
*
* @returns {boolean} True if the passed arguments were valid and the caller could be registered.
* False otherwise.
*/
registerWindowListener(aID, aExtensionHook) {
if (!aID) {
Cu.reportError("No extension ID provided for the window listener");
return false;
}
if (extensionHooks.has(aID)) {
Cu.reportError(
"Window listener for extension + '" + aID + "' already registered"
);
return false;
}
if (
!("onLoadWindow" in aExtensionHook) &&
!("onUnloadWindow" in aExtensionHook)
) {
Cu.reportError(
"The extension + '" + aID + "' does not provide any callbacks"
);
return false;
}
extensionHooks.set(aID, aExtensionHook);
// Add our global listener if there isn't one already
// (only when we have first caller).
if (extensionHooks.size == 1) {
Services.wm.addListener(this._windowListener);
}
if (openWindowList) {
// We already have a list of open windows, notify the caller about them.
openWindowList.forEach(domWindow =>
ExtensionSupport._checkAndRunMatchingExtensions(domWindow, "load", aID)
);
} else {
openWindowList = new Set();
// Get the list of windows already open.
let windows = Services.wm.getEnumerator(null);
while (windows.hasMoreElements()) {
let domWindow = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
if (domWindow.document.location.href === "about:blank") {
ExtensionSupport._waitForLoad(domWindow, aID);
} else {
ExtensionSupport._addToListAndNotify(domWindow, aID);
}
}
}
return true;
},
/**
* Unregister listening for windows for the given caller.
*
* @param aID {String} Some identification of the caller, usually the extension ID.
*
* @returns {boolean} True if the passed arguments were valid and the caller could be unregistered.
* False otherwise.
*/
unregisterWindowListener(aID) {
if (!aID) {
Cu.reportError("No extension ID provided for the window listener");
return false;
}
let windowListener = extensionHooks.get(aID);
if (!windowListener) {
Cu.reportError(
"Couldn't remove window listener for extension + '" + aID + "'"
);
return false;
}
extensionHooks.delete(aID);
// Remove our global listener if there are no callers registered anymore.
if (extensionHooks.size == 0) {
Services.wm.removeListener(this._windowListener);
openWindowList.clear();
openWindowList = undefined;
}
return true;
},
get openWindows() {
return openWindowList.values();
},
_windowListener: {
// nsIWindowMediatorListener functions
onOpenWindow(xulWindow) {
// A new window has opened.
let domWindow = xulWindow.docShell.domWindow;
// Here we pass no caller ID, so all registered callers get notified.
ExtensionSupport._waitForLoad(domWindow);
},
onCloseWindow(xulWindow) {
// One of the windows has closed.
let domWindow = xulWindow.docShell.domWindow;
openWindowList.delete(domWindow);
},
},
/**
* Set up listeners to run the callbacks on the given window.
*
* @param aWindow {nsIDOMWindow} The window to set up.
* @param aID {String} Optional. ID of the new caller that has registered right now.
*/
_waitForLoad(aWindow, aID) {
// Wait for the load event of the window. At that point
// aWindow.document.location.href will not be "about:blank" any more.
aWindow.addEventListener(
"load",
function() {
ExtensionSupport._addToListAndNotify(aWindow, aID);
},
{ once: true }
);
},
/**
* Once the window is fully loaded with the href referring to the XUL document,
* add it to our list, attach the "unload" listener to it and notify interested
* callers.
*
* @param aWindow {nsIDOMWindow} The window to process.
* @param aID {String} Optional. ID of the new caller that has registered right now.
*/
_addToListAndNotify(aWindow, aID) {
openWindowList.add(aWindow);
aWindow.addEventListener(
"unload",
function() {
ExtensionSupport._checkAndRunMatchingExtensions(aWindow, "unload");
},
{ once: true }
);
ExtensionSupport._checkAndRunMatchingExtensions(aWindow, "load", aID);
},
/**
* Check if the caller matches the given window and run its callback function.
*
* @param aWindow {nsIDOMWindow} The window to run the callbacks on.
* @param aEventType {String} Which callback to run if caller matches (load/unload).
* @param aID {String} Optional ID of the caller whose callback is to be run.
* If not given, all registered callers are notified.
*/
_checkAndRunMatchingExtensions(aWindow, aEventType, aID) {
if (aID) {
checkAndRunExtensionCode(extensionHooks.get(aID));
} else {
for (let extensionHook of extensionHooks.values()) {
checkAndRunExtensionCode(extensionHook);
}
}
/**
* Check if the single given caller matches the given window
* and run its callback function.
*
* @param aExtensionHook {Object} The object describing the hook the caller
* has registered.
*/
function checkAndRunExtensionCode(aExtensionHook) {
let windowChromeURL = aWindow.document.location.href;
// Check if extension applies to this document URL.
if (
"chromeURLs" in aExtensionHook &&
!aExtensionHook.chromeURLs.some(url => url == windowChromeURL)
) {
return;
}
// Run the relevant callback.
switch (aEventType) {
case "load":
if ("onLoadWindow" in aExtensionHook) {
aExtensionHook.onLoadWindow(aWindow);
}
break;
case "unload":
if ("onUnloadWindow" in aExtensionHook) {
aExtensionHook.onUnloadWindow(aWindow);
}
break;
}
}
},
get registeredWindowListenerCount() {
return extensionHooks.size;
},
};
AddonManager.addAddonListener(ExtensionSupport.loadedLegacyExtensions);

View File

@@ -0,0 +1,143 @@
/* 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/. */
const EXPORTED_SYMBOLS = ["IOUtils"];
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
var kStringBlockSize = 4096;
var kStreamBlockSize = 8192;
var IOUtils = {
/**
* Read a file containing ASCII text into a string.
*
* @param aFile An nsIFile representing the file to read or a string containing
* the file name of a file under user's profile.
* @returns A string containing the contents of the file, presumed to be ASCII
* text. If the file didn't exist, returns null.
*/
loadFileToString(aFile) {
let file;
if (!(aFile instanceof Ci.nsIFile)) {
file = Services.dirsvc.get("ProfD", Ci.nsIFile);
file.append(aFile);
} else {
file = aFile;
}
if (!file.exists()) {
return null;
}
let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
Ci.nsIFileInputStream
);
// PR_RDONLY
fstream.init(file, 0x01, 0, 0);
let sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
Ci.nsIScriptableInputStream
);
sstream.init(fstream);
let data = "";
while (sstream.available()) {
data += sstream.read(kStringBlockSize);
}
sstream.close();
fstream.close();
return data;
},
/**
* Save a string containing ASCII text into a file. The file will be overwritten
* and contain only the given text.
*
* @param aFile An nsIFile representing the file to write or a string containing
* the file name of a file under user's profile.
* @param aData The string to write.
* @param aPerms The octal file permissions for the created file. If unset
* the default of 0o600 is used.
*/
saveStringToFile(aFile, aData, aPerms = 0o600) {
let file;
if (!(aFile instanceof Ci.nsIFile)) {
file = Services.dirsvc.get("ProfD", Ci.nsIFile);
file.append(aFile);
} else {
file = aFile;
}
let foStream = Cc[
"@mozilla.org/network/safe-file-output-stream;1"
].createInstance(Ci.nsIFileOutputStream);
// PR_WRONLY + PR_CREATE_FILE + PR_TRUNCATE
foStream.init(file, 0x02 | 0x08 | 0x20, aPerms, 0);
// safe-file-output-stream appears to throw an error if it doesn't write everything at once
// so we won't worry about looping to deal with partial writes.
// In case we try to use this function for big files where buffering
// is needed we could use the implementation in saveStreamToFile().
foStream.write(aData, aData.length);
foStream.QueryInterface(Ci.nsISafeOutputStream).finish();
foStream.close();
},
/**
* Saves the given input stream to a file.
*
* @param aIStream The input stream to save.
* @param aFile The file to which the stream is saved.
* @param aPerms The octal file permissions for the created file. If unset
* the default of 0o600 is used.
*/
saveStreamToFile(aIStream, aFile, aPerms = 0o600) {
if (!(aIStream instanceof Ci.nsIInputStream)) {
throw new Error("Invalid stream passed to saveStreamToFile");
}
if (!(aFile instanceof Ci.nsIFile)) {
throw new Error("Invalid file passed to saveStreamToFile");
}
let fstream = Cc[
"@mozilla.org/network/safe-file-output-stream;1"
].createInstance(Ci.nsIFileOutputStream);
let buffer = Cc[
"@mozilla.org/network/buffered-output-stream;1"
].createInstance(Ci.nsIBufferedOutputStream);
// Write the input stream to the file.
// PR_WRITE + PR_CREATE + PR_TRUNCATE
fstream.init(aFile, 0x04 | 0x08 | 0x20, aPerms, 0);
buffer.init(fstream, kStreamBlockSize);
buffer.writeFrom(aIStream, aIStream.available());
// Close the output streams.
if (buffer instanceof Ci.nsISafeOutputStream) {
buffer.finish();
} else {
buffer.close();
}
if (fstream instanceof Ci.nsISafeOutputStream) {
fstream.finish();
} else {
fstream.close();
}
// Close the input stream.
aIStream.close();
return aFile;
},
/**
* Returns size of system memory.
*/
getPhysicalMemorySize() {
return Services.sysinfo.getPropertyAsInt64("memsize");
},
};

View File

@@ -0,0 +1,599 @@
/* 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/. */
/**
* Load overlays in a similar way as XUL did for legacy XUL add-ons.
*/
"use strict";
this.EXPORTED_SYMBOLS = ["Overlays"];
const { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
ChromeUtils.defineModuleGetter(
this,
"Services",
"resource://gre/modules/Services.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"setTimeout",
"resource://gre/modules/Timer.jsm"
);
let oconsole = new ConsoleAPI({
prefix: "Overlays.jsm",
consoleID: "overlays-jsm",
maxLogLevelPref: "extensions.overlayloader.loglevel",
});
/**
* The overlays class, providing support for loading overlays like they used to work. This class
* should likely be called through its static method Overlays.load()
*/
class Overlays {
/**
* Load overlays for the given window using the overlay provider, which can for example be a
* ChromeManifest object.
*
* @param {ChromeManifest} overlayProvider The overlay provider that contains information
* about styles and overlays.
* @param {DOMWindow} window The window to load into
*/
static load(overlayProvider, window) {
let instance = new Overlays(overlayProvider, window);
let urls = overlayProvider.overlay.get(instance.location, false);
instance.load(urls);
}
/**
* Constructs the overlays instance. This class should be called via Overlays.load() instead.
*
* @param {ChromeManifest} overlayProvider The overlay provider that contains information
* about styles and overlays.
* @param {DOMWindow} window The window to load into
*/
constructor(overlayProvider, window) {
this.overlayProvider = overlayProvider;
this.window = window;
if (window.location.protocol == "about:") {
this.location = window.location.protocol + window.location.pathname;
} else {
this.location = window.location.origin + window.location.pathname;
}
}
/**
* A shorthand to this.window.document
*/
get document() {
return this.window.document;
}
/**
* Loads the given urls into the window, recursively loading further overlays as provided by the
* overlayProvider.
*
* @param {string[]} urls The urls to load
*/
load(urls) {
let unloadedOverlays = this._collectOverlays(this.document).concat(urls);
let forwardReferences = [];
let unloadedScripts = [];
let unloadedSheets = [];
this._toolbarsToResolve = [];
let xulStore = Services.xulStore;
this.persistedIDs = new Set();
// Load css styles from the registry
for (let sheet of this.overlayProvider.style.get(this.location, false)) {
unloadedSheets.push(sheet);
}
if (!unloadedOverlays.length && !unloadedSheets.length) {
return;
}
while (unloadedOverlays.length) {
let url = unloadedOverlays.shift();
let xhr = this.fetchOverlay(url);
let doc = xhr.responseXML;
oconsole.debug(`Applying ${url} to ${this.location}`);
// clean the document a bit
let emptyNodes = doc.evaluate(
"//text()[normalize-space(.) = '']",
doc,
null,
7,
null
);
for (let i = 0, len = emptyNodes.snapshotLength; i < len; ++i) {
let node = emptyNodes.snapshotItem(i);
node.remove();
}
let commentNodes = doc.evaluate("//comment()", doc, null, 7, null);
for (let i = 0, len = commentNodes.snapshotLength; i < len; ++i) {
let node = commentNodes.snapshotItem(i);
node.remove();
}
// Load css styles from the registry
for (let sheet of this.overlayProvider.style.get(url, false)) {
unloadedSheets.push(sheet);
}
// Load css processing instructions from the overlay
let stylesheets = doc.evaluate(
"/processing-instruction('xml-stylesheet')",
doc,
null,
7,
null
);
for (let i = 0, len = stylesheets.snapshotLength; i < len; ++i) {
let node = stylesheets.snapshotItem(i);
let match = node.nodeValue.match(/href=["']([^"']*)["']/);
if (match) {
unloadedSheets.push(new URL(match[1], node.baseURI).href);
}
}
// Prepare loading further nested xul overlays from the overlay
unloadedOverlays.push(...this._collectOverlays(doc));
// Prepare loading further nested xul overlays from the registry
for (let overlayUrl of this.overlayProvider.overlay.get(url, false)) {
unloadedOverlays.push(overlayUrl);
}
// Run through all overlay nodes on the first level (hookup nodes). Scripts will be deferred
// until later for simplicity (c++ code seems to process them earlier?).
for (let node of doc.documentElement.children) {
if (node.localName == "script") {
unloadedScripts.push(node);
} else {
forwardReferences.push(node);
}
}
}
let ids = xulStore.getIDsEnumerator(this.location);
while (ids.hasMore()) {
this.persistedIDs.add(ids.getNext());
}
// At this point, all (recursive) overlays are loaded. Unloaded scripts and sheets are ready and
// in order, and forward references are good to process.
let previous = 0;
while (forwardReferences.length && forwardReferences.length != previous) {
previous = forwardReferences.length;
let unresolved = [];
for (let ref of forwardReferences) {
if (!this._resolveForwardReference(ref)) {
unresolved.push(ref);
}
}
forwardReferences = unresolved;
}
if (forwardReferences.length) {
oconsole.warn(
`Could not resolve ${forwardReferences.length} references`,
forwardReferences
);
}
// Loading the sheets now to avoid race conditions with xbl bindings
for (let sheet of unloadedSheets) {
this.loadCSS(sheet);
}
this._decksToResolve = new Map();
for (let id of this.persistedIDs.values()) {
let element = this.document.getElementById(id);
if (element) {
let attrNames = xulStore.getAttributeEnumerator(this.location, id);
while (attrNames.hasMore()) {
let attrName = attrNames.getNext();
let attrValue = xulStore.getValue(this.location, id, attrName);
if (attrName == "selectedIndex" && element.localName == "deck") {
this._decksToResolve.set(element, attrValue);
} else {
element.setAttribute(attrName, attrValue);
}
}
}
}
// We've resolved all the forward references we can, we can now go ahead and load the scripts
let deferredLoad = [];
for (let script of unloadedScripts) {
deferredLoad.push(...this.loadScript(script));
}
if (this.document.readyState == "complete") {
let sheet;
let overlayTrigger = this.document.createXULElement("overlayTrigger");
overlayTrigger.addEventListener(
"bindingattached",
() => {
oconsole.debug("XBL binding attached, continuing with load");
if (sheet) {
sheet.remove();
}
overlayTrigger.remove();
setTimeout(() => {
this._finish();
// Now execute load handlers since we are done loading scripts
let bubbles = [];
for (let { listener, useCapture } of deferredLoad) {
if (useCapture) {
this._fireEventListener(listener);
} else {
bubbles.push(listener);
}
}
for (let listener of bubbles) {
this._fireEventListener(listener);
}
}, 0);
},
{ once: true }
);
this.document.documentElement.appendChild(overlayTrigger);
if (overlayTrigger.parentNode) {
sheet = this.loadCSS("chrome://messenger/content/overlayBindings.css");
}
} else {
this.document.defaultView.addEventListener(
"load",
this._finish.bind(this),
{ once: true }
);
}
}
_finish() {
for (let [deck, selectedIndex] of this._decksToResolve.entries()) {
deck.setAttribute("selectedIndex", selectedIndex);
}
for (let bar of this._toolbarsToResolve) {
let currentset = Services.xulStore.getValue(
this.location,
bar.id,
"currentset"
);
if (currentset) {
bar.currentSet = currentset;
} else if (bar.getAttribute("defaultset")) {
bar.currentSet = bar.getAttribute("defaultset");
}
}
}
/**
* Gets the overlays referenced by processing instruction on a document.
*
* @param {DOMDocument} document The document to read instuctions from
* @returns {string[]} URLs of the overlays from the document
*/
_collectOverlays(doc) {
let urls = [];
let instructions = doc.evaluate(
"/processing-instruction('xul-overlay')",
doc,
null,
7,
null
);
for (let i = 0, len = instructions.snapshotLength; i < len; ++i) {
let node = instructions.snapshotItem(i);
let match = node.nodeValue.match(/href=["']([^"']*)["']/);
if (match) {
urls.push(match[1]);
}
}
return urls;
}
/**
* Fires a "load" event for the given listener, using the current window
*
* @param {EventListener|Function} listener The event listener to call
*/
_fireEventListener(listener) {
let fakeEvent = new this.window.UIEvent("load", { view: this.window });
if (typeof listener == "function") {
listener(fakeEvent);
} else if (listener && typeof listener == "object") {
listener.handleEvent(fakeEvent);
} else {
oconsole.error("Unknown listener type", listener);
}
}
/**
* Resolves forward references for the given node. If the node exists in the target document, it
* is merged in with the target node. If the node has no id it is inserted at documentElement
* level.
*
* @param {Element} node The DOM Element to resolve in the target document.
* @returns {boolean} True, if the node was merged/inserted, false otherwise
*/
_resolveForwardReference(node) {
if (node.id) {
let target = this.document.getElementById(node.id);
if (node.localName == "toolbarpalette") {
let box;
if (target) {
box = target.closest("toolbox");
} else {
// These vanish from the document but still exist via the palette property
let boxes = [...this.document.getElementsByTagName("toolbox")];
box = boxes.find(box => box.palette && box.palette.id == node.id);
let palette = box ? box.palette : null;
if (!palette) {
oconsole.debug(
`The palette for ${node.id} could not be found, deferring to later`
);
return false;
}
target = palette;
}
this._toolbarsToResolve.push(
...box.querySelectorAll('toolbar:not([type="menubar"])')
);
} else if (!target) {
oconsole.debug(
`The node ${node.id} could not be found, deferring to later`
);
return false;
}
this._mergeElement(target, node);
} else {
this._insertElement(this.document.documentElement, node);
}
return true;
}
/**
* Insert the node in the given parent, observing the insertbefore/insertafter/position attributes
*
* @param {Element} parent The parent element to insert the node into.
* @param {Element} node The node to insert.
*/
_insertElement(parent, node) {
// These elements need their values set before they are added to
// the document, or bad things happen.
for (let element of node.querySelectorAll("menulist, radiogroup")) {
if (element.id && this.persistedIDs.has(element.id)) {
element.setAttribute(
"value",
Services.xulStore.getValue(this.location, element.id, "value")
);
}
}
let wasInserted = false;
let pos = node.getAttribute("insertafter");
let after = true;
if (!pos) {
pos = node.getAttribute("insertbefore");
after = false;
}
if (pos) {
for (let id of pos.split(",")) {
let targetchild = this.document.getElementById(id);
if (targetchild && targetchild.parentNode == parent) {
parent.insertBefore(
node,
after ? targetchild.nextSibling : targetchild
);
wasInserted = true;
break;
}
}
}
if (!wasInserted) {
// position is 1-based
let position = parseInt(node.getAttribute("position"), 10);
if (position > 0 && position - 1 <= parent.childNodes.length) {
parent.insertBefore(node, parent.childNodes[position - 1]);
wasInserted = true;
}
}
if (!wasInserted) {
parent.appendChild(node);
}
}
/**
* Merge the node into the target, adhering to the removeelement attribute, merging further
* attributes into the target node, and merging children as appropriate for xul nodes. If a child
* has an id, it will be searched in the target document and recursively merged.
*
* @param {Element} target The node to merge into
* @param {Element} node The node that is being merged
*/
_mergeElement(target, node) {
for (let attribute of node.attributes) {
if (attribute.name == "id") {
continue;
}
if (attribute.name == "removeelement" && attribute.value == "true") {
target.remove();
return;
}
target.setAttributeNS(
attribute.namespaceURI,
attribute.name,
attribute.value
);
}
for (let i = 0, len = node.childElementCount; i < len; i++) {
let child = node.firstElementChild;
child.remove();
let elementInDocument = child.id
? this.document.getElementById(child.id)
: null;
let parentId = elementInDocument ? elementInDocument.parentNode.id : null;
if (parentId && parentId == target.id) {
this._mergeElement(elementInDocument, child);
} else {
this._insertElement(target, child);
}
}
}
/**
* Fetches the overlay from the given chrome:// or resource:// URL. This happen synchronously so
* we have a chance to complete before the load event.
*
* @param {string} srcUrl The URL to load
* @returns {XMLHttpRequest} The completed XHR.
*/
fetchOverlay(srcUrl) {
if (!srcUrl.startsWith("chrome://") && !srcUrl.startsWith("resource://")) {
throw new Error(
"May only load overlays from chrome:// or resource:// uris"
);
}
let xhr = new XMLHttpRequest();
xhr.overrideMimeType("application/xml");
xhr.open("GET", srcUrl, false);
// Elevate the request, so DTDs will work. Should not be a security issue since we
// only load chrome, resource and file URLs, and that is our privileged chrome package.
try {
xhr.channel.owner = Services.scriptSecurityManager.getSystemPrincipal();
} catch (ex) {
oconsole.error(
"Failed to set system principal while fetching overlay " + srcUrl
);
xhr.close();
throw new Error("Failed to set system principal");
}
xhr.send(null);
return xhr;
}
/**
* Loads scripts described by the given script node. The node can either have a src attribute, or
* be an inline script with textContent.
*
* @param {Element} node The <script> element to load the script from
* @returns {Object[]} An object with listener and useCapture,
* describing load handlers the script creates
* when first run.
*/
loadScript(node) {
let deferredLoad = [];
let oldAddEventListener = this.window.addEventListener;
if (this.document.readyState == "complete") {
this.window.addEventListener = function(
type,
listener,
useCapture,
...args
) {
if (type == "load") {
if (typeof useCapture == "object") {
useCapture = useCapture.capture;
}
if (typeof useCapture == "undefined") {
useCapture = true;
}
deferredLoad.push({ listener, useCapture });
return null;
}
return oldAddEventListener.call(
this,
type,
listener,
useCapture,
...args
);
};
}
if (node.hasAttribute("src")) {
let url = new URL(node.getAttribute("src"), node.baseURI).href;
oconsole.debug(`Loading script ${url} into ${this.window.location}`);
try {
Services.scriptloader.loadSubScript(url, this.window);
} catch (ex) {
Cu.reportError(ex);
}
} else if (node.textContent) {
oconsole.debug(`Loading eval'd script into ${this.window.location}`);
try {
let dataURL =
"data:application/javascript," + encodeURIComponent(node.textContent);
// It would be great if we could have script errors show the right url, but for now
// loadSubScript will have to do.
Services.scriptloader.loadSubScript(dataURL, this.window);
} catch (ex) {
Cu.reportError(ex);
}
}
if (this.document.readyState == "complete") {
this.window.addEventListener = oldAddEventListener;
}
// This works because we only care about immediately executed addEventListener calls and
// loadSubScript is synchronous. Everyone else should be checking readyState anyway.
return deferredLoad;
}
/**
* Load the CSS stylesheet from the given url
*
* @param {string} url The url to load from
* @returns {Element} An HTML link element for this stylesheet
*/
loadCSS(url) {
oconsole.debug(`Loading ${url} into ${this.window.location}`);
// domWindowUtils.loadSheetUsingURIString doesn't record the sheet in document.styleSheets,
// adding a html link element seems to do so.
let link = this.document.createElementNS(
"http://www.w3.org/1999/xhtml",
"link"
);
link.setAttribute("rel", "stylesheet");
link.setAttribute("type", "text/css");
link.setAttribute("href", url);
this.document.documentElement.appendChild(link);
return link;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,138 @@
/* 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";
var EXPORTED_SYMBOLS = ["InstallRDF"];
ChromeUtils.defineModuleGetter(
this,
"RDFDataSource",
"resource:///modules/RDFDataSource.jsm"
);
const RDFURI_INSTALL_MANIFEST_ROOT = "urn:mozilla:install-manifest";
function EM_R(aProperty) {
return `http://www.mozilla.org/2004/em-rdf#${aProperty}`;
}
function getValue(literal) {
return literal && literal.getValue();
}
function getProperty(resource, property) {
return getValue(resource.getProperty(EM_R(property)));
}
class Manifest {
constructor(ds) {
this.ds = ds;
}
static loadFromString(text) {
return new this(RDFDataSource.loadFromString(text));
}
static loadFromBuffer(buffer) {
return new this(RDFDataSource.loadFromBuffer(buffer));
}
static async loadFromFile(uri) {
return new this(await RDFDataSource.loadFromFile(uri));
}
}
class InstallRDF extends Manifest {
_readProps(source, obj, props) {
for (let prop of props) {
let val = getProperty(source, prop);
if (val != null) {
obj[prop] = val;
}
}
}
_readArrayProp(source, obj, prop, target, decode = getValue) {
let result = Array.from(source.getObjects(EM_R(prop)), target =>
decode(target)
);
if (result.length) {
obj[target] = result;
}
}
_readArrayProps(source, obj, props, decode = getValue) {
for (let [prop, target] of Object.entries(props)) {
this._readArrayProp(source, obj, prop, target, decode);
}
}
_readLocaleStrings(source, obj) {
this._readProps(source, obj, [
"name",
"description",
"creator",
"homepageURL",
]);
this._readArrayProps(source, obj, {
locale: "locales",
developer: "developers",
translator: "translators",
contributor: "contributors",
});
}
decode() {
let root = this.ds.getResource(RDFURI_INSTALL_MANIFEST_ROOT);
let result = {};
let props = [
"id",
"version",
"type",
"updateURL",
"optionsURL",
"optionsType",
"aboutURL",
"iconURL",
"bootstrap",
"unpack",
"strictCompatibility",
];
this._readProps(root, result, props);
let decodeTargetApplication = source => {
let app = {};
this._readProps(source, app, ["id", "minVersion", "maxVersion"]);
return app;
};
let decodeLocale = source => {
let localized = {};
this._readLocaleStrings(source, localized);
return localized;
};
this._readLocaleStrings(root, result);
this._readArrayProps(root, result, { targetPlatform: "targetPlatforms" });
this._readArrayProps(
root,
result,
{ targetApplication: "targetApplications" },
decodeTargetApplication
);
this._readArrayProps(
root,
result,
{ localized: "localized" },
decodeLocale
);
this._readArrayProps(root, result, { dependency: "dependencies" }, source =>
getProperty(source, "id")
);
return result;
}
}

View File

@@ -0,0 +1,132 @@
/* 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 file contains helper methods for dealing with XPCOM iterators (arrays
* and enumerators) in JS-friendly ways.
*/
const EXPORTED_SYMBOLS = ["fixIterator", "toXPCOMArray", "toArray"];
var JS_HAS_SYMBOLS = typeof Symbol === "function";
var ITERATOR_SYMBOL = JS_HAS_SYMBOLS ? Symbol.iterator : "@@iterator";
/**
* This function will take a number of objects and convert them to an array.
*
* Currently, we support the following objects:
* Anything you can for (let x of aObj) on
* (e.g. toArray(fixIterator(enum))[4],
* also a NodeList from element.children)
*
* @param aObj The object to convert
*/
function toArray(aObj) {
// Iterable object
if (ITERATOR_SYMBOL in aObj) {
return Array.from(aObj);
}
// New style generator function
if (
typeof aObj == "function" &&
typeof aObj.constructor == "function" &&
aObj.constructor.name == "GeneratorFunction"
) {
return [...aObj()];
}
// We got something unexpected, notify the caller loudly.
throw new Error(
"An unsupported object sent to toArray: " +
("toString" in aObj ? aObj.toString() : aObj)
);
}
/**
* Given a JS array, JS iterator, or one of a variety of XPCOM collections or
* iterators, return a JS iterator suitable for use in a for...of expression.
*
* Currently, we support the following types of XPCOM iterators:
* nsIArray
* nsISimpleEnumerator
*
* Note that old-style JS iterators are explicitly not supported in this
* method, as they are going away.
*
* @param aEnum the enumerator to convert
* @param aIface (optional) an interface to QI each object to prior to
* returning
*
* @note This returns an object that can be used in 'for...of' loops.
* Do not use 'for each...in' or 'for...in'.
* This does *not* return an Array object. To create such an array, use
* let array = toArray(fixIterator(xpcomEnumerator));
*/
function fixIterator(aEnum, aIface) {
// If the input is an array, nsISimpleEnumerator or something that sports Symbol.iterator,
// then the original input is sufficient to directly return. However, if we want
// to support the aIface parameter, we need to do a lazy version of
// Array.prototype.map.
if (
Array.isArray(aEnum) ||
aEnum instanceof Ci.nsISimpleEnumerator ||
ITERATOR_SYMBOL in aEnum
) {
if (!aIface) {
return aEnum[ITERATOR_SYMBOL]();
}
return (function*() {
for (let o of aEnum) {
yield o.QueryInterface(aIface);
}
})();
}
let face = aIface || Ci.nsISupports;
// Figure out which kind of array object we have.
// First try nsIArray (covers nsIMutableArray too).
if (aEnum instanceof Ci.nsIArray) {
return (function*() {
let count = aEnum.length;
for (let i = 0; i < count; i++) {
yield aEnum.queryElementAt(i, face);
}
})();
}
// We got something unexpected, notify the caller loudly.
throw new Error(
"An unsupported object sent to fixIterator: " +
("toString" in aEnum ? aEnum.toString() : aEnum)
);
}
/**
* This function takes an Array object and returns an XPCOM array
* of the desired type. It will *not* work if you extend Array.prototype.
*
* @param aArray the array (anything fixIterator supports) to convert to an XPCOM array
* @param aInterface the type of XPCOM array to convert
*
* @note The returned array is *not* dynamically updated. Changes made to the
* JS array after a call to this function will not be reflected in the
* XPCOM array.
*/
function toXPCOMArray(aArray, aInterface) {
if (aInterface.equals(Ci.nsIMutableArray)) {
let mutableArray = Cc["@mozilla.org/array;1"].createInstance(
Ci.nsIMutableArray
);
for (let item of fixIterator(aArray)) {
mutableArray.appendElement(item);
}
return mutableArray;
}
// We got something unexpected, notify the caller loudly.
throw new Error(
"An unsupported interface requested from toXPCOMArray: " + aInterface
);
}

View File

@@ -0,0 +1,21 @@
# vim: set filetype=python:
# 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/.
EXTRA_JS_MODULES += [
"BootstrapLoader.jsm",
"ChromeManifest.jsm",
"ExtensionSupport.jsm",
"IOUtils.jsm",
"iteratorUtils.jsm",
"Overlays.jsm",
"RDFDataSource.jsm",
"RDFManifestConverter.jsm",
]
LOCAL_INCLUDES += ["/netwerk/base"]
FINAL_LIBRARY = "xul"
XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.ini"]

View File

@@ -0,0 +1,9 @@
"use strict";
module.exports = {
extends: "plugin:mozilla/xpcshell-test",
rules: {
"func-names": "off",
},
};

View File

@@ -0,0 +1,42 @@
/* 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";
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
var EXPORTED_SYMBOLS = ["monitor"];
function notify(event, originalMethod, data, reason) {
let info = {
event,
data: Object.assign({}, data, {
resourceURI: data.resourceURI.spec,
}),
reason,
};
let subject = { wrappedJSObject: { data } };
Services.obs.notifyObservers(
subject,
"bootstrapmonitor-event",
JSON.stringify(info)
);
// If the bootstrap scope already declares a method call it
if (originalMethod) {
originalMethod(data, reason);
}
}
// Allows a simple one-line bootstrap script:
// Components.utils.import("resource://xpcshelldata/bootstrapmonitor.jsm").monitor(this);
var monitor = function(
scope,
methods = ["install", "startup", "shutdown", "uninstall"]
) {
for (let event of methods) {
scope[event] = notify.bind(null, event, scope[event]);
}
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
const ADDONS = {
test_bootstrap_const: {
"install.rdf": createInstallRDF({
id: "bootstrap@tests.mozilla.org",
}),
"bootstrap.js":
'var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");\n\nconst install = function() {\n Services.obs.notifyObservers(null, "addon-install");\n};\n',
},
};
add_task(async function() {
await promiseStartupManager();
let sawInstall = false;
Services.obs.addObserver(function() {
sawInstall = true;
}, "addon-install");
await AddonTestUtils.promiseInstallXPI(ADDONS.test_bootstrap_const);
ok(sawInstall);
});

View File

@@ -0,0 +1,65 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
// This verifies that bootstrap.js has the expected globals defined
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
const ADDONS = {
bootstrap_globals: {
"install.rdf": createInstallRDF({
id: "bootstrap_globals@tests.mozilla.org",
}),
"bootstrap.js": String.raw`var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
var seenGlobals = new Set();
var scope = this;
function checkGlobal(name, type) {
if (scope[name] && typeof(scope[name]) == type)
seenGlobals.add(name);
}
var wrapped = {};
Services.obs.notifyObservers({ wrappedJSObject: wrapped }, "bootstrap-request-globals");
for (let [name, type] of wrapped.expectedGlobals) {
checkGlobal(name, type);
}
function startup(data, reason) {
Services.obs.notifyObservers({ wrappedJSObject: seenGlobals }, "bootstrap-seen-globals");
}
function install(data, reason) {}
function shutdown(data, reason) {}
function uninstall(data, reason) {}
`,
},
};
const EXPECTED_GLOBALS = [["console", "object"]];
async function run_test() {
do_test_pending();
await promiseStartupManager();
let sawGlobals = false;
Services.obs.addObserver(function(subject) {
subject.wrappedJSObject.expectedGlobals = EXPECTED_GLOBALS;
}, "bootstrap-request-globals");
Services.obs.addObserver(function({ wrappedJSObject: seenGlobals }) {
for (let [name] of EXPECTED_GLOBALS) {
Assert.ok(seenGlobals.has(name));
}
sawGlobals = true;
}, "bootstrap-seen-globals");
await AddonTestUtils.promiseInstallXPI(ADDONS.bootstrap_globals);
Assert.ok(sawGlobals);
await promiseShutdownManager();
do_test_finished();
}

View File

@@ -0,0 +1,54 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
const ADDON = {
"install.rdf": createInstallRDF({
id: "bug675371@tests.mozilla.org",
}),
"chrome.manifest": `content bug675371 .`,
"test.js": `var active = true;`,
};
add_task(async function run_test() {
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
await promiseStartupManager();
});
function checkActive(expected) {
let target = { active: false };
let load = () => {
Services.scriptloader.loadSubScript(
"chrome://bug675371/content/test.js",
target
);
};
if (expected) {
load();
} else {
Assert.throws(load, /Error opening input stream/);
}
equal(target.active, expected, "Manifest is active?");
}
add_task(async function test() {
let { addon } = await AddonTestUtils.promiseInstallXPI(ADDON);
Assert.ok(addon.isActive);
// Tests that chrome.manifest is registered when the addon is installed.
checkActive(true);
await addon.disable();
checkActive(false);
await addon.enable();
checkActive(true);
await promiseShutdownManager();
// Tests that chrome.manifest remains registered at app shutdown.
checkActive(true);
});

View File

@@ -0,0 +1,167 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
// Test that side-loaded extensions with invalid install.rdf files are
// not initialized at startup.
const APP_ID = "xpcshell@tests.mozilla.org";
Services.prefs.setIntPref("extensions.enabledScopes", AddonManager.SCOPE_USER);
createAppInfo(APP_ID, "XPCShell", "1", "1.9.2");
const userAppDir = AddonTestUtils.profileDir.clone();
userAppDir.append("app-extensions");
userAppDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
AddonTestUtils.registerDirectory("XREUSysExt", userAppDir);
const userExtensions = userAppDir.clone();
userExtensions.append(APP_ID);
userExtensions.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
XPCOMUtils.defineLazyServiceGetters(this, {
ChromeRegistry: [
"@mozilla.org/chrome/chrome-registry;1",
"nsIChromeRegistry",
],
});
function hasChromeEntry(package) {
try {
void ChromeRegistry.convertChromeURL(
Services.io.newURI(`chrome://${package}/content/`)
);
return true;
} catch (e) {
return false;
}
}
add_task(async function() {
await promiseWriteInstallRDFToXPI(
{
id: "langpack-foo@addons.mozilla.org",
version: "1.0",
type: 8,
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
name: "Invalid install.rdf extension",
},
userExtensions,
undefined,
{
"chrome.manifest": `
content foo-langpack ./
`,
}
);
await promiseWriteInstallRDFToXPI(
{
id: "foo@addons.mozilla.org",
version: "1.0",
bootstrap: true,
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
name: "Invalid install.rdf extension",
},
userExtensions,
undefined,
{
"chrome.manifest": `
content foo ./
`,
}
);
await promiseWriteInstallRDFToXPI(
{
id: "foo-legacy-legacy@addons.mozilla.org",
version: "1.0",
bootstrap: false,
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
name: "Invalid install.rdf extension",
},
userExtensions,
undefined,
{
"chrome.manifest": `
content foo-legacy-legacy ./
`,
}
);
equal(
hasChromeEntry("foo-langpack"),
false,
"Should not have registered foo-langpack resource before AOM startup"
);
equal(
hasChromeEntry("foo-legacy-legacy"),
false,
"Should not have registered foo-legacy-legacy resource before AOM startup"
);
equal(
hasChromeEntry("foo"),
false,
"Should not have registered foo resource before AOM startup"
);
await promiseStartupManager();
equal(
hasChromeEntry("foo-langpack"),
false,
"Should not have registered chrome manifest for invalid extension"
);
equal(
hasChromeEntry("foo-legacy-legacy"),
false,
"Should not have registered chrome manifest for non-restartless extension"
);
equal(
hasChromeEntry("foo"),
true,
"Should have registered chrome manifest for valid extension"
);
await promiseRestartManager();
equal(
hasChromeEntry("foo-langpack"),
false,
"Should still not have registered chrome manifest for invalid extension after restart"
);
equal(
hasChromeEntry("foo-legacy-legacy"),
false,
"Should still not have registered chrome manifest for non-restartless extension"
);
equal(
hasChromeEntry("foo"),
true,
"Should still have registered chrome manifest for valid extension after restart"
);
await promiseShutdownManager();
userAppDir.remove(true);
});

View File

@@ -0,0 +1,806 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
// This tests that all properties are read from the install manifests and that
// items are correctly enabled/disabled based on them (blocklist tests are
// elsewhere)
const ADDONS = [
{
"install.rdf": {
id: "addon1@tests.mozilla.org",
version: "1.0",
bootstrap: true,
aboutURL: "chrome://test/content/about.xul",
iconURL: "chrome://test/skin/icon.png",
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
name: "Test Addon 1",
description: "Test Description",
creator: "Test Creator",
homepageURL: "http://www.example.com",
developer: ["Test Developer 1", "Test Developer 2"],
translator: ["Test Translator 1", "Test Translator 2"],
contributor: ["Test Contributor 1", "Test Contributor 2"],
},
expected: {
id: "addon1@tests.mozilla.org",
type: "extension",
version: "1.0",
optionsType: null,
aboutURL: "chrome://test/content/about.xul",
iconURL: "chrome://test/skin/icon.png",
icons: {
32: "chrome://test/skin/icon.png",
48: "chrome://test/skin/icon.png",
},
name: "Test Addon 1",
description: "Test Description",
creator: "Test Creator",
homepageURL: "http://www.example.com",
developers: ["Test Developer 1", "Test Developer 2"],
translators: ["Test Translator 1", "Test Translator 2"],
contributors: ["Test Contributor 1", "Test Contributor 2"],
isActive: true,
userDisabled: false,
appDisabled: false,
isCompatible: true,
providesUpdatesSecurely: true,
blocklistState: Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
},
},
{
"install.rdf": {
id: "addon2@tests.mozilla.org",
version: "1.0",
bootstrap: true,
updateURL: "https://www.foo.com",
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
name: "Test Addon 2",
},
expected: {
id: "addon2@tests.mozilla.org",
isActive: true,
userDisabled: false,
appDisabled: false,
providesUpdatesSecurely: true,
},
},
{
"install.rdf": {
id: "addon3@tests.mozilla.org",
version: "1.0",
bootstrap: true,
updateURL: "http://www.foo.com",
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
name: "Test Addon 3",
},
expected: {
id: "addon3@tests.mozilla.org",
isActive: false,
userDisabled: false,
appDisabled: true,
providesUpdatesSecurely: false,
},
},
{
"install.rdf": {
id: "addon4@tests.mozilla.org",
version: "1.0",
bootstrap: true,
updateURL: "http://www.foo.com",
updateKey: "foo",
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
name: "Test Addon 4",
},
expected: {
id: "addon4@tests.mozilla.org",
isActive: false,
userDisabled: false,
appDisabled: true,
providesUpdatesSecurely: false,
},
},
{
"install.rdf": {
id: "addon5@tests.mozilla.org",
version: "1.0",
bootstrap: true,
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "*",
},
],
name: "Test Addon 5",
},
expected: {
isActive: true,
userDisabled: false,
appDisabled: false,
isCompatible: true,
},
},
{
"install.rdf": {
id: "addon6@tests.mozilla.org",
version: "1.0",
bootstrap: true,
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "0",
maxVersion: "1",
},
],
name: "Test Addon 6",
},
expected: {
isActive: true,
userDisabled: false,
appDisabled: false,
isCompatible: true,
},
},
{
"install.rdf": {
id: "addon7@tests.mozilla.org",
version: "1.0",
bootstrap: true,
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "0",
maxVersion: "0",
},
],
name: "Test Addon 7",
},
expected: {
isActive: false,
userDisabled: false,
appDisabled: true,
isCompatible: false,
},
},
{
"install.rdf": {
id: "addon8@tests.mozilla.org",
version: "1.0",
bootstrap: true,
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1.1",
maxVersion: "*",
},
],
name: "Test Addon 8",
},
expected: {
isActive: false,
userDisabled: false,
appDisabled: true,
isCompatible: false,
},
},
{
"install.rdf": {
id: "addon9@tests.mozilla.org",
version: "1.0",
bootstrap: true,
targetApplications: [
{
id: "toolkit@mozilla.org",
minVersion: "1.9.2",
maxVersion: "1.9.*",
},
],
name: "Test Addon 9",
},
expected: {
isActive: true,
userDisabled: false,
appDisabled: false,
isCompatible: true,
},
},
{
"install.rdf": {
id: "addon10@tests.mozilla.org",
version: "1.0",
bootstrap: true,
targetApplications: [
{
id: "toolkit@mozilla.org",
minVersion: "1.9.2.1",
maxVersion: "1.9.*",
},
],
name: "Test Addon 10",
},
expected: {
isActive: false,
userDisabled: false,
appDisabled: true,
isCompatible: false,
},
},
{
"install.rdf": {
id: "addon11@tests.mozilla.org",
version: "1.0",
bootstrap: true,
targetApplications: [
{
id: "toolkit@mozilla.org",
minVersion: "1.9",
maxVersion: "1.9.2",
},
],
name: "Test Addon 11",
},
expected: {
isActive: true,
userDisabled: false,
appDisabled: false,
isCompatible: true,
},
},
{
"install.rdf": {
id: "addon12@tests.mozilla.org",
version: "1.0",
bootstrap: true,
targetApplications: [
{
id: "toolkit@mozilla.org",
minVersion: "1.9",
maxVersion: "1.9.1.*",
},
],
name: "Test Addon 12",
},
expected: {
isActive: false,
userDisabled: false,
appDisabled: true,
isCompatible: false,
},
},
{
"install.rdf": {
id: "addon13@tests.mozilla.org",
version: "1.0",
bootstrap: true,
targetApplications: [
{
id: "toolkit@mozilla.org",
minVersion: "1.9",
maxVersion: "1.9.*",
},
{
id: "xpcshell@tests.mozilla.org",
minVersion: "0",
maxVersion: "0.5",
},
],
name: "Test Addon 13",
},
expected: {
isActive: false,
userDisabled: false,
appDisabled: true,
isCompatible: false,
},
},
{
"install.rdf": {
id: "addon14@tests.mozilla.org",
version: "1.0",
bootstrap: true,
targetApplications: [
{
id: "toolkit@mozilla.org",
minVersion: "1.9",
maxVersion: "1.9.1",
},
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
name: "Test Addon 14",
},
expected: {
isActive: true,
userDisabled: false,
appDisabled: false,
isCompatible: true,
},
},
{
"install.rdf": {
id: "addon15@tests.mozilla.org",
version: "1.0",
bootstrap: true,
updateKey: "foo",
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
name: "Test Addon 15",
},
expected: {
isActive: true,
userDisabled: false,
appDisabled: false,
isCompatible: true,
providesUpdatesSecurely: true,
},
},
{
"install.rdf": {
id: "addon16@tests.mozilla.org",
version: "1.0",
bootstrap: true,
updateKey: "foo",
updateURL: "https://www.foo.com",
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
name: "Test Addon 16",
},
expected: {
isActive: true,
userDisabled: false,
appDisabled: false,
isCompatible: true,
providesUpdatesSecurely: true,
},
},
{
"install.rdf": {
id: "addon17@tests.mozilla.org",
version: "1.0",
bootstrap: true,
optionsURL: "chrome://test/content/options.xul",
optionsType: "2",
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
name: "Test Addon 17",
},
// An obsolete optionsType means the add-on isn't registered.
expected: null,
},
{
"install.rdf": {
id: "addon18@tests.mozilla.org",
version: "1.0",
bootstrap: true,
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
name: "Test Addon 18",
},
extraFiles: { "options.xul": "" },
expected: {
isActive: true,
userDisabled: false,
appDisabled: false,
isCompatible: true,
optionsURL: null,
optionsType: null,
},
},
{
"install.rdf": {
id: "addon19@tests.mozilla.org",
version: "1.0",
bootstrap: true,
optionsType: "99",
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
name: "Test Addon 19",
},
expected: null,
},
{
"install.rdf": {
id: "addon20@tests.mozilla.org",
version: "1.0",
bootstrap: true,
optionsURL: "chrome://test/content/options.xul",
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
name: "Test Addon 20",
},
// Even with a defined optionsURL optionsType is null by default.
expected: {
isActive: true,
userDisabled: false,
appDisabled: false,
isCompatible: true,
optionsURL: "chrome://test/content/options.xul",
optionsType: null,
},
},
{
"install.rdf": {
id: "addon21@tests.mozilla.org",
version: "1.0",
bootstrap: true,
optionsType: "3",
optionsURL: "chrome://test/content/options.xul",
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
name: "Test Addon 21",
},
expected: {
isActive: true,
userDisabled: false,
appDisabled: false,
isCompatible: true,
optionsURL: "chrome://test/content/options.xul",
optionsType: AddonManager.OPTIONS_TYPE_TAB,
},
},
{
"install.rdf": {
id: "addon22@tests.mozilla.org",
version: "1.0",
bootstrap: true,
optionsType: "2",
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
name: "Test Addon 22",
},
// An obsolete optionsType means the add-on isn't registered.
expected: null,
},
{
"install.rdf": {
id: "addon23@tests.mozilla.org",
version: "1.0",
bootstrap: true,
optionsType: "2",
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
name: "Test Addon 23",
},
extraFiles: { "options.xul": "" },
// An obsolete optionsType means the add-on isn't registered.
expected: null,
},
{
"install.rdf": {
id: "addon24@tests.mozilla.org",
version: "1.0",
bootstrap: true,
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
name: "Test Addon 24",
},
extraFiles: { "options.xul": "" },
expected: {
optionsType: null,
optionsURL: null,
},
},
{
"install.rdf": {
id: "addon25@tests.mozilla.org",
version: "1.0",
bootstrap: true,
optionsType: "3",
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
name: "Test Addon 25",
},
expected: {
optionsType: null,
optionsURL: null,
},
},
{
"install.rdf": {
id: "addon26@tests.mozilla.org",
version: "1.0",
bootstrap: true,
optionsType: "4",
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
name: "Test Addon 26",
},
extraFiles: { "options.xul": "" },
expected: null,
},
// Tests compatibility based on target platforms.
// No targetPlatforms so should be compatible
{
"install.rdf": {
id: "tp-addon1@tests.mozilla.org",
version: "1.0",
bootstrap: true,
name: "Test 1",
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
},
expected: {
appDisabled: false,
isPlatformCompatible: true,
isActive: true,
},
},
// Matches the OS
{
"install.rdf": {
id: "tp-addon2@tests.mozilla.org",
version: "1.0",
bootstrap: true,
name: "Test 2",
targetPlatforms: ["XPCShell", "WINNT_x86", "XPCShell"],
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
},
expected: {
appDisabled: false,
isPlatformCompatible: true,
isActive: true,
},
},
// Matches the OS and ABI
{
"install.rdf": {
id: "tp-addon3@tests.mozilla.org",
version: "1.0",
bootstrap: true,
name: "Test 3",
targetPlatforms: ["WINNT", "XPCShell_noarch-spidermonkey"],
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
},
expected: {
appDisabled: false,
isPlatformCompatible: true,
isActive: true,
},
},
// Doesn't match
{
"install.rdf": {
id: "tp-addon4@tests.mozilla.org",
version: "1.0",
bootstrap: true,
name: "Test 4",
targetPlatforms: [
"WINNT_noarch-spidermonkey",
"Darwin",
"WINNT_noarch-spidermonkey",
],
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
},
expected: {
appDisabled: true,
isPlatformCompatible: false,
isActive: false,
},
},
// Matches the OS but since a different entry specifies ABI this doesn't match.
{
"install.rdf": {
id: "tp-addon5@tests.mozilla.org",
version: "1.0",
bootstrap: true,
name: "Test 5",
targetPlatforms: ["XPCShell", "XPCShell_foo"],
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
},
expected: {
appDisabled: true,
isPlatformCompatible: false,
isActive: false,
},
},
];
const IDS = ADDONS.map(a => a["install.rdf"].id);
add_task(async function setup() {
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
const profileDir = gProfD.clone();
profileDir.append("extensions");
for (let addon of ADDONS) {
await promiseWriteInstallRDFForExtension(
addon["install.rdf"],
profileDir,
undefined,
addon.extraFiles
);
}
});
add_task(async function test_values() {
await promiseStartupManager();
let addons = await getAddons(IDS);
for (let addon of ADDONS) {
let { id } = addon["install.rdf"];
checkAddon(id, addons.get(id), addon.expected);
}
await promiseShutdownManager();
});

View File

@@ -0,0 +1,140 @@
/* 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";
const ID = "bug397778@tests.mozilla.org";
const ADDON = createInstallRDF({
id: "bug397778@tests.mozilla.org",
version: "1.0",
name: "Fallback Name",
description: "Fallback Description",
bootstrap: true,
targetApplications: [
{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1",
},
],
localized: [
{
locale: ["fr"],
name: "fr Name",
description: "fr Description",
},
{
locale: ["de-DE"],
name: "Deutsches W\u00f6rterbuch",
},
{
locale: ["es-ES"],
name: "es-ES Name",
description: "es-ES Description",
},
{
locale: ["zh-TW"],
name: "zh-TW Name",
description: "zh-TW Description",
},
{
locale: ["zh-CN"],
name: "zh-CN Name",
description: "zh-CN Description",
},
{
locale: ["en-GB"],
name: "en-GB Name",
description: "en-GB Description",
},
{
locale: ["en"],
name: "en Name",
description: "en Description",
},
{
locale: ["en-CA"],
name: "en-CA Name",
description: "en-CA Description",
},
],
});
add_task(async function setup() {
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1");
Services.locale.requestedLocales = ["fr-FR"];
await promiseStartupManager();
await promiseInstallXPI(ADDON);
});
add_task(async function test_1() {
let addon = await AddonManager.getAddonByID(ID);
Assert.notEqual(addon, null);
Assert.equal(addon.name, "fr Name");
Assert.equal(addon.description, "fr Description");
await addon.disable();
await promiseRestartManager();
let newAddon = await AddonManager.getAddonByID(ID);
Assert.notEqual(newAddon, null);
Assert.equal(newAddon.name, "fr Name");
});
add_task(async function test_2() {
// Change locale. The more specific de-DE is the best match
await restartWithLocales(["de"]);
let addon = await AddonManager.getAddonByID(ID);
Assert.notEqual(addon, null);
Assert.equal(addon.name, "Deutsches W\u00f6rterbuch");
Assert.equal(addon.description, null);
});
add_task(async function test_3() {
// Change locale. Locale case should have no effect
await restartWithLocales(["DE-de"]);
let addon = await AddonManager.getAddonByID(ID);
Assert.notEqual(addon, null);
Assert.equal(addon.name, "Deutsches W\u00f6rterbuch");
Assert.equal(addon.description, null);
});
add_task(async function test_4() {
// Change locale. es-ES should closely match
await restartWithLocales(["es-AR"]);
let addon = await AddonManager.getAddonByID(ID);
Assert.notEqual(addon, null);
Assert.equal(addon.name, "es-ES Name");
Assert.equal(addon.description, "es-ES Description");
});
add_task(async function test_5() {
// Change locale. Either zh-CN or zh-TW could match
await restartWithLocales(["zh"]);
let addon = await AddonManager.getAddonByID(ID);
Assert.notEqual(addon, null);
ok(
addon.name == "zh-TW Name" || addon.name == "zh-CN Name",
`Add-on name mismatch: ${addon.name}`
);
});
add_task(async function test_6() {
// Unknown locale should try to match against en-US as well. Of en,en-GB
// en should match as being less specific
await restartWithLocales(["nl-NL"]);
let addon = await AddonManager.getAddonByID(ID);
Assert.notEqual(addon, null);
Assert.equal(addon.name, "en Name");
Assert.equal(addon.description, "en Description");
});

View File

@@ -0,0 +1,13 @@
[DEFAULT]
tags = addons
head = head_addons.js
support-files =
data/**
[test_bootstrap.js]
[test_bootstrap_const.js]
[test_bootstrap_globals.js]
[test_bootstrapped_chrome_manifest.js]
[test_invalid_install_rdf.js]
[test_manifest.js]
[test_manifest_locales.js]

View File

@@ -4,6 +4,8 @@
# 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/.
DIRS += ["extensions/common"]
DIST_SUBDIR = "browser"
export("DIST_SUBDIR")