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 @@