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..1e5050d --- /dev/null +++ b/xul-ext/api/windowListener/implementation.js @@ -0,0 +1,1194 @@ +/* + * This file is provided by the addon-developer-support repository at + * https://github.com/thundernest/addon-developer-support + * + * Version: 1.57 + * + * 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); + } + + getThunderbirdVersion() { + let parts = Services.appinfo.version.split("."); + return { + major: parseInt(parts[0]), + minor: parseInt(parts[1]), + revision: parts.length > 2 ? parseInt(parts[2]) : 0, + } + } + + 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.getThunderbirdVersion().major < 86) { + let ownerDoc = e.document || e.target.ownerDocument; + doc = ownerDoc.getElementById("html-view-browser").contentDocument; + } else if (this.getThunderbirdVersion().major < 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) { + let optionsMenu = + (this.getThunderbirdVersion().major > 78 && this.getThunderbirdVersion().major < 88) || + (this.getThunderbirdVersion().major == 78 && this.getThunderbirdVersion().minor < 10) || + (this.getThunderbirdVersion().major == 78 && this.getThunderbirdVersion().minor == 10 && this.getThunderbirdVersion().revision < 2); + if (optionsMenu) { + // Options menu in 78.0-78.10 and 79-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( + ".windowlistener-options-button" + ); + if (card.addon.isActive && !addonOptionsButton) { + addonOptionsButton = card.ownerDocument.createElement("button"); + addonOptionsButton.classList.add("windowlistener-options-button"); + addonOptionsButton.classList.add("extension-options-button"); + card.optionsButton.parentNode.insertBefore( + addonOptionsButton, + card.optionsButton + ); + card + .querySelector(".windowlistener-options-button") + .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) { + if (tab.browser && tab.mode.name == "contentTab") { + 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 managerWindow = this.getAddonManagerFromTab(tab); + if (managerWindow) { + return managerWindow; + } + } + } + + async getAddonManagerFromWindowWaitForLoad(window) { + let { setTimeout } = Services.wm.getMostRecentWindow("mail:3pane"); + + let tabMail = this.getTabMail(window); + for (let tab of tabMail.tabInfo) { + if (tab.browser && tab.mode.name == "contentTab") { + // Instead of registering a load observer, wait until its loaded. Not nice, + // but gets aroud a lot of edge cases. + while(!tab.pageLoaded) { + await new Promise(r => setTimeout(r, 150)); + } + let managerWindow = this.getAddonManagerFromTab(tab); + if (managerWindow) { + return managerWindow; + } + } + } + } + + setupAddonManager(managerWindow, forceLoad = false) { + if (!managerWindow) { + return; + } + if (!( + managerWindow && + managerWindow[this.uniqueRandomID] && + managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners + )) { + managerWindow.document.addEventListener("ViewChanged", this); + managerWindow.document.addEventListener("update", this); + managerWindow.document.addEventListener("view-loaded", this); + managerWindow[this.uniqueRandomID] = {}; + managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners = true; + } + if (forceLoad) 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 + if (context.viewType != "background") + throw new Error( + "The WindowListener API may only be called from the background page." + ); + + 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(tab) { }, + onTabClosing(tab) { }, + onTabPersist(tab) { }, + onTabRestored(tab) { }, + onTabSwitched(aNewTab, aOldTab) { }, + async onTabOpened(tab) { + if (tab.browser && tab.mode.name == "contentTab") { + let { setTimeout } = Services.wm.getMostRecentWindow("mail:3pane"); + // Instead of registering a load observer, wait until its loaded. Not nice, + // but gets aroud a lot of edge cases. + while(!tab.pageLoaded) { + await new Promise(r => setTimeout(r, 150)); + } + self.setupAddonManager(self.getAddonManagerFromTab(tab)); + } + }, + }; + + return { + WindowListener: { + 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) { + 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.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); + if (windowHref == "chrome://messenger/content/commonDialog.xhtml") { + // @XN-BUGFIX + // there is a bug somewhere with aDocumentExistsAt and this very specific window + // the existence test fails for chrome://global/content/commonDialog.xhtml + // and succeed for chrome://messenger/content/commonDialog.xhtml + // BUT this commonDialog from messenger fails to _loadIntoWindow(window, isAddonActivation) and does not activate + // WORKAROUD : set xhtml namespace to global for this url when it's supposed to be in messenger + self.log( + "reverting chrome commonDialog from messenger to global" + ); + windowHref = "chrome://global/content/commonDialog.xhtml"; + } + + self.registeredWindows[windowHref] = path; + } else { + self.error( + "Window <" + windowHref + "> has already been registered" + ); + } + }, + + registerStartupScript(aPath) { + self.pathToStartupScript = aPath.startsWith("chrome://") + ? aPath + : context.extension.rootURI.resolve(aPath); + }, + + registerShutdownScript(aPath) { + 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() { + // 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.getThunderbirdVersion().major < 78) { + let element_addonPrefs = window.document.getElementById( + self.menu_addonPrefs_id + ); + element_addonPrefs.addEventListener( + "popupshowing", + self + ); + } else { + // 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; + // Setup the options button/menu in the add-on manager, if it is already open. + let managerWindow = await self.getAddonManagerFromWindowWaitForLoad(window); + self.setupAddonManager(managerWindow, 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) { + if (isAppShutdown) { + return; // the application gets unloaded anyway + } + + // 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.getThunderbirdVersion().major < 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("view-loaded", this); + managerWindow.document.removeEventListener("update", this); + + let cards = this.getCards(managerWindow); + if (this.getThunderbirdVersion().major < 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( + ".windowlistener-options-button" + ); + 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..03963a5 --- /dev/null +++ b/xul-ext/api/windowListener/schema.json @@ -0,0 +1,108 @@ +[ + { + "namespace": "WindowListener", + "functions": [ + { + "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..3cd5673 --- /dev/null +++ b/xul-ext/background.js @@ -0,0 +1,63 @@ +/** + * 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://messenger/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..59b16eb 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.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.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 @@