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)
411 lines
13 KiB
JavaScript
411 lines
13 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
"use strict";
|
|
|
|
/**
|
|
* 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);
|