diff --git a/Makefile b/Makefile
index 9e3ca29..1e193cb 100644
--- a/Makefile
+++ b/Makefile
@@ -2,6 +2,8 @@
PREFIX := /usr
DATADIR := $(PREFIX)/share
+# the profile dir is copied and left unmodified by web-ext
+PROFILE := /home/xavier/.thunderbird/6ujxy9ck.default-release-2/
# UUID below is for Thunderbird
TB_EXT_DIR := $(DATADIR)/mozilla/extensions/{3550f703-e582-4d05-9a08-453d09bdfdc6}
@@ -17,3 +19,8 @@ clean:
install: all
install -d $(DESTDIR)$(TB_EXT_DIR)
install --mode=644 *.xpi $(DESTDIR)$(TB_EXT_DIR)
+
+run:
+ web-ext run --verbose --firefox=/usr/bin/thunderbird --firefox-profile=$(PROFILE) -s xul-ext/
+lint:
+ web-ext lint -s xul-ext/
\ No newline at end of file
diff --git a/xul-ext/api/windowListener/implementation.js b/xul-ext/api/windowListener/implementation.js
new file mode 100644
index 0000000..7baa45c
--- /dev/null
+++ b/xul-ext/api/windowListener/implementation.js
@@ -0,0 +1,1074 @@
+/*
+ * This file is provided by the addon-developer-support repository at
+ * https://github.com/thundernest/addon-developer-support
+ *
+ * Version: 1.44
+ * - Add notifyExperiment() function to send data to privileged scripts inside
+ * an Experiment. The privileged script must include notifyTools.js from the
+ * addon-developer-support repository.
+ *
+ * // In a WebExtension background script:
+ * // Note: Restrictions of the structured clone algorythm apply to the send data.
+ * messenger.WindowListener.notifyExperiment({data: "voilá"});
+ *
+ * // In a privileged script inside an Experiment:
+ * let Listerner1 = notifyTools.registerListener((rv) => console.log("listener #1", rv));
+ * let Listerner2 = notifyTools.registerListener((rv) => console.log("listener #2", rv));
+ * let Listerner3 = notifyTools.registerListener((rv) => console.log("listener #3", rv));
+ * notifyTools.removeListener(Listerner2);
+ *
+ * - Add onNotifyBackground event, which can be registered in the background page,
+ * to receive data from privileged scripts inside an Experiment. The privileged
+ * script must include notifyTools.js from the addon-developer-support repository.
+ *
+ * // In a WebExtension background script:
+ * messenger.WindowListener.onNotifyBackground.addListener(async (info) => {
+ * switch (info.command) {
+ * case "doSomething":
+ * let rv = await doSomething(info.data);
+ * return {
+ * result: rv,
+ * data: [1,2,3]
+ * };
+ * break;
+ * }
+ * });
+ *
+ * // In a privileged script inside an Experiment:
+ * let rv = await notifyTools.notifyBackground({command: "doSomething", data: [1,2,3]});
+ * // rv will be whatever has been returned by the background script.
+ * // Note: Restrictions of the structured clone algorythm apply to
+ * // the send and recieved data.
+ *
+ * Version: 1.39
+ * - fix for 68
+ *
+ * Version: 1.36
+ * - fix for beta 87
+ *
+ * Version: 1.35
+ * - add support for options button/menu in add-on manager and fix 68 double menu entry
+ *
+ * Version: 1.34
+ * - fix error in unload
+ *
+ * Version: 1.33
+ * - fix for e10s
+ *
+ * Version: 1.30
+ * - replace setCharPref by setStringPref to cope with URTF-8 encoding
+ *
+ * Version: 1.29
+ * - open options window centered
+ *
+ * Version: 1.28
+ * - do not crash on missing icon
+ *
+ * Version: 1.27
+ * - add openOptionsDialog()
+ *
+ * Version: 1.26
+ * - pass WL object to legacy preference window
+ *
+ * Version: 1.25
+ * - adding waitForMasterPassword
+ *
+ * Version: 1.24
+ * - automatically localize i18n locale strings in injectElements()
+ *
+ * Version: 1.22
+ * - to reduce confusions, only check built-in URLs as add-on URLs cannot
+ * be resolved if a temp installed add-on has bin zipped
+ *
+ * Version: 1.21
+ * - print debug messages only if add-ons are installed temporarily from
+ * the add-on debug page
+ * - add checks to registered windows and scripts, if they actually exists
+ *
+ * Version: 1.20
+ * - fix long delay before customize window opens
+ * - fix non working removal of palette items
+ *
+ * Version: 1.19
+ * - add support for ToolbarPalette
+ *
+ * Version: 1.18
+ * - execute shutdown script also during global app shutdown (fixed)
+ *
+ * Version: 1.17
+ * - execute shutdown script also during global app shutdown
+ *
+ * Version: 1.16
+ * - support for persist
+ *
+ * Version: 1.15
+ * - make (undocumented) startup() async
+ *
+ * Version: 1.14
+ * - support resource urls
+ *
+ * Version: 1.12
+ * - no longer allow to enforce custom "namespace"
+ * - no longer call it namespace but uniqueRandomID / scopeName
+ * - expose special objects as the global WL object
+ * - autoremove injected elements after onUnload has ben executed
+ *
+ * Version: 1.9
+ * - automatically remove all entries added by injectElements
+ *
+ * Version: 1.8
+ * - add injectElements
+ *
+ * Version: 1.7
+ * - add injectCSS
+ * - add optional enforced namespace
+ *
+ * Version: 1.6
+ * - added mutation observer to be able to inject into browser elements
+ * - use larger icons as fallback
+ *
+ * Author: John Bieling (john@thunderbird.net)
+ *
+ * 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/.
+ */
+
+
+// Import some things we need.
+var { ExtensionCommon } = ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
+var { ExtensionSupport } = ChromeUtils.import("resource:///modules/ExtensionSupport.jsm");
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+var WindowListener = class extends ExtensionCommon.ExtensionAPI {
+ log(msg) {
+ if (this.debug) console.log("WindowListener API: " + msg);
+ }
+
+ getThunderbirdMajorVersion() {
+ return parseInt(Services.appinfo.version.split(".").shift());
+ }
+
+ getCards(e) {
+ // This gets triggered by real events but also manually by providing the outer window.
+ // The event is attached to the outer browser, get the inner one.
+ let doc;
+
+ // 78,86, and 87+ need special handholding. *Yeah*.
+ if (this.getThunderbirdMajorVersion() < 86) {
+ let ownerDoc = e.document || e.target.ownerDocument;
+ doc = ownerDoc.getElementById("html-view-browser").contentDocument;
+ } else if (this.getThunderbirdMajorVersion() < 87) {
+ let ownerDoc = e.document || e.target;
+ doc = ownerDoc.getElementById("html-view-browser").contentDocument;
+ } else {
+ doc = e.document || e.target;
+ }
+ return doc.querySelectorAll("addon-card");
+ }
+
+ // Add pref entry to 68
+ add68PrefsEntry(event) {
+ let id = this.menu_addonPrefs_id + "_" + this.uniqueRandomID;
+
+ // Get the best size of the icon (16px or bigger)
+ let iconSizes = this.extension.manifest.icons
+ ? Object.keys(this.extension.manifest.icons)
+ : [];
+ iconSizes.sort((a,b)=>a-b);
+ let bestSize = iconSizes.filter(e => parseInt(e) >= 16).shift();
+ let icon = bestSize ? this.extension.manifest.icons[bestSize] : "";
+
+ let name = this.extension.manifest.name;
+ let entry = icon
+ ? event.target.ownerGlobal.MozXULElement.parseXULToFragment(
+ ``)
+ : event.target.ownerGlobal.MozXULElement.parseXULToFragment(
+ ``);
+
+ event.target.appendChild(entry);
+ let noPrefsElem = event.target.querySelector('[disabled="true"]');
+ // using collapse could be undone by core, so we use display none
+ // noPrefsElem.setAttribute("collapsed", "true");
+ noPrefsElem.style.display = "none";
+ event.target.ownerGlobal.document.getElementById(id).addEventListener("command", this);
+ }
+
+ // Event handler for the addon manager, to update the state of the options button.
+ handleEvent(e) {
+ switch (e.type) {
+ // 68 add-on options menu showing
+ case "popupshowing": {
+ this.add68PrefsEntry(e);
+ }
+ break;
+
+ // 78/88 add-on options menu/button click
+ case "click": {
+ e.preventDefault();
+ e.stopPropagation();
+ let WL = {}
+ WL.extension = this.extension;
+ WL.messenger = this.getMessenger(this.context);
+ let w = Services.wm.getMostRecentWindow("mail:3pane");
+ w.openDialog(this.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", WL);
+ }
+ break;
+
+ // 68 add-on options menu command
+ case "command": {
+ let WL = {}
+ WL.extension = this.extension;
+ WL.messenger = this.getMessenger(this.context);
+ e.target.ownerGlobal.openDialog(this.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", WL);
+ }
+ break;
+
+ // update, ViewChanged and manual call for add-on manager options overlay
+ default: {
+ let cards = this.getCards(e);
+ for (let card of cards) {
+ // Setup either the options entry in the menu or the button
+ //window.document.getElementById(id).addEventListener("command", function() {window.openDialog(self.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", WL)});
+ if (card.addon.id == this.extension.id) {
+ if (this.getThunderbirdMajorVersion() < 88) {
+ // Options menu in 78-87
+ let addonOptionsLegacyEntry = card.querySelector(".extension-options-legacy");
+ if (card.addon.isActive && !addonOptionsLegacyEntry) {
+ let addonOptionsEntry = card.querySelector("addon-options panel-list panel-item[action='preferences']");
+ addonOptionsLegacyEntry = card.ownerDocument.createElement("panel-item");
+ addonOptionsLegacyEntry.setAttribute("data-l10n-id", "preferences-addon-button");
+ addonOptionsLegacyEntry.classList.add("extension-options-legacy");
+ addonOptionsEntry.parentNode.insertBefore(
+ addonOptionsLegacyEntry,
+ addonOptionsEntry
+ );
+ card.querySelector(".extension-options-legacy").addEventListener("click", this);
+ } else if (!card.addon.isActive && addonOptionsLegacyEntry) {
+ addonOptionsLegacyEntry.remove();
+ }
+ } else {
+ // Add-on button in 88
+ let addonOptionsButton = card.querySelector(".extension-options-button2");
+ if (card.addon.isActive && !addonOptionsButton) {
+ addonOptionsButton = card.ownerDocument.createElement("button");
+ addonOptionsButton.classList.add("extension-options-button2");
+ addonOptionsButton.style["min-width"] = "auto";
+ addonOptionsButton.style["min-height"] = "auto";
+ addonOptionsButton.style["width"] = "24px";
+ addonOptionsButton.style["height"] = "24px";
+ addonOptionsButton.style["margin"] = "0";
+ addonOptionsButton.style["margin-inline-start"] = "8px";
+ addonOptionsButton.style["-moz-context-properties"] = "fill";
+ addonOptionsButton.style["fill"] = "currentColor";
+ addonOptionsButton.style["background-image"] = "url('chrome://messenger/skin/icons/developer.svg')";
+ addonOptionsButton.style["background-repeat"] = "no-repeat";
+ addonOptionsButton.style["background-position"] = "center center";
+ addonOptionsButton.style["padding"] = "1px";
+ addonOptionsButton.style["display"] = "flex";
+ addonOptionsButton.style["justify-content"] = "flex-end";
+ card.optionsButton.parentNode.insertBefore(
+ addonOptionsButton,
+ card.optionsButton
+ );
+ card.querySelector(".extension-options-button2").addEventListener("click", this);
+ } else if (!card.addon.isActive && addonOptionsButton) {
+ addonOptionsButton.remove();
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+// Some tab/add-on-manager related functions
+ getTabMail(window) {
+ return window.document.getElementById("tabmail");
+ }
+
+ // returns the outer browser, not the nested browser of the add-on manager
+ // events must be attached to the outer browser
+ getAddonManagerFromTab(tab) {
+ let win = tab.browser.contentWindow;
+ if (win && win.location.href == "about:addons") {
+ return win;
+ }
+ }
+
+ getAddonManagerFromWindow(window) {
+ let tabMail = this.getTabMail(window);
+ for (let tab of tabMail.tabInfo) {
+ let win = this.getAddonManagerFromTab(tab)
+ if (win) {
+ return win;
+ }
+ }
+ }
+
+ setupAddonManager(managerWindow, paint = true) {
+ if (!managerWindow) {
+ return;
+ }
+ if (managerWindow
+ && managerWindow[this.uniqueRandomID]
+ && managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners
+ ) {
+ return;
+ }
+ managerWindow.document.addEventListener("ViewChanged", this);
+ managerWindow.document.addEventListener("update", this);
+ managerWindow[this.uniqueRandomID] = {};
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners = true;
+ if (paint) {
+ this.handleEvent(managerWindow);
+ }
+ }
+
+
+ getMessenger(context) {
+ let apis = [
+ "storage",
+ "runtime",
+ "extension",
+ "i18n",
+ ];
+
+ function getStorage() {
+ let localstorage = null;
+ try {
+ localstorage = context.apiCan.findAPIPath("storage");
+ localstorage.local.get = (...args) =>
+ localstorage.local.callMethodInParentProcess("get", args);
+ localstorage.local.set = (...args) =>
+ localstorage.local.callMethodInParentProcess("set", args);
+ localstorage.local.remove = (...args) =>
+ localstorage.local.callMethodInParentProcess("remove", args);
+ localstorage.local.clear = (...args) =>
+ localstorage.local.callMethodInParentProcess("clear", args);
+ } catch (e) {
+ console.info("Storage permission is missing");
+ }
+ return localstorage;
+ }
+
+ let messenger = {};
+ for (let api of apis) {
+ switch (api) {
+ case "storage":
+ XPCOMUtils.defineLazyGetter(messenger, "storage", () =>
+ getStorage()
+ );
+ break;
+
+ default:
+ XPCOMUtils.defineLazyGetter(messenger, api, () =>
+ context.apiCan.findAPIPath(api)
+ );
+ }
+ }
+ return messenger;
+ }
+
+ error(msg) {
+ if (this.debug) console.error("WindowListener API: " + msg);
+ }
+
+ // async sleep function using Promise
+ async sleep(delay) {
+ let timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
+ return new Promise(function(resolve, reject) {
+ let event = {
+ notify: function(timer) {
+ resolve();
+ }
+ }
+ timer.initWithCallback(event, delay, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
+ });
+ }
+
+ getAPI(context) {
+ // track if this is the background/main context
+ this.isBackgroundContext = (context.viewType == "background");
+ this.context = context;
+
+ this.uniqueRandomID = "AddOnNS" + context.extension.instanceId;
+ this.menu_addonPrefs_id = "addonPrefs";
+
+ this.registeredWindows = {};
+ this.pathToStartupScript = null;
+ this.pathToShutdownScript = null;
+ this.pathToOptionsPage = null;
+ this.chromeHandle = null;
+ this.chromeData = null;
+ this.resourceData = null;
+ this.openWindows = [];
+ this.debug = context.extension.addonData.temporarilyInstalled;
+
+ const aomStartup = Cc["@mozilla.org/addons/addon-manager-startup;1"].getService(Ci.amIAddonManagerStartup);
+ const resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsISubstitutingProtocolHandler);
+
+ let self = this;
+
+ // TabMonitor to detect opening of tabs, to setup the options button in the add-on manager.
+ this.tabMonitor = {
+ onTabTitleChanged(aTab) {},
+ onTabClosing(aTab) {},
+ onTabPersist(aTab) {},
+ onTabRestored(aTab) {},
+ onTabSwitched(aNewTab, aOldTab) {},
+ async onTabOpened(aTab) {
+ if (!aTab.pageLoaded) {
+ // await a location change if browser is not loaded yet
+ await new Promise(resolve => {
+ let reporterListener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+ onStateChange() {},
+ onProgressChange() {},
+ onLocationChange(
+ /* in nsIWebProgress*/ aWebProgress,
+ /* in nsIRequest*/ aRequest,
+ /* in nsIURI*/ aLocation
+ ) {
+ aTab.browser.removeProgressListener(reporterListener);
+ resolve();
+ },
+ onStatusChange() {},
+ onSecurityChange() {},
+ onContentBlockingEvent() {}
+ }
+ aTab.browser.addProgressListener(reporterListener);
+ });
+ }
+ // Setup the ViewChange event listener in the outer browser of the add-on,
+ // but do not actually add the button/menu, as the inner browser is not yet ready,
+ // let the ViewChange event do it
+ self.setupAddonManager(self.getAddonManagerFromTab(aTab), false);
+ },
+ };
+
+ this.onNotifyBackgroundObserver = {
+ observe: async function (aSubject, aTopic, aData) {
+ if (self.observerTracker && aData == self.extension.id) {
+ let payload = aSubject.wrappedJSObject;
+ // This is called from the WL observer.js and therefore it should have a resolve
+ // payload, but better check.
+ if (payload.resolve) {
+ let rv = await self.observerTracker(payload.data);
+ payload.resolve(rv);
+ } else {
+ self.observerTracker(payload.data);
+ }
+ }
+ }
+ }
+ return {
+ WindowListener: {
+
+ notifyExperiment(data) {
+ Services.obs.notifyObservers(
+ // Stuff data in an array so simple strings can be used as payload
+ // without the observerService complaining.
+ [data],
+ "WindowListenerNotifyExperimentObserver",
+ self.extension.id
+ );
+ },
+
+ onNotifyBackground: new ExtensionCommon.EventManager({
+ context,
+ name: "WindowListener.onNotifyBackground",
+ register: fire => {
+ Services.obs.addObserver(self.onNotifyBackgroundObserver, "WindowListenerNotifyBackgroundObserver", false);
+ self.observerTracker = fire.sync;
+ return () => {
+ Services.obs.removeObserver(self.onNotifyBackgroundObserver, "WindowListenerNotifyBackgroundObserver", false);
+ self.observerTracker = null;
+ };
+ },
+ }).api(),
+
+ async waitForMasterPassword() {
+ // Wait until master password has been entered (if needed)
+ while (!Services.logins.isLoggedIn) {
+ self.log("Waiting for master password.");
+ await self.sleep(1000);
+ }
+ self.log("Master password has been entered.");
+ },
+
+ aDocumentExistsAt(uriString) {
+ self.log("Checking if document at <" + uriString + "> used in registration actually exists.");
+ try {
+ let uriObject = Services.io.newURI(uriString);
+ let content = Cu.readUTF8URI(uriObject);
+ } catch (e) {
+ Components.utils.reportError(e);
+ return false;
+ }
+ return true;
+ },
+
+ registerOptionsPage(optionsUrl) {
+ self.pathToOptionsPage = optionsUrl.startsWith("chrome://")
+ ? optionsUrl
+ : context.extension.rootURI.resolve(optionsUrl);
+ },
+
+ registerDefaultPrefs(defaultUrl) {
+ let url = context.extension.rootURI.resolve(defaultUrl);
+
+ let prefsObj = {};
+ prefsObj.Services = ChromeUtils.import("resource://gre/modules/Services.jsm").Services;
+ prefsObj.pref = function(aName, aDefault) {
+ let defaults = Services.prefs.getDefaultBranch("");
+ switch (typeof aDefault) {
+ case "string":
+ return defaults.setStringPref(aName, aDefault);
+
+ case "number":
+ return defaults.setIntPref(aName, aDefault);
+
+ case "boolean":
+ return defaults.setBoolPref(aName, aDefault);
+
+ default:
+ throw new Error("Preference <" + aName + "> has an unsupported type <" + typeof aDefault + ">. Allowed are string, number and boolean.");
+ }
+ }
+ Services.scriptloader.loadSubScript(url, prefsObj, "UTF-8");
+ },
+
+ registerChromeUrl(data) {
+ if (!self.isBackgroundContext)
+ throw new Error("The WindowListener API may only be called from the background page.");
+
+ let chromeData = [];
+ let resourceData = [];
+ for (let entry of data) {
+ if (entry[0] == "resource") resourceData.push(entry);
+ else chromeData.push(entry)
+ }
+
+ if (chromeData.length > 0) {
+ const manifestURI = Services.io.newURI(
+ "manifest.json",
+ null,
+ context.extension.rootURI
+ );
+ self.chromeHandle = aomStartup.registerChrome(manifestURI, chromeData);
+ }
+
+ for (let res of resourceData) {
+ // [ "resource", "shortname" , "path" ]
+ let uri = Services.io.newURI(
+ res[2],
+ null,
+ context.extension.rootURI
+ );
+ resProto.setSubstitutionWithFlags(
+ res[1],
+ uri,
+ resProto.ALLOW_CONTENT_ACCESS
+ );
+ }
+
+ self.chromeData = chromeData;
+ self.resourceData = resourceData;
+ },
+
+ registerWindow(windowHref, jsFile) {
+ if (!self.isBackgroundContext)
+ throw new Error("The WindowListener API may only be called from the background page.");
+
+ if (self.debug && !this.aDocumentExistsAt(windowHref)) {
+ self.error("Attempt to register an injector script for non-existent window: " + windowHref);
+ return;
+ }
+
+ if (!self.registeredWindows.hasOwnProperty(windowHref)) {
+ // path to JS file can either be chrome:// URL or a relative URL
+ let path = jsFile.startsWith("chrome://")
+ ? jsFile
+ : context.extension.rootURI.resolve(jsFile)
+
+ self.registeredWindows[windowHref] = path;
+ } else {
+ self.error("Window <" +windowHref + "> has already been registered");
+ }
+ },
+
+ registerStartupScript(aPath) {
+ if (!self.isBackgroundContext)
+ throw new Error("The WindowListener API may only be called from the background page.");
+
+ self.pathToStartupScript = aPath.startsWith("chrome://")
+ ? aPath
+ : context.extension.rootURI.resolve(aPath);
+ },
+
+ registerShutdownScript(aPath) {
+ if (!self.isBackgroundContext)
+ throw new Error("The WindowListener API may only be called from the background page.");
+
+ self.pathToShutdownScript = aPath.startsWith("chrome://")
+ ? aPath
+ : context.extension.rootURI.resolve(aPath);
+ },
+
+ openOptionsDialog(windowId) {
+ let window = context.extension.windowManager.get(windowId, context).window
+ let WL = {}
+ WL.extension = self.extension;
+ WL.messenger = self.getMessenger(self.context);
+ window.openDialog(self.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", WL);
+ },
+
+ async startListening() {
+ if (!self.isBackgroundContext)
+ throw new Error("The WindowListener API may only be called from the background page.");
+
+ // load the registered startup script, if one has been registered
+ // (mail3:pane may not have been fully loaded yet)
+ if (self.pathToStartupScript) {
+ let startupJS = {};
+ startupJS.WL = {}
+ startupJS.WL.extension = self.extension;
+ startupJS.WL.messenger = self.getMessenger(self.context);
+ try {
+ if (self.pathToStartupScript) {
+ Services.scriptloader.loadSubScript(self.pathToStartupScript, startupJS, "UTF-8");
+ // delay startup until startup has been finished
+ self.log("Waiting for async startup() in <" + self.pathToStartupScript + "> to finish.");
+ if (startupJS.startup) {
+ await startupJS.startup();
+ self.log("startup() in <" + self.pathToStartupScript + "> finished");
+ } else {
+ self.log("No startup() in <" + self.pathToStartupScript + "> found.");
+ }
+ }
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+ }
+
+ let urls = Object.keys(self.registeredWindows);
+ if (urls.length > 0) {
+ // Before registering the window listener, check which windows are already open
+ self.openWindows = [];
+ for (let window of Services.wm.getEnumerator(null)) {
+ self.openWindows.push(window);
+ }
+
+ // Register window listener for all pre-registered windows
+ ExtensionSupport.registerWindowListener("injectListener_" + self.uniqueRandomID, {
+ // React on all windows and manually reduce to the registered
+ // windows, so we can do special actions when the main
+ // messenger window is opened.
+ //chromeURLs: Object.keys(self.registeredWindows),
+ async onLoadWindow(window) {
+ // Create add-on scope
+ window[self.uniqueRandomID] = {};
+
+ // Special action #1: If this is the main messenger window
+ if (window.location.href == "chrome://messenger/content/messenger.xul" ||
+ window.location.href == "chrome://messenger/content/messenger.xhtml") {
+
+ if (self.pathToOptionsPage) {
+ if (self.getThunderbirdMajorVersion() < 78) {
+ let element_addonPrefs = window.document.getElementById(self.menu_addonPrefs_id);
+ element_addonPrefs.addEventListener("popupshowing", self);
+ } else {
+ // Setup the options button/menu in the add-on manager, if it is already open.
+ self.setupAddonManager(self.getAddonManagerFromWindow(window));
+ // Add a tabmonitor, to be able to setup the options button/menu in the add-on manager.
+ self.getTabMail(window).registerTabMonitor(self.tabMonitor);
+ window[self.uniqueRandomID].hasTabMonitor = true;
+ }
+ }
+ }
+
+ // Special action #2: If this page contains browser elements
+ let browserElements = window.document.getElementsByTagName("browser");
+ if (browserElements.length > 0) {
+ //register a MutationObserver
+ window[self.uniqueRandomID]._mObserver = new window.MutationObserver(function(mutations) {
+ mutations.forEach(async function(mutation) {
+ if (mutation.attributeName == "src" && self.registeredWindows.hasOwnProperty(mutation.target.getAttribute("src"))) {
+ // When the MutationObserver callsback, the window is still showing "about:black" and it is going
+ // to unload and then load the new page. Any eventListener attached to the window will be removed
+ // so we cannot listen for the load event. We have to poll manually to learn when loading has finished.
+ // On my system it takes 70ms.
+ let loaded = false;
+ for (let i=0; i < 100 && !loaded; i++) {
+ await self.sleep(100);
+ let targetWindow = mutation.target.contentWindow.wrappedJSObject;
+ if (targetWindow && targetWindow.location.href == mutation.target.getAttribute("src") && targetWindow.document.readyState == "complete") {
+ loaded = true;
+ break;
+ }
+ }
+ if (loaded) {
+ let targetWindow = mutation.target.contentWindow.wrappedJSObject;
+ // Create add-on scope
+ targetWindow[self.uniqueRandomID] = {};
+ // Inject with isAddonActivation = false
+ self._loadIntoWindow(targetWindow, false);
+ }
+ }
+ });
+ });
+
+ for (let element of browserElements) {
+ if (self.registeredWindows.hasOwnProperty(element.getAttribute("src"))) {
+ let targetWindow = element.contentWindow.wrappedJSObject;
+ // Create add-on scope
+ targetWindow[self.uniqueRandomID] = {};
+ // Inject with isAddonActivation = true
+ self._loadIntoWindow(targetWindow, true);
+ } else {
+ // Window/Browser is not yet fully loaded, postpone injection via MutationObserver
+ window[self.uniqueRandomID]._mObserver.observe(element, { attributes: true, childList: false, characterData: false });
+ }
+ }
+ }
+
+ // Load JS into window
+ self._loadIntoWindow(window, self.openWindows.includes(window));
+ },
+
+ onUnloadWindow(window) {
+ // Remove JS from window, window is being closed, addon is not shut down
+ self._unloadFromWindow(window, false);
+ }
+ });
+ } else {
+ self.error("Failed to start listening, no windows registered");
+ }
+ },
+
+ }
+ };
+ }
+
+ _loadIntoWindow(window, isAddonActivation) {
+ if (window.hasOwnProperty(this.uniqueRandomID) && this.registeredWindows.hasOwnProperty(window.location.href)) {
+ try {
+ let uniqueRandomID = this.uniqueRandomID;
+ let extension = this.extension;
+
+ // Add reference to window to add-on scope
+ window[this.uniqueRandomID].window = window;
+ window[this.uniqueRandomID].document = window.document;
+
+ // Keep track of toolbarpalettes we are injecting into
+ window[this.uniqueRandomID]._toolbarpalettes = {};
+
+ //Create WLDATA object
+ window[this.uniqueRandomID].WL = {};
+ window[this.uniqueRandomID].WL.scopeName = this.uniqueRandomID;
+
+ // Add helper function to inject CSS to WLDATA object
+ window[this.uniqueRandomID].WL.injectCSS = function (cssFile) {
+ let element;
+ let v = parseInt(Services.appinfo.version.split(".").shift());
+
+ // using createElementNS in TB78 delays the insert process and hides any security violation errors
+ if (v > 68) {
+ element = window.document.createElement("link");
+ } else {
+ let ns = window.document.documentElement.lookupNamespaceURI("html");
+ element = window.document.createElementNS(ns, "link");
+ }
+
+ element.setAttribute("wlapi_autoinjected", uniqueRandomID);
+ element.setAttribute("rel", "stylesheet");
+ element.setAttribute("href", cssFile);
+ return window.document.documentElement.appendChild(element);
+ }
+
+ // Add helper function to inject XUL to WLDATA object
+ window[this.uniqueRandomID].WL.injectElements = function (xulString, dtdFiles = [], debug = false) {
+ let toolbarsToResolve = [];
+
+ function checkElements(stringOfIDs) {
+ let arrayOfIDs = stringOfIDs.split(",").map(e => e.trim());
+ for (let id of arrayOfIDs) {
+ let element = window.document.getElementById(id);
+ if (element) {
+ return element;
+ }
+ }
+ return null;
+ }
+
+ function localize(entity) {
+ let msg = entity.slice("__MSG_".length,-2);
+ return extension.localeData.localizeMessage(msg)
+ }
+
+ function injectChildren(elements, container) {
+ if (debug) console.log(elements);
+
+ for (let i = 0; i < elements.length; i++) {
+ // take care of persists
+ const uri = window.document.documentURI;
+ for (const persistentNode of elements[i].querySelectorAll("[persist]")) {
+ for (const persistentAttribute of persistentNode.getAttribute("persist").trim().split(" ")) {
+ if (Services.xulStore.hasValue(uri, persistentNode.id, persistentAttribute)) {
+ persistentNode.setAttribute(
+ persistentAttribute,
+ Services.xulStore.getValue(uri, persistentNode.id, persistentAttribute)
+ );
+ }
+ }
+ }
+
+ if (elements[i].hasAttribute("insertafter") && checkElements(elements[i].getAttribute("insertafter"))) {
+ let insertAfterElement = checkElements(elements[i].getAttribute("insertafter"));
+
+ if (debug) console.log(elements[i].tagName + "#" + elements[i].id + ": insertafter " + insertAfterElement.id);
+ if (debug && elements[i].id && window.document.getElementById(elements[i].id)) {
+ console.error("The id <" + elements[i].id + "> of the injected element already exists in the document!");
+ }
+ elements[i].setAttribute("wlapi_autoinjected", uniqueRandomID);
+ insertAfterElement.parentNode.insertBefore(elements[i], insertAfterElement.nextSibling);
+
+ } else if (elements[i].hasAttribute("insertbefore") && checkElements(elements[i].getAttribute("insertbefore"))) {
+ let insertBeforeElement = checkElements(elements[i].getAttribute("insertbefore"));
+
+ if (debug) console.log(elements[i].tagName + "#" + elements[i].id + ": insertbefore " + insertBeforeElement.id);
+ if (debug && elements[i].id && window.document.getElementById(elements[i].id)) {
+ console.error("The id <" + elements[i].id + "> of the injected element already exists in the document!");
+ }
+ elements[i].setAttribute("wlapi_autoinjected", uniqueRandomID);
+ insertBeforeElement.parentNode.insertBefore(elements[i], insertBeforeElement);
+
+ } else if (elements[i].id && window.document.getElementById(elements[i].id)) {
+ // existing container match, dive into recursivly
+ if (debug) console.log(elements[i].tagName + "#" + elements[i].id + " is an existing container, injecting into " + elements[i].id);
+ injectChildren(Array.from(elements[i].children), window.document.getElementById(elements[i].id));
+
+ } else if (elements[i].localName === "toolbarpalette") {
+ // These vanish from the document but still exist via the palette property
+ if (debug) console.log(elements[i].id + " is a toolbarpalette");
+ let boxes = [...window.document.getElementsByTagName("toolbox")];
+ let box = boxes.find(box => box.palette && box.palette.id === elements[i].id);
+ let palette = box ? box.palette : null;
+
+ if (!palette) {
+ if (debug) console.log(`The palette for ${elements[i].id} could not be found, deferring to later`);
+ continue;
+ }
+
+ if (debug) console.log(`The toolbox for ${elements[i].id} is ${box.id}`);
+
+ toolbarsToResolve.push(...box.querySelectorAll("toolbar"));
+ toolbarsToResolve.push(...window.document.querySelectorAll(`toolbar[toolboxid="${box.id}"]`));
+ for (let child of elements[i].children) {
+ child.setAttribute("wlapi_autoinjected", uniqueRandomID);
+ }
+ window[uniqueRandomID]._toolbarpalettes[palette.id] = palette;
+ injectChildren(Array.from(elements[i].children), palette);
+ } else {
+ // append element to the current container
+ if (debug) console.log(elements[i].tagName + "#" + elements[i].id + ": append to " + container.id);
+ elements[i].setAttribute("wlapi_autoinjected", uniqueRandomID);
+ container.appendChild(elements[i]);
+ }
+ }
+ }
+
+ if (debug) console.log ("Injecting into root document:");
+ let localizedXulString = xulString.replace(/__MSG_(.*?)__/g, localize);
+ injectChildren(Array.from(window.MozXULElement.parseXULToFragment(localizedXulString, dtdFiles).children), window.document.documentElement);
+
+ for (let bar of toolbarsToResolve) {
+ let currentset = Services.xulStore.getValue(
+ window.location,
+ bar.id,
+ "currentset"
+ );
+ if (currentset) {
+ bar.currentSet = currentset;
+ } else if (bar.getAttribute("defaultset")) {
+ bar.currentSet = bar.getAttribute("defaultset");
+ }
+ }
+ }
+
+ // Add extension object to WLDATA object
+ window[this.uniqueRandomID].WL.extension = this.extension;
+ // Add messenger object to WLDATA object
+ window[this.uniqueRandomID].WL.messenger = this.getMessenger(this.context);
+ // Load script into add-on scope
+ Services.scriptloader.loadSubScript(this.registeredWindows[window.location.href], window[this.uniqueRandomID], "UTF-8");
+ window[this.uniqueRandomID].onLoad(isAddonActivation);
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+ }
+ }
+
+ _unloadFromWindow(window, isAddonDeactivation) {
+ // unload any contained browser elements
+ if (window.hasOwnProperty(this.uniqueRandomID) && window[this.uniqueRandomID].hasOwnProperty("_mObserver")) {
+ window[this.uniqueRandomID]._mObserver.disconnect();
+ let browserElements = window.document.getElementsByTagName("browser");
+ for (let element of browserElements) {
+ if (element.contentWindow) {
+ this._unloadFromWindow(element.contentWindow.wrappedJSObject, isAddonDeactivation);
+ }
+ }
+ }
+
+ if (window.hasOwnProperty(this.uniqueRandomID) && this.registeredWindows.hasOwnProperty(window.location.href)) {
+ // Remove this window from the list of open windows
+ this.openWindows = this.openWindows.filter(e => (e != window));
+
+ if (window[this.uniqueRandomID].onUnload) {
+ try {
+ // Call onUnload()
+ window[this.uniqueRandomID].onUnload(isAddonDeactivation);
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+ }
+
+ // Remove all auto injected objects
+ let elements = Array.from(window.document.querySelectorAll('[wlapi_autoinjected="' + this.uniqueRandomID + '"]'));
+ for (let element of elements) {
+ element.remove();
+ }
+
+ // Remove all autoinjected toolbarpalette items
+ for (const palette of Object.values(window[this.uniqueRandomID]._toolbarpalettes)) {
+ let elements = Array.from(palette.querySelectorAll('[wlapi_autoinjected="' + this.uniqueRandomID + '"]'));
+ for (let element of elements) {
+ element.remove();
+ }
+ }
+
+ }
+
+ // Remove add-on scope, if it exists
+ if (window.hasOwnProperty(this.uniqueRandomID)) {
+ delete window[this.uniqueRandomID];
+ }
+ }
+
+ onShutdown(isAppShutdown) {
+ // Unload from all still open windows
+ let urls = Object.keys(this.registeredWindows);
+ if (urls.length > 0) {
+ for (let window of Services.wm.getEnumerator(null)) {
+
+ //remove our entry in the add-on options menu
+ if (this.pathToOptionsPage && (
+ window.location.href == "chrome://messenger/content/messenger.xul" ||
+ window.location.href == "chrome://messenger/content/messenger.xhtml")
+ ) {
+ if (this.getThunderbirdMajorVersion() < 78) {
+ let element_addonPrefs = window.document.getElementById(this.menu_addonPrefs_id);
+ element_addonPrefs.removeEventListener("popupshowing", this);
+ // Remove our entry.
+ let entry = window.document.getElementById(this.menu_addonPrefs_id + "_" + this.uniqueRandomID);
+ if (entry) entry.remove();
+ // Do we have to unhide the noPrefsElement?
+ if (element_addonPrefs.children.length == 1) {
+ let noPrefsElem = element_addonPrefs.querySelector('[disabled="true"]');
+ noPrefsElem.style.display = "inline";
+ }
+ } else {
+ // Remove event listener for addon manager view changes
+ let managerWindow = this.getAddonManagerFromWindow(window);
+ if (managerWindow && managerWindow[this.uniqueRandomID] && managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners) {
+ managerWindow.document.removeEventListener("ViewChanged", this);
+ managerWindow.document.removeEventListener("update", this);
+
+ let cards = this.getCards(managerWindow);
+ if (this.getThunderbirdMajorVersion() < 88) {
+ // Remove options menu in 78-87
+ for (let card of cards) {
+ let addonOptionsLegacyEntry = card.querySelector(".extension-options-legacy");
+ if (addonOptionsLegacyEntry) addonOptionsLegacyEntry.remove();
+ }
+ } else {
+ // Remove options button in 88
+ for (let card of cards) {
+ if (card.addon.id == this.extension.id) {
+ let addonOptionsButton = card.querySelector(".extension-options-button2");
+ if (addonOptionsButton) addonOptionsButton.remove();
+ break;
+ }
+ }
+ }
+ }
+
+ // Remove tabmonitor
+ if (window[this.uniqueRandomID].hasTabMonitor) {
+ this.getTabMail(window).unregisterTabMonitor(this.tabMonitor);
+ window[this.uniqueRandomID].hasTabMonitor = false;
+ }
+ }
+ }
+
+ // if it is app shutdown, it is not just an add-on deactivation
+ this._unloadFromWindow(window, !isAppShutdown);
+ }
+ // Stop listening for new windows.
+ ExtensionSupport.unregisterWindowListener("injectListener_" + this.uniqueRandomID);
+ }
+
+ // Load registered shutdown script
+ let shutdownJS = {};
+ shutdownJS.extension = this.extension;
+ try {
+ if (this.pathToShutdownScript) Services.scriptloader.loadSubScript(this.pathToShutdownScript, shutdownJS, "UTF-8");
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+
+ // Extract all registered chrome content urls
+ let chromeUrls = [];
+ if (this.chromeData) {
+ for (let chromeEntry of this.chromeData) {
+ if (chromeEntry[0].toLowerCase().trim() == "content") {
+ chromeUrls.push("chrome://" + chromeEntry[1] + "/");
+ }
+ }
+ }
+
+ // Unload JSMs of this add-on
+ const rootURI = this.extension.rootURI.spec;
+ for (let module of Cu.loadedModules) {
+ if (module.startsWith(rootURI) || (module.startsWith("chrome://") && chromeUrls.find(s => module.startsWith(s)))) {
+ this.log("Unloading: " + module);
+ Cu.unload(module);
+ }
+ }
+
+ // Flush all caches
+ Services.obs.notifyObservers(null, "startupcache-invalidate");
+ this.registeredWindows = {};
+
+ if (this.resourceData) {
+ const resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsISubstitutingProtocolHandler);
+ for (let res of this.resourceData) {
+ // [ "resource", "shortname" , "path" ]
+ resProto.setSubstitution(
+ res[1],
+ null,
+ );
+ }
+ }
+
+ if (this.chromeHandle) {
+ this.chromeHandle.destruct();
+ this.chromeHandle = null;
+ }
+ }
+};
diff --git a/xul-ext/api/windowListener/schema.json b/xul-ext/api/windowListener/schema.json
new file mode 100644
index 0000000..6147b6e
--- /dev/null
+++ b/xul-ext/api/windowListener/schema.json
@@ -0,0 +1,134 @@
+[
+ {
+ "namespace": "WindowListener",
+ "events": [
+ {
+ "name": "onNotifyBackground",
+ "type": "function",
+ "description": "Fired when a new notification from notifyTools.js in an Experiment has been received.",
+ "parameters": [
+ {
+ "name": "data",
+ "type": "any",
+ "description": "Restrictions of the structured clone algorythm apply."
+ }
+ ]
+ }
+ ],
+ "functions": [
+ {
+ "name": "notifyExperiment",
+ "type": "function",
+ "description": "Notifies notifyTools.js in an Experiment and sends data.",
+ "parameters": [
+ {
+ "name": "data",
+ "type": "any",
+ "description": "Restrictions of the structured clone algorythm apply."
+ }
+ ]
+ },
+ {
+ "name": "registerDefaultPrefs",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "aPath",
+ "type": "string",
+ "description": "Relative path to the default file."
+ }
+ ]
+ },
+ {
+ "name": "registerOptionsPage",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "aPath",
+ "type": "string",
+ "description": "Path to the options page, which should be made accessible in the (legacy) Add-On Options menu."
+ }
+ ]
+ },
+ {
+ "name": "registerChromeUrl",
+ "type": "function",
+ "description": "Register folders which should be available as chrome:// urls (as defined in the legacy chrome.manifest)",
+ "parameters": [
+ {
+ "name": "data",
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "description": "Array of manifest url definitions (content, locale, resource)"
+ }
+ ]
+ },
+ {
+ "name": "waitForMasterPassword",
+ "type": "function",
+ "async": true,
+ "parameters": []
+ },
+ {
+ "name": "openOptionsDialog",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "windowId",
+ "type": "integer",
+ "description": "Id of the window the dialog should be opened from."
+ }
+ ]
+ },
+ {
+ "name": "startListening",
+ "type": "function",
+ "async": true,
+ "parameters": []
+ },
+ {
+ "name": "registerWindow",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "windowHref",
+ "type": "string",
+ "description": "Url of the window, which should be listen for."
+ },
+ {
+ "name": "jsFile",
+ "type": "string",
+ "description": "Path to the JavaScript file, which should be loaded into the window."
+ }
+ ]
+ },
+ {
+ "name": "registerStartupScript",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "aPath",
+ "type": "string",
+ "description": "Path to a JavaScript file, which should be executed on add-on startup. The script will be executed after the main application window has been sucessfully loaded."
+ }
+ ]
+ },
+ {
+ "name": "registerShutdownScript",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "aPath",
+ "type": "string",
+ "description": "Path to a JavaScript file, which should be executed on add-on shutdown."
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/xul-ext/background.js b/xul-ext/background.js
new file mode 100644
index 0000000..4dc6bc2
--- /dev/null
+++ b/xul-ext/background.js
@@ -0,0 +1,62 @@
+/**
+ * commented out locales are not complete enough on Transifex
+ */
+const locales = [
+ 'cs',
+ //'da',
+ 'de',
+ //'el',
+ 'en-US',
+ 'es-AR',
+ 'fi',
+ 'fr',
+ //'hi',
+ 'hu',
+ //'it',
+ //'ja',
+ //'ko-KR',
+ 'nl',
+ //'pl',
+ //'pt-BR',
+ //'pt-PT',
+ //'ro',
+ 'ru',
+ 'sl',
+ //'sv-SE',
+ 'tr',
+ 'ul',
+ 'zh-CN'
+];
+(async () => {
+ // https://github.com/thundernest/addon-developer-support/wiki/WindowListener-API:-Getting-Started#windowlistenerregisterdefaultprefs
+ messenger.WindowListener.registerDefaultPrefs("defaults/preferences/prefs.js")
+ /**
+ * https://github.com/thundernest/addon-developer-support/wiki/WindowListener-API:-Getting-Started#windowlistenerregisterchromeurl
+ * Register the content, resource and locale entries from your legacy chrome.manifest via a call to registerChromeUrl().
+ */
+ messenger.WindowListener.registerChromeUrl([
+ ["content", "keefox", "chrome/content/"],
+ ["resource", "keefox", /*"classic/1.0", */ "chrome/skin/"],
+ ["resource", "kfmod", "modules/"],
+ ...locales.map((localeId) =>
+ ["locale", "keefox", localeId, `chrome/locale/${localeId}/`])
+ ]);
+ //xul-ext/chrome/skin
+ // https://github.com/thundernest/addon-developer-support/wiki/WindowListener-API:-Getting-Started#windowlistenerregisteroptionspage
+ messenger.WindowListener.registerOptionsPage("chrome://keefox/content/options.xhtml");
+
+ //
+ messenger.WindowListener.registerWindow(
+ "chrome://messenger/content/messenger.xhtml",
+ "chrome://keefox/content/scripts/messengerPanel.js");
+
+ messenger.WindowListener.registerWindow(
+ "chrome://global/content/commonDialog.xhtml",
+ "chrome://keefox/content/scripts/KFcommonDialog.js");
+ /*
+ messenger.WindowListener.registerWindow(
+ "chrome://global/content/win.xul",
+ "chrome://keefox/content/scripts/install.js");*/
+
+ await messenger.WindowListener.startListening();
+})()
\ No newline at end of file
diff --git a/xul-ext/chrome.manifest b/xul-ext/chrome.manifest
index 9a3b2fd..c6bd3f3 100644
--- a/xul-ext/chrome.manifest
+++ b/xul-ext/chrome.manifest
@@ -11,7 +11,7 @@ overlay chrome://messenger/content/messenger.xul chrome://keefox/content/panel.x
overlay chrome://gdata-provider/content/browserRequest.xul chrome://keefox/content/gdata-provider.xul
overlay chrome://messenger/content/browserRequest.xul chrome://keefox/content/gdata-provider.xul
-skin keefox classic/1.0 chrome/skin/
+resource keefox classic/1.0 chrome/skin/
resource kfmod modules/
# locales
diff --git a/xul-ext/chrome/content/KFcommonDialog.js b/xul-ext/chrome/content/KFcommonDialog.js
index 36aab27..1ccb761 100644
--- a/xul-ext/chrome/content/KFcommonDialog.js
+++ b/xul-ext/chrome/content/KFcommonDialog.js
@@ -39,6 +39,7 @@ if (!Cu)
const { keefox_org } = ChromeUtils.import("resource://kfmod/KF.js");
const { keeFoxLoginInfo } = ChromeUtils.import("resource://kfmod/kfDataModel.js");
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
var keeFoxDialogManager = {
scriptLoader : Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
@@ -47,9 +48,7 @@ var keeFoxDialogManager = {
__promptBundle : null, // String bundle for L10N
get _promptBundle() {
if (!this.__promptBundle) {
- var bunService = Components.classes["@mozilla.org/intl/stringbundle;1"].
- getService(Components.interfaces.nsIStringBundleService);
- this.__promptBundle = bunService.createBundle(
+ this.__promptBundle = Services.strings.createBundle(
"chrome://global/locale/prompts.properties");
if (!this.__promptBundle)
throw "Prompt string bundle not present!";
@@ -60,9 +59,7 @@ var keeFoxDialogManager = {
__cdBundle : null, // String bundle for L10N
get _cdBundle() {
if (!this.__cdBundle) {
- var bunService = Components.classes["@mozilla.org/intl/stringbundle;1"].
- getService(Components.interfaces.nsIStringBundleService);
- this.__cdBundle = bunService.createBundle(
+ this.__cdBundle = Services.strings.createBundle(
"chrome://global/locale/commonDialogs.properties");
if (!this.__cdBundle)
throw "Common Dialogs string bundle not present!";
@@ -73,9 +70,7 @@ var keeFoxDialogManager = {
__messengerBundle : null, // string bundle for thunderbird l10n
get _messengerBundle() {
if (!this.__messengerBundle) {
- var bunService = Components.classes["@mozilla.org/intl/stringbundle;1"].
- getService(Components.interfaces.nsIStringBundleService);
- this.__messengerBundle = bunService.createBundle(
+ this.__messengerBundle = Services.strings.createBundle(
"chrome://messenger/locale/messenger.properties");
if (!this.__messengerBundle)
throw "Messenger string bundle not present!";
@@ -86,9 +81,7 @@ var keeFoxDialogManager = {
__localMsgsBundle : null, // string bundle for thunderbird l10n
get _localMsgsBundle() {
if (!this.__localMsgsBundle) {
- var bunService = Components.classes["@mozilla.org/intl/stringbundle;1"].
- getService(Components.interfaces.nsIStringBundleService);
- this.__localMsgsBundle = bunService.createBundle(
+ this.__localMsgsBundle = Services.strings.createBundle(
"chrome://messenger/locale/localMsgs.properties");
if (!this.__localMsgsBundle)
throw "localMsgs string bundle not present!";
@@ -99,9 +92,7 @@ var keeFoxDialogManager = {
__imapMsgsBundle : null, // string bundle for thunderbird l10n
get _imapMsgsBundle() {
if (!this.__imapMsgsBundle) {
- var bunService = Components.classes["@mozilla.org/intl/stringbundle;1"].
- getService(Components.interfaces.nsIStringBundleService);
- this.__imapMsgsBundle = bunService.createBundle(
+ this.__imapMsgsBundle = Services.strings.createBundle(
"chrome://messenger/locale/imapMsgs.properties");
if (!this.__imapMsgsBundle)
throw "imapMsgs string bundle not present!";
@@ -128,9 +119,7 @@ var keeFoxDialogManager = {
__newsBundle : null, // string bundle for thunderbird l10n
get _newsBundle() {
if (!this.__newsBundle) {
- var bunService = Components.classes["@mozilla.org/intl/stringbundle;1"].
- getService(Components.interfaces.nsIStringBundleService);
- this.__newsBundle = bunService.createBundle(
+ this.__newsBundle = Services.strings.createBundle(
"chrome://messenger/locale/news.properties");
if (!this.__newsBundle)
throw "news string bundle not present!";
@@ -141,9 +130,7 @@ var keeFoxDialogManager = {
__composeBundle : null, // string bundle for thunderbird l10n
get _composeBundle() {
if (!this.__composeBundle) {
- var bunService = Components.classes["@mozilla.org/intl/stringbundle;1"].
- getService(Components.interfaces.nsIStringBundleService);
- this.__composeBundle = bunService.createBundle(
+ this.__composeBundle = Services.strings.createBundle(
"chrome://messenger/locale/messengercompose/composeMsgs.properties");
if (!this.__composeBundle)
throw "Compose Message string bundle not present!";
@@ -155,7 +142,7 @@ var keeFoxDialogManager = {
.getService(Components.interfaces.nsIXULAppInfo),
dialogInit : function(e) {
- window.removeEventListener("load", keeFoxDialogManager.dialogInit);
+ //window.removeEventListener("load", keeFoxDialogManager.dialogInit);
try
{
document.addEventListener("dialogaccept", event => {
@@ -283,112 +270,139 @@ var keeFoxDialogManager = {
{
let regexChars = /[\[\{\(\)\*\+\?\.\\\^\$\|]/g;
protocols[aDialogType] = aDialogType.split("-")[0];
- titles[aDialogType] = aStringBundle.GetStringFromName(aTitlePropertyName);
- prompts[aDialogType] = aStringBundle.GetStringFromName(aPromptPropertyName);
- prompts[aDialogType] = prompts[aDialogType].replace(regexChars, "\\$&");
- aHostPlaceholder = aHostPlaceholder.replace(regexChars, "\\$&");
- // use null as a flag to indicate that there was only one
- // placeholder and hostIsFirst and secondIsUserName are not applicable
- hostIsFirst[aDialogType] = null;
- if (aUserPlaceholder != null)
- {
- aUserPlaceholder = aUserPlaceholder.replace(regexChars, "\\$&");
- hostIsFirst[aDialogType] = prompts[aDialogType].indexOf(aHostPlaceholder) <
- prompts[aDialogType].indexOf(aUserPlaceholder);
- secondIsUserName[aDialogType] = true;
- }
- if (aRealmPlaceholder != null)
- {
- aRealmPlaceholder = aRealmPlaceholder.replace(regexChars, "\\$&");
- hostIsFirst[aDialogType] = prompts[aDialogType].indexOf(aHostPlaceholder) <
- prompts[aDialogType].indexOf(aRealmPlaceholder);
- secondIsUserName[aDialogType] = false;
- }
- prompts[aDialogType] = prompts[aDialogType].replace(aHostPlaceholder, "([^\\s]+)");
- if (aUserPlaceholder != null)
- {
- prompts[aDialogType] = prompts[aDialogType].replace(aUserPlaceholder, "([^\\s]+)");
+ // dirty fix
+ // use a big try/catch here to avoid errors on null string resources
+ try {
+ titles[aDialogType] = aStringBundle.GetStringFromName(aTitlePropertyName);
+ prompts[aDialogType] = aStringBundle.GetStringFromName(aPromptPropertyName);
+ titles[aDialogType] = titles[aDialogType].replace(regexChars, "\\$&");
+ prompts[aDialogType] = prompts[aDialogType].replace(regexChars, "\\$&");
+ aHostPlaceholder = aHostPlaceholder.replace(regexChars, "\\$&");
+ // use null as a flag to indicate that there was only one
+ // placeholder and hostIsFirst and secondIsUserName are not applicable
+ hostIsFirst[aDialogType] = null;
+ if (aUserPlaceholder != null)
+ {
+ aUserPlaceholder = aUserPlaceholder.replace(regexChars, "\\$&");
+ hostIsFirst[aDialogType] = prompts[aDialogType].indexOf(aHostPlaceholder) <
+ prompts[aDialogType].indexOf(aUserPlaceholder);
+ secondIsUserName[aDialogType] = true;
+ }
+ if (aRealmPlaceholder != null)
+ {
+ aRealmPlaceholder = aRealmPlaceholder.replace(regexChars, "\\$&");
+ hostIsFirst[aDialogType] = prompts[aDialogType].indexOf(aHostPlaceholder) <
+ prompts[aDialogType].indexOf(aRealmPlaceholder);
+ secondIsUserName[aDialogType] = false;
+ }
+ titles[aDialogType] = titles[aDialogType].replace(aHostPlaceholder, "([^\\s]+)");
+ prompts[aDialogType] = prompts[aDialogType].replace(aHostPlaceholder, "([^\\s]+)");
+ if (aUserPlaceholder != null)
+ {
+ titles[aDialogType] = titles[aDialogType].replace(aUserPlaceholder, "([^\\s]+)");
+ prompts[aDialogType] = prompts[aDialogType].replace(aUserPlaceholder, "([^\\s]+)");
+ }
+ if (aRealmPlaceholder != null)
+ {
+ prompts[aDialogType] = prompts[aDialogType].replace(aRealmPlaceholder, "([^\\s]+)");
+ }
+ extractUserFromHost[aDialogType] = aExtractUserFromHost;
}
- if (aRealmPlaceholder != null)
- {
- prompts[aDialogType] = prompts[aDialogType].replace(aRealmPlaceholder, "([^\\s]+)");
+ catch (resourceError) {
+ keefox_org._KFLog.error(`LoadDialogData failed for ${aDialogType},${aTitlePropertyName},${aPromptPropertyName}`, resourceError);
}
- extractUserFromHost[aDialogType] = aExtractUserFromHost;
}
- }
-
- LoadDialogData(this._composeBundle, "smtp", "smtpEnterPasswordPromptTitle",
+ };
+ LoadDialogData(this._composeBundle, "smtp", "smtpEnterPasswordPrompt",
"smtpEnterPasswordPromptWithUsername", "%1$S", null, "%2$S");
+
+ // SMTP alt
+ // title "Password Required for Outgoing (SMTP) Server %1$S"
+ // prompt "Enter your password for %2$S on %1$S:"
+ // see omni.ja/chrome/en-US/locale/en-US/messenger/messengercompose/composeMsgs.properties
+ LoadDialogData(this._composeBundle,
+ "smtp2",
+ "smtpEnterPasswordPromptTitleWithHostname",
+ "smtpEnterPasswordPromptWithUsername",
+ "%1$S",
+ null,
+ "%2$S");
+
var imapEnterPasswordPromptTitle = this._imapBundleUsesStrings
- ? "imapEnterPasswordPromptTitle" : "5051";
+ ? "imapEnterPasswordPromptTitleWithUsername" : "5051";
var imapEnterPasswordPrompt = this._imapBundleUsesStrings
- ? "imapEnterPasswordPrompt" : "5047";
- var isPreTB40ImapStrings = true;
- try {
+ ? "imapEnterServerPasswordPrompt" : "5047";
+ var isPreTB40ImapStrings = false;
+ /*try {
// Take a peek at imapEnterPasswordPrompt
this._imapMsgsBundle.GetStringFromName(imapEnterPasswordPrompt);
} catch (e) {
// The string identifier changed again in TB 40
imapEnterPasswordPrompt = "imapEnterServerPasswordPrompt";
isPreTB40ImapStrings = false;
- }
+ }*/
// The prompt changed from using one parameter to using two in TB40
LoadDialogData(this._imapMsgsBundle, "imap",
imapEnterPasswordPromptTitle, imapEnterPasswordPrompt,
isPreTB40ImapStrings ? "%S" : "%2$S", null,
isPreTB40ImapStrings ? null : "%1$S", isPreTB40ImapStrings);
- LoadDialogData(this._localMsgsBundle, "pop3", "pop3EnterPasswordPromptTitle",
- "pop3EnterPasswordPrompt", "%2$S", null, "%1$S");
+
+ LoadDialogData(this._localMsgsBundle, "pop3", "pop3EnterPasswordPromptTitleWithUsername",
+ "pop3EnterPasswordPromptTitleWithUsername", "%2$S", null, "%1$S");
+
LoadDialogData(this._newsBundle, "nntp-1", "enterUserPassTitle",
"enterUserPassServer", "%S");
+
LoadDialogData(this._newsBundle, "nntp-2", "enterUserPassTitle",
"enterUserPassGroup", "%2$S", "%1$S");
+
LoadDialogData(this._messengerBundle, "mail", "passwordTitle",
"passwordPrompt", "%2$S", null, "%1$S");
- for (let type in titles)
- {
- if (Dialog.args.title == titles[type])
- {
- // some types have the same title, so we have more checking to do
- let regEx = new RegExp(prompts[type]);
- let matches = Dialog.args.text.match(regEx);
- if (!matches)
- {
- continue;
- }
- if (hostIsFirst[type] === null) {
- // there is only one parameter, so nothing is first
- if (matches.length == 2) {
- if (extractUserFromHost[type])
- {
- // user and host are separated by @ character
- let lastAtSym = matches[1].lastIndexOf("@");
- username = matches[1].substring(0, lastAtSym);
- host = protocols[type] + "://" +
- matches[1].substring(lastAtSym + 1, matches[1].length);
- } else {
- host = protocols[type] + "://" + matches[1];
- }
- break;
+ const matchings = Object.keys(titles).filter((type) => {
+ // refactor the title identification with a regEx because some have placeholders like $S
+ let reTitle = new RegExp(titles[type]);
+ return reTitle.test(Dialog.args.title);
+ }).map((type) => {
+ let regEx = new RegExp(prompts[type]);
+ let matches = Dialog.args.text.match(regEx);
+ return {
+ type,
+ matches
+ };
+ });
+
+ matchings.forEach((m) => {
+ const type = m.type;
+ const matches = m.matches;
+ if (hostIsFirst[type] === null) {
+ // there is only one parameter, so nothing is first
+ if (matches !== null && matches.length == 2) {
+ if (extractUserFromHost[type])
+ {
+ // user and host are separated by @ character
+ let lastAtSym = matches[1].lastIndexOf("@");
+ username = matches[1].substring(0, lastAtSym);
+ host = protocols[type] + "://" +
+ matches[1].substring(lastAtSym + 1, matches[1].length);
+ } else {
+ host = protocols[type] + "://" + matches[1];
}
- } else
- {
- if (matches.length == 3) {
- if (hostIsFirst[type]) {
- host = protocols[type] + "://" + matches[1];
- username = matches[2];
- } else {
- host = protocols[type] + "://" + matches[2];
- username = matches[1];
- }
- break;
+ }
+ } else {
+ if (matches !== null && matches.length == 3) {
+ if (hostIsFirst[type]) {
+ host = protocols[type] + "://" + matches[1];
+ username = matches[2];
+ } else {
+ host = protocols[type] + "://" + matches[2];
+ username = matches[1];
}
}
}
- }
+ });
} // end if Thunderbird
-
+
if (host.length < 1)
{
// e.g. en-US:
@@ -560,24 +574,23 @@ var keeFoxDialogManager = {
this.mustAutoSubmit = mustAutoSubmit;
/* add ui elements to dialog */
-
- var row = document.createElement("row");
+
+ var row = document.createElement("div");
row.setAttribute("id","keefox-autoauth-row");
- row.setAttribute("flex", "1");
+ row.setAttribute("class","dialogRow");
// spacer to take up first column in layout
- var spacer = document.createElement("spacer");
- spacer.setAttribute("flex", "1");
+ var spacer = document.createElement("div");
row.appendChild(spacer);
// this box displays labels and also the list of entries when fetched
- var box = document.createElement("hbox");
+ var box = document.createXULElement("hbox");
box.setAttribute("id","keefox-autoauth-box");
box.setAttribute("align", "center");
box.setAttribute("flex", "1");
box.setAttribute("pack", "start");
- var loadingPasswords = document.createElement("description");
+ var loadingPasswords = document.createXULElement("description");
loadingPasswords.setAttribute("id","keefox-autoauth-description");
loadingPasswords.setAttribute("align", "start");
loadingPasswords.setAttribute("flex", "1");
@@ -585,7 +598,7 @@ var keeFoxDialogManager = {
row.appendChild(box);
// button to lauch KeePass
- var launchKeePassButton = document.createElement("button");
+ var launchKeePassButton = document.createXULElement("button");
launchKeePassButton.setAttribute("id", "keefox-launch-kp-button");
launchKeePassButton.setAttribute("label", keefox_org.locale.$STR("launchKeePass.label"));
launchKeePassButton.addEventListener("command", function (event) { keefox_org.launchKeePass(''); }, false);
@@ -597,6 +610,10 @@ var keeFoxDialogManager = {
}
},
+ /**
+ * This is where we send the identification request to keefox RPC
+ * Assuming that the validation token has been stored
+ */
updateDialog : function()
{
// check to make sure prepareFill was called
@@ -761,9 +778,9 @@ var keeFoxDialogManager = {
if (showList) {
var box = dialogFindLoginStorage.document.getElementById("keefox-autoauth-box");
- var list = dialogFindLoginStorage.document.createElement("menulist");
+ var list = dialogFindLoginStorage.document.createXULElement("menulist");
list.setAttribute("id","autoauth-list");
- var popup = dialogFindLoginStorage.document.createElement("menupopup");
+ var popup = dialogFindLoginStorage.document.createXULElement("menupopup");
var done = false;
for (var i = 0; i < matchedLogins.length; i++) {
@@ -779,7 +796,7 @@ var keeFoxDialogManager = {
});
for (var i = 0; i < matchedLogins.length; i++){
- var item = dialogFindLoginStorage.document.createElement("menuitem");
+ var item = dialogFindLoginStorage.document.createXULElement("menuitem");
item.setAttribute("label", keefox_org.locale.$STRF("matchedLogin.label",
[matchedLogins[i].username, matchedLogins[i].host]));
item.setAttribute("tooltiptext", keefox_org.locale.$STRF("matchedLogin.tip",
@@ -928,4 +945,5 @@ KPRPCConnectionObserver.prototype = {
keeFoxDialogManager.Logger = keefox_org._KFLog;
keeFoxDialogManager.scriptLoader.loadSubScript(
"chrome://keefox/content/shared/uriUtils.js", keeFoxDialogManager);
-window.addEventListener("load", keeFoxDialogManager.dialogInit, false);
+//window.addEventListener("load", keeFoxDialogManager.dialogInit, false);
+
diff --git a/xul-ext/chrome/content/UninstallHelper.js b/xul-ext/chrome/content/UninstallHelper.js
index c725cf0..9a0a496 100644
--- a/xul-ext/chrome/content/UninstallHelper.js
+++ b/xul-ext/chrome/content/UninstallHelper.js
@@ -214,14 +214,12 @@ keefox_win.UninstallHelper.prototype =
extraLabel.setAttribute("control", "keefox-uninstall-helper-feedback-extra");
container.appendChild(extraLabel);
- // FIXME: textbox multiline="true" is removed in TB68
- // https://developer.thunderbird.net/add-ons/updates/tb68#less-than-textbox-multiline-true-greater-than
- let extra = this.doc.createElement('textbox');
+ let extra = this.doc.createElementNS("http://www.w3.org/1999/xhtml",'textarea');
extra.setAttribute("id","keefox-uninstall-helper-feedback-extra");
- extra.setAttribute("multiline","true");
extra.addEventListener("keypress", pnCountNoteChars, false);
container.appendChild(extra);
+
if (!varients.smallScreen && varients.whyLocation == 2)
attachWhy.call(this);
diff --git a/xul-ext/chrome/content/context.js b/xul-ext/chrome/content/context.js
index 04d5b0a..72026b9 100644
--- a/xul-ext/chrome/content/context.js
+++ b/xul-ext/chrome/content/context.js
@@ -135,7 +135,7 @@ keefox_win.context = {
var tempButton = i == 0 ?
container :
- this._currentWindow.document.createElement("menuitem");
+ this._currentWindow.document.createXULElement("menuitem");
tempButton.setAttribute("label", keefox_org.locale.$STRF("matchedLogin.label"
, [usernameDisplayValue, login.title]));
tempButton.setAttribute("class", "menuitem-iconic");
diff --git a/xul-ext/chrome/content/context.xul b/xul-ext/chrome/content/context.xhtml
similarity index 97%
rename from xul-ext/chrome/content/context.xul
rename to xul-ext/chrome/content/context.xhtml
index f505848..a86e876 100644
--- a/xul-ext/chrome/content/context.xul
+++ b/xul-ext/chrome/content/context.xhtml
@@ -56,7 +56,7 @@
diff --git a/xul-ext/chrome/content/famsOptions.js b/xul-ext/chrome/content/famsOptions.js
index 6f84b13..9e9b00c 100644
--- a/xul-ext/chrome/content/famsOptions.js
+++ b/xul-ext/chrome/content/famsOptions.js
@@ -62,6 +62,10 @@ function onLoad()
event.preventDefault();
}
});
+ document.addEventListener("dialogcancel", event => {
+ event.preventDefault(); // Prevent the dialog closing.
+ return;
+ });
go();
}
@@ -91,7 +95,7 @@ function go() {
function renderAllMessageGroups()
{
var msgGroupContainer = window.document.getElementById("msgGroupContainer");
- var famsDescription = window.document.createElement("description");
+ var famsDescription = window.document.createXULElement("description");
famsDescription.textContent = FAMS.locale.internationaliseString(config.description);
msgGroupContainer.appendChild(famsDescription);
@@ -108,11 +112,11 @@ function renderAllMessageGroups()
function renderDownloadOptions()
{
var msgGroupContainer = window.document.getElementById("msgGroupContainer");
- var downloadSliderComplete = window.document.createElement("hbox");
- var downloadLabelExplanation = window.document.createElement("label");
- var downloadLabelNote = window.document.createElement("description");
- var downloadSlider = window.document.createElement("scale");
- var downloadLabel = window.document.createElement("label");
+ var downloadSliderComplete = window.document.createXULElement("hbox");
+ var downloadLabelExplanation = window.document.createXULElement("label");
+ var downloadLabelNote = window.document.createXULElement("description");
+ var downloadSlider = window.document.createXULElement("scale");
+ var downloadLabel = window.document.createXULElement("label");
downloadLabelExplanation.setAttribute("value", FAMS.getLocalisedString("Options-Download-Freq.label"));
downloadLabelNote.textContent = FAMS.getLocalisedString("Options-Download-Freq.desc", config.name);
@@ -142,14 +146,14 @@ function renderMessageGroup(msgGroupIndex)
var msgGroup = config.messageGroups[msgGroupIndex];
var msgGroupContainer = window.document.getElementById("msgGroupContainer");
- var singleMsgGroupContainer = window.document.createElement("vbox");
- var desc = window.document.createElement("description");
- var appearanceLabelNote = window.document.createElement("description");
- var enabledCheckbox = window.document.createElement("checkbox");
- var appearanceSliderComplete = window.document.createElement("hbox");
- var appearanceLabelExplanation = window.document.createElement("label");
- var appearanceSlider = window.document.createElement("scale");
- var appearanceLabel = window.document.createElement("label");
+ var singleMsgGroupContainer = window.document.createXULElement("vbox");
+ var desc = window.document.createXULElement("description");
+ var appearanceLabelNote = window.document.createXULElement("description");
+ var enabledCheckbox = window.document.createXULElement("checkbox");
+ var appearanceSliderComplete = window.document.createXULElement("hbox");
+ var appearanceLabelExplanation = window.document.createXULElement("label");
+ var appearanceSlider = window.document.createXULElement("scale");
+ var appearanceLabel = window.document.createXULElement("label");
//var seeAllButton = window.document.createElement("button");
desc.textContent = FAMS.locale.internationaliseString(msgGroup.description);
diff --git a/xul-ext/chrome/content/famsOptions.xul b/xul-ext/chrome/content/famsOptions.xhtml
similarity index 98%
rename from xul-ext/chrome/content/famsOptions.xul
rename to xul-ext/chrome/content/famsOptions.xhtml
index 7738c19..b4d8b1f 100644
--- a/xul-ext/chrome/content/famsOptions.xul
+++ b/xul-ext/chrome/content/famsOptions.xhtml
@@ -26,7 +26,6 @@