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:
@@ -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;
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
469
browser/components/extensions/parent/ext-legacy.js
Normal file
469
browser/components/extensions/parent/ext-legacy.js
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
43
browser/components/extensions/schemas/legacy.json
Normal file
43
browser/components/extensions/schemas/legacy.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
458
waterfox/browser/extensions/common/BootstrapLoader.jsm
Normal file
458
waterfox/browser/extensions/common/BootstrapLoader.jsm
Normal 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);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
363
waterfox/browser/extensions/common/ChromeManifest.jsm
Normal file
363
waterfox/browser/extensions/common/ChromeManifest.jsm
Normal 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();
|
||||
}
|
||||
}
|
||||
410
waterfox/browser/extensions/common/ExtensionSupport.jsm
Normal file
410
waterfox/browser/extensions/common/ExtensionSupport.jsm
Normal 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);
|
||||
143
waterfox/browser/extensions/common/IOUtils.jsm
Normal file
143
waterfox/browser/extensions/common/IOUtils.jsm
Normal 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");
|
||||
},
|
||||
};
|
||||
599
waterfox/browser/extensions/common/Overlays.jsm
Normal file
599
waterfox/browser/extensions/common/Overlays.jsm
Normal 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;
|
||||
}
|
||||
}
|
||||
1724
waterfox/browser/extensions/common/RDFDataSource.jsm
Normal file
1724
waterfox/browser/extensions/common/RDFDataSource.jsm
Normal file
File diff suppressed because it is too large
Load Diff
138
waterfox/browser/extensions/common/RDFManifestConverter.jsm
Normal file
138
waterfox/browser/extensions/common/RDFManifestConverter.jsm
Normal 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;
|
||||
}
|
||||
}
|
||||
132
waterfox/browser/extensions/common/iteratorUtils.jsm
Normal file
132
waterfox/browser/extensions/common/iteratorUtils.jsm
Normal 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
|
||||
);
|
||||
}
|
||||
21
waterfox/browser/extensions/common/moz.build
Normal file
21
waterfox/browser/extensions/common/moz.build
Normal 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"]
|
||||
@@ -0,0 +1,9 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
extends: "plugin:mozilla/xpcshell-test",
|
||||
|
||||
rules: {
|
||||
"func-names": "off",
|
||||
},
|
||||
};
|
||||
@@ -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]);
|
||||
}
|
||||
};
|
||||
1716
waterfox/browser/extensions/common/test/xpcshell/head_addons.js
Normal file
1716
waterfox/browser/extensions/common/test/xpcshell/head_addons.js
Normal file
File diff suppressed because it is too large
Load Diff
1174
waterfox/browser/extensions/common/test/xpcshell/test_bootstrap.js
Normal file
1174
waterfox/browser/extensions/common/test/xpcshell/test_bootstrap.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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]
|
||||
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user