diff --git a/_locales/de/messages.json b/_locales/de/messages.json new file mode 100644 index 000000000..7ec6bf588 --- /dev/null +++ b/_locales/de/messages.json @@ -0,0 +1,189 @@ +{ + "extensionName": { + "message": "QuickFolders", + "description": "Name of the extension." + }, + + "extensionDescription": { + "message": "Ordner als Tabs - vereinfacht das Chaos im Verzeichnisbaum indem die wichtigsten Ordner zu Tabs gemacht werden.", + "description": "Description of the extension." + }, + + "notificationTitle": { + "message": "Click notification", + "description": "Title of the click notification." + }, + + + "window-title": { + "message": "Willkommen bei $addonName$", + "description": "Seitentitel. Der erste placeholder wird durch den Add-on Namen ersetzt", + "placeholders": { + "addonName" : { + "content" : "$1", + "example" : "quickFilters" + } + } + }, + + "heading-updated": { + "message": "$addonName$ wurde erfolgreich aktualisiert", + "description": "Seitenheader. Der erste placeholder wird durch den Add-on Namen ersetzt", + "placeholders": { + "addonName" : { + "content" : "$1", + "example" : "quickFilters" + } + } + }, + + "heading-installed": { + "message": "$addonName$ wurde erfolgreich installiert", + "description": "Seitenheader. Der erste placeholder wird durch den Add-on Namen ersetzt", + "placeholders": { + "addonName" : { + "content" : "$1", + "example" : "quickFilters" + } + } + }, + + "thanks-for-updating-intro": { + "message": "Danke dass Sie $addonName$ aktualisiert haben.", + "placeholders": { + "addonName" : { + "content" : "$1", + "example" : "quickFilters" + } + } + }, + + "thanks-for-installing-intro": { + "message": "Danke dass Sie $addonName$ installiert haben.", + "placeholders": { + "addonName" : { + "content" : "$1", + "example" : "quickFilters" + } + } + }, + + + "active-version-info" : { + "message":"Es läuft jetzt {boldStart} Version $addonVer$ {boldEnd} auf Thunderbird $appVer$.", + "description": "The boldStart and boldEnd parts will be replaced with html tags", + "placeholders": { + "addonVer" : { + "content" : "$1", + "example" : "5.0.1" + }, + "appVer" : { + "content" : "$2", + "example" : "78.0b4" + } + + } + }, + + "time-and-effort": { + "message": "Meine Add-Ons erfordern grundlegende Änderungen, um mit der Mozilla-Plattform kompatibel zu bleiben. Mein Entwicklungsteam auf Github (Realraven2000, opto und paenglab) arbeitet fleißig daran, die volle Funktionalität für $addonName$ wiederherzustellen.", + "placeholders": { + "addonName" : { + "content" : "$1", + "example" : "quickFilters" + } + } + }, + + "hours-effort" : { + "message": "Wir haben bereits erhebliche Ressourcen (über $hoursSpent$ Stunden) für das Konvertierungsprojekt aufgewendet - und es gibt mehr Arbeit bei der Behebung von Regressionen und Problemen, die durch Änderungen in Thunderbird verursacht werden.", + "placeholders": { + "hoursSpent" : { + "content" : "$1", + "example" : "250" + } + } + }, + + "support-suggestion" : { + "message": "Damit $addonName$ weiterhin funktionsfähig und lebendig bleiben kann, tun Sie ihm bitte einen entscheidenden Gefallen:", + "placeholders": { + "addonName" : { + "content" : "$1", + "example" : "quickFilters" + } + } + } , + + "purchase-a-license" : { + "message" : "Kaufen Sie eine Lizenz." + }, + + "donate" : { + "message" : "Helfen Sie mit einer einmaligen Spende." + }, + + "purchase-heading" : { + "message" : "Lizenz kaufen!" + }, + + "support-preference" : { + "message": "Dies ist die bevorzugte Methode zur Unterstützung von $addonName$.", + "placeholders": { + "addonName" : { + "content" : "$1", + "example" : "quickFilters" + } + } + }, + + "ongoing-work": { + "message": "Die notwendigen Änderungen, um auf das neue Modell ohne Legacy (Mail-Experiment) umzusteigen, haben bereits mehrere Wochen gedauert und dauern noch an, während wir möglicherweise einige Funktionen verlieren werden. Mit einem komplexen Add-On wie diesem wird teure Arbeit unvermeidlich." + }, + + + "btn-want-a-license": { + "message" : "Ich möchte eine Lizenz kaufen" + } , + + "donate-heading" : { + "message" : "Helfen durch Spenden!" + } , + + "cost-of-independence" : { + "message" : "Softwareentwicklung kostet Zeit und Geld - im Gegensatz zu vielen Plattform-Apps werden Add-Ons weder von großen Unternehmen geschrieben noch in irgendeiner Weise von Mozilla gesponsert." + } , + + "mozillas-basic-platform" : { + "message" : "Mozilla bietet nur eine Basis-Plattform, um unsere Software für Sie als Endbenutzer weiterzuleiten. Wenn wir unsere Benutzer um Zahlung oder Support bitten müssen, können wir dies nicht über addons.thunderbird.net tun - es handelt sich nicht um einen App Store mit integrierten Paywalls oder Lizenzverwaltung wie iTunes oder Google Play." + } , + + "addon-authors-struggle" : { + "message" : "Um weiterhin nützlich zu sein, müssen Add-On-Entwickler nun vermehrt die für die Reparatur und das Umschreiben erforderlichen Mittel aufbringen, damit unsere Benutzer weiterhin von den Funktionen profitieren können, die wir an den Tisch bringen." + } , + + "btn-want-to-donate" : { + "message" : "Ich möchte mit einer einmaligen Spende helfen." + } , + + "label-remind-me" : { + "message": "Erinnern Sie mich noch einmal in $ countDays $ Tagen.", + "placeholders": { + "countDays" : { + "content" : "$1", + "example" : "10" + } + } + }, + + "notificationContent": { + "message": "You clicked $URL$.", + "description": "Tells the user which link they clicked.", + "placeholders": { + "url" : { + "content" : "$1", + "example" : "https://developer.mozilla.org" + } + } + } +} \ No newline at end of file diff --git a/_locales/en/messages.json b/_locales/en/messages.json new file mode 100644 index 000000000..c53b2477c --- /dev/null +++ b/_locales/en/messages.json @@ -0,0 +1,188 @@ +{ + "extensionName": { + "message": "QuickFolders", + "description": "Name of the extension." + }, + + "extensionDescription": { + "message": "Bookmark your favorite mail folders in Thunderbird.", + "description": "Description of the extension." + }, + + "notificationTitle": { + "message": "Click notification", + "description": "Title of the click notification." + }, + + "window-title": { + "message": "Welcome to $addonName$", + "description": "page title. first placeholder will be replaced with Add-on name", + "placeholders": { + "addonName" : { + "content" : "$1", + "example" : "quickFilters" + } + } + }, + + "heading-updated": { + "message": "$addonName$ Updated Successfully", + "description": "page heading. first placeholder will be replaced with Add-on name", + "placeholders": { + "addonName" : { + "content" : "$1", + "example" : "quickFilters" + } + } + }, + + "heading-installed": { + "message": "Successfully installed $addonName$", + "description": "page heading. first placeholder will be replaced with Add-on name", + "placeholders": { + "addonName" : { + "content" : "$1", + "example" : "quickFilters" + } + } + }, + + + "thanks-for-updating-intro": { + "message": "Thank you for updating $addonName$.", + "placeholders": { + "addonName" : { + "content" : "$1", + "example" : "quickFilters" + } + } + }, + + "thanks-for-installing-intro": { + "message": "Thank you for installing $addonName$.", + "placeholders": { + "addonName" : { + "content" : "$1", + "example" : "quickFilters" + } + } + }, + + "active-version-info" : { + "message": "You are now running {boldStart} version $addonVer$ {boldEnd} on Thunderbird $appVer$.", + "description": "The boldStart and boldEnd parts will be replaced with html tags", + "placeholders": { + "addonVer" : { + "content" : "$1", + "example" : "Version 1.1" + }, + "appVer" : { + "content" : "$2", + "example" : "78.0b4" + } + + } + }, + + "time-and-effort": { + "message": "My Add-ons require fundamental changes in order to remain compatible with the Mozilla platform. My development team at Github (Realraven2000, opto and paenglab) is working diligently to restore full functionality for $addonName$ .", + "placeholders": { + "addonName" : { + "content" : "$1", + "example" : "quickFilters" + } + } + }, + + "hours-effort" : { + "message": "We have already spent significant resources (over $hoursSpent$ hours) on the conversion project - and there is more work in fixing regressions and issues caused by changes in Thunderbird.", + "placeholders": { + "hoursSpent" : { + "content" : "$1", + "example" : "250" + } + } + }, + + "support-suggestion" : { + "message": "So that $addonName$ stays functional and alive, please do it a vital favor:", + "placeholders": { + "addonName" : { + "content" : "$1", + "example" : "quickFilters" + } + } + }, + + "purchase-a-license" : { + "message" : "Purchase a license." + }, + + "donate" : { + "message" : "Help out with a one time donation." + }, + + "purchase-heading" : { + "message" : "Buying a License!" + }, + + "support-preference" : { + "message": "This is the preferred way of supporting $addonName$.", + "placeholders": { + "addonName" : { + "content" : "$1", + "example" : "quickFilters" + } + } + }, + + "ongoing-work": { + "message": "The necessary alterations to switch to the new legacy-less model (mail experiment) already took multiple weeks and are still ongoing, while it is possible that we will be losing some functionality. With a complex Add-on like this, expensive work becomes inevitable." + }, + + "btn-want-a-license": { + "message" : "I want to buy a license" + } , + + "donate-heading" : { + "message" : "Help by Donating!" + } , + + "cost-of-independence" : { + "message" : "Software development costs time and money - unlike many platform apps, Add-ons are not written by large corporations, nor are they sponsored in any way by Mozilla." + } , + + "mozillas-basic-platform" : { + "message" : "Mozilla only provides a basic platform for delivering our software to you, the end users. If we need to ask our users for payment or support we cannot do this from addons.thunderbird.net - it is not an 'app store' with built in paywalls or license management like iTunes or google play." + } , + + "addon-authors-struggle" : { + "message" : "To continue to be useful, Add-on developers now need to step up the resources needed to fix and rewrite so that our users can keep benefiting from the functionality we bring to the table." + } , + + "btn-want-to-donate" : { + "message" : "I want to help with a one time donation." + } , + + "label-remind-me" : { + "message": "Remind me again in $countDays$ days.", + "placeholders": { + "countDays" : { + "content" : "$1", + "example" : "10" + } + } + }, + + + "notificationContent": { + "message": "You clicked $URL$.", + "description": "Tells the user which link they clicked.", + "placeholders": { + "url" : { + "content" : "$1", + "example" : "https://developer.mozilla.org" + } + } + } +} \ No newline at end of file diff --git a/build.bat b/build.bat index 4efa5ac52..af8cde111 100644 --- a/build.bat +++ b/build.bat @@ -5,7 +5,7 @@ set /a quickFoldersRev+=1 REM replace previous rev with new pwsh -Command "(gc -en UTF8NoBOM manifest.json) -replace 'pre%oldRev%', 'pre%quickFoldersRev%' | Out-File manifest.json" rem "C:\Program Files\7-Zip\7z" a -xr!.svn quickFolders.zip install.rdf chrome.manifest chrome defaults license.txt -"C:\Program Files\7-Zip\7z" a -xr!.svn QuickFoldersWeb.zip manifest.json chrome.manifest chrome defaults popup license.txt qf-background.js release-notes.html +"C:\Program Files\7-Zip\7z" a -xr!.svn QuickFoldersWeb.zip manifest.json _locales chrome defaults popup license.txt *.js release-notes.html echo %quickFoldersRev% > revision.txt move QuickFolders*.xpi "..\..\..\Release\_Test Versions\5.0\" pwsh -Command "Start-Sleep -m 150" diff --git a/chrome/content/api/WindowListener/implementation.js b/chrome/content/api/WindowListener/implementation.js index 58ee29b35..17fc5c614 100644 --- a/chrome/content/api/WindowListener/implementation.js +++ b/chrome/content/api/WindowListener/implementation.js @@ -1,631 +1,693 @@ -/* - * This file is provided by the addon-developer-support repository at - * https://github.com/thundernest/addon-developer-support - * - * 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 { - getAPI(context) { - // track if this is the background/main context - this.isBackgroundContext = (context.viewType == "background"); - - this.uniqueRandomID = "AddOnNS" + context.extension.instanceId; - this.menu_addonsManager_id ="addonsManager"; - this.menu_addonsManager_prefs_id = "addonsManager_prefs_revived"; - this.menu_addonPrefs_id = "addonPrefs_revived"; - this.context= context, - - this.registeredWindows = {}; - this.pathToStartupScript = null; - this.pathToShutdownScript = null; - this.pathToOptionsPage = null; - this.chromeHandle = null; - this.chromeData = null; - this.resourceData = null; - this.openWindows = []; - - 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; - - return { - WindowListener: { - context:5, - 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.setCharPref(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.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 { - console.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); - }, - - async startListening() { - // async sleep function using Promise - async function 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); - }); - }; - - 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.context = 5; - startupJS.WL.messenger = Array.from(self.extension.views).find( - view => view.viewType === "background").xulBrowser.contentWindow - .wrappedJSObject.browser; - try { - if (self.pathToStartupScript) { - Services.scriptloader.loadSubScript(self.pathToStartupScript, startupJS, "UTF-8"); - // delay startup until startup has been finished - console.log("Waiting for async startup() in <" + self.pathToStartupScript + "> to finish."); - if (startupJS.startup) { - await startupJS.startup(); - console.log("startup() in <" + self.pathToStartupScript + "> finished"); - } else { - console.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) { - try { - // add the add-on options menu if needed - if (!window.document.getElementById(self.menu_addonsManager_prefs_id)) { - let addonprefs = window.MozXULElement.parseXULToFragment(` - - - - - `, ["chrome://messenger/locale/messenger.dtd"]); - - let element_addonsManager = window.document.getElementById(self.menu_addonsManager_id); - element_addonsManager.parentNode.insertBefore(addonprefs, element_addonsManager.nextSibling); - } - - // add the options entry - let element_addonPrefs = window.document.getElementById(self.menu_addonPrefs_id); - let id = self.menu_addonPrefs_id + "_" + self.uniqueRandomID; - - // Get the best size of the icon (16px or bigger) - let iconSizes = Object.keys(self.extension.manifest.icons); - iconSizes.sort((a,b)=>a-b); - let bestSize = iconSizes.filter(e => parseInt(e) >= 16).shift(); - let icon = bestSize ? self.extension.manifest.icons[bestSize] : ""; - - let name = self.extension.manifest.name; - let entry = window.MozXULElement.parseXULToFragment( - ``); - element_addonPrefs.appendChild(entry); - window.document.getElementById(id).addEventListener("command", function() {window.openDialog(self.pathToOptionsPage, "AddonOptions")}); - } catch (e) { - Components.utils.reportError(e) - } - } - } - - // 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 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 { - console.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; - - // 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 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 (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 (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:"); - injectChildren(Array.from(window.MozXULElement.parseXULToFragment(xulString, 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; - window[this.uniqueRandomID].WL.context = this.context; - // Add messenger object to WLDATA object - window[this.uniqueRandomID].WL.messenger = Array.from(this.extension.views).find( - view => view.viewType === "background").xulBrowser.contentWindow - .wrappedJSObject.browser; - // 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) { - 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")) { - let id = this.menu_addonPrefs_id + "_" + this.uniqueRandomID; - window.document.getElementById(id).remove(); - - //do we have to remove the entire add-on options menu? - let element_addonPrefs = window.document.getElementById(this.menu_addonPrefs_id); - if (element_addonPrefs.children.length == 0) { - window.document.getElementById(this.menu_addonsManager_prefs_id).remove(); - } - } - - // 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)))) { - console.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; - } - } -}; +/* + * This file is provided by the addon-developer-support repository at + * https://github.com/thundernest/addon-developer-support + * + * 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); + } + + 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.uniqueRandomID = "AddOnNS" + context.extension.instanceId; + this.menu_addonsManager_id ="addonsManager"; + this.menu_addonsManager_prefs_id = "addonsManager_prefs_revived"; + this.menu_addonPrefs_id = "addonPrefs_revived"; + + 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; + + return { + WindowListener: { + + async waitForMasterPassword() { + // Wait until master password has been entered (if needed) + while (!Services.logins.isLoggedIn) { + console.log("Waiting for master password."); + await self.sleep(1000); + } + console.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.setCharPref(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); + }, + + 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 = Array.from(self.extension.views).find( + view => view.viewType === "background").xulBrowser.contentWindow + .wrappedJSObject.browser; + 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) { + try { + // add the add-on options menu if needed + if (!window.document.getElementById(self.menu_addonsManager_prefs_id)) { + let addonprefs = window.MozXULElement.parseXULToFragment(` + + + + + `, ["chrome://messenger/locale/messenger.dtd"]); + + let element_addonsManager = window.document.getElementById(self.menu_addonsManager_id); + element_addonsManager.parentNode.insertBefore(addonprefs, element_addonsManager.nextSibling); + } + + // add the options entry + let element_addonPrefs = window.document.getElementById(self.menu_addonPrefs_id); + let id = self.menu_addonPrefs_id + "_" + self.uniqueRandomID; + + // Get the best size of the icon (16px or bigger) + let iconSizes = Object.keys(self.extension.manifest.icons); + iconSizes.sort((a,b)=>a-b); + let bestSize = iconSizes.filter(e => parseInt(e) >= 16).shift(); + let icon = bestSize ? self.extension.manifest.icons[bestSize] : ""; + + let name = self.extension.manifest.name; + let entry = window.MozXULElement.parseXULToFragment( + ``); + element_addonPrefs.appendChild(entry); + let WL = {} + WL.extension = self.extension; + WL.messenger = Array.from(self.extension.views).find( + view => view.viewType === "background").xulBrowser.contentWindow + .wrappedJSObject.browser; + window.document.getElementById(id).addEventListener("command", function() {window.openDialog(self.pathToOptionsPage, "AddonOptions", null, WL)}); + } catch (e) { + Components.utils.reportError(e) + } + } + } + + // 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 localicedXulString = xulString.replace(/__MSG_(.*?)__/g, localize); + injectChildren(Array.from(window.MozXULElement.parseXULToFragment(localicedXulString, 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 = Array.from(this.extension.views).find( + view => view.viewType === "background").xulBrowser.contentWindow + .wrappedJSObject.browser; + // 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) { + 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")) { + let id = this.menu_addonPrefs_id + "_" + this.uniqueRandomID; + window.document.getElementById(id).remove(); + + //do we have to remove the entire add-on options menu? + let element_addonPrefs = window.document.getElementById(this.menu_addonPrefs_id); + if (element_addonPrefs.children.length == 0) { + window.document.getElementById(this.menu_addonsManager_prefs_id).remove(); + } + } + + // 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/chrome/content/api/WindowListener/schema.json b/chrome/content/api/WindowListener/schema.json index 92968b4f4..328fb495d 100644 --- a/chrome/content/api/WindowListener/schema.json +++ b/chrome/content/api/WindowListener/schema.json @@ -42,6 +42,12 @@ } ] }, + { + "name": "waitForMasterPassword", + "type": "function", + "async": true, + "parameters": [] + }, { "name": "startListening", "type": "function", diff --git a/manifest.json b/manifest.json index 5ff1f3a8a..040ab2948 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,9 @@ "manifest_version" : 2, "name" : "QuickFolders", "description" : "Bookmark your favorite mail folders in Thunderbird.", - "version" : "5.0pre94", + "version" : "5.0pre98", + "default_locale": "en", + "author": "Axel Grude, Klaus Buecher/opto", "developer" : { "name" : "Axel Grude", "url" : "https://quickfolders.org/index.html" diff --git a/popup/installed.html b/popup/installed.html index 47b05e3eb..35f2e416d 100644 --- a/popup/installed.html +++ b/popup/installed.html @@ -3,56 +3,52 @@ - Welcome to QuickFolders + __MSG_window-title__ - + +
-

Successfully installed {addon}

-

Thank you for installing {addon}. You are now running {addon} {version} on Thunderbird {appver}.

+

__MSG_heading-installed__

+

+ __MSG_thanks-for-installing-intro__ + __MSG_active-version-info__ +

-

To help {addon} stay functional and alive, please:

+

+ __MSG_support-suggestion__ +

    -
  • Purchase a license
  • -
  • Help out with a one time donation.
  • +
  • __MSG_purchase-a-license__
  • +
  • __MSG_donate__
-

Buy a License!

- -

My Add-ons are currently undergoing fundamental changes in order to - remain compatible with the Mozilla platform. Changes there have been - increasingly more complex to the point that the annual conversion - effort to the new ESR period takes a considerable amount of time - and resources.

- -

In the case of {addon}, alterations to the new legacy-less - model already took multiple weeks and are still ongoing, while - it is likely that we will be losing some functionality. With a - complex Add-on like this, it is inevitable.

+

__MSG_purchase-heading__

+ +

+ __MSG_support-preference__ + __MSG_ongoing-work__ +

+ __MSG_btn-want-a-license__ +

+ -

Help with a Donation!

+

__MSG_donate-heading__

-

Software development costs time and money - unlike many platform apps, - Add-ons are not written - by large corporations, nor are they sponsored in any way by Mozilla. - Mozilla only provide us with a platform to bring our software to you, - the end users - if we need to charge or ask for help we cannot do - this from addons.thunderbird.net - it is not an "app store" with built in - paywalls or license management like iTunes or google play.

- -

In order to remain useful, Add-on authors have to now raise the funds - necessary to repair and rewrite so that our users can keep benefiting - from the functionality we bring to the table.

+

+ __MSG_cost-of-independence__ + __MSG_mozillas-basic-platform__ +

+

__MSG_addon-authors-struggle__

+ diff --git a/popup/installed.js b/popup/installed.js index 5826eaca7..affd1bf7c 100644 --- a/popup/installed.js +++ b/popup/installed.js @@ -2,7 +2,7 @@ addEventListener("click", async (event) => { if (event.target.id.startsWith("register")) { - messenger.Utilities.openLinkExternally("http://sites.fastspring.com/quickfolders/product/quickfolders?referrer=landing-update"); + messenger.Utilities.openLinkExternally("https://sites.fastspring.com/quickfolders/product/quickfolders?referrer=landing-update"); } }); @@ -32,13 +32,57 @@ addEventListener("click", async (event) => { addEventListener("load", async (event) => { - debugger; - let text = document.body.innerHTML, - htmltext = text.replace(/{addon}/g, await messenger.Utilities.getAddonName()), - htmltext2 = htmltext.replace(/{version}/g, await messenger.Utilities.getAddonVersion()); //oder: browser.runtime.getManifest().version - htmltext = htmltext2.replace(/{appver}/g, await messenger.Utilities.getTBVersion()); - //same for license, let htmltext=text.replace(/{addon}/g, await messenger.Utilities.getAddonName()); - document.body.innerHTML = htmltext; + const addonName = await browser.runtime.getManifest().name, // or mxUtilties.getAddonName()); == 'quickFilters' + hoursWorked = 250; + const mxUtilties = messenger.Utilities; + + console.log("load event install case."); + // force replacement for __MSG_xx__ entities + // using John's helper method (which calls i18n API) + i18n.updateDocument(); + + let h1 = document.getElementById('heading-installed'); + if (h1) { + // this api function can do replacements for us + h1.innerText = messenger.i18n.getMessage('heading-installed', addonName); + } + + let thanksInfo = document.getElementById('thanks-for-installing-intro'); + if (thanksInfo) { + console.log("thanksInfo = v v v "); + console.log(thanksInfo); + thanksInfo.innerText = messenger.i18n.getMessage("thanks-for-installing-intro", addonName); + } + + let verInfo = document.getElementById('active-version-info'); + if (verInfo) { + let addonVer = await mxUtilties.getAddonVersion(), + appVer = await mxUtilties.getTBVersion(); + + // use the i18n API + // You are now running version {version} on Thunderbird {appver}. + // for multiple replacements, pass an array + verInfo.innerHTML = messenger.i18n.getMessage("active-version-info", [addonVer, appVer]) + .replace("{boldStart}","") + .replace("{boldEnd}",""); + } + + let suggestion = document.getElementById('support-suggestion'); + if (suggestion) { + suggestion.innerText = messenger.i18n.getMessage("support-suggestion", addonName); + } + + let preference = document.getElementById('support-preference'); + if (preference) { + preference.innerText = messenger.i18n.getMessage("support-preference", addonName); + } + + + + + let title = document.getElementById('window-title'); + if (title) + title.innerText = messenger.i18n.getMessage("window-title", addonName); }); diff --git a/popup/update.html b/popup/update.html index 76f0536d0..378d6db36 100644 --- a/popup/update.html +++ b/popup/update.html @@ -3,67 +3,66 @@ - Update Landing Screen + __MSG_window-title__ +
-

{addon} Updated

-

Thank you for updating {addon}. You are now running version {version} on Thunderbird {appver}.

- -

My Add-ons require fundamental changes in order to - remain compatible with the Mozilla platform. My team at github.com - (realraven2000, opto and paenglab) is working to make {addon} fully - functional again. -

+

__MSG_heading-updated__

+

+ __MSG_thanks-for-updating-intro__ + __MSG_active-version-info__ +

-

- We have already spent over 200 hours on the conversion project - and there is more work in - fixing regressions and problems caused by changes in Thunderbird. -

- -

To help {addon} stay functional and alive:

-
    -
  • Purchase a license
  • -
  • Help out with a one time donation.
  • -
+

+ __MSG_time-and-effort__ +

+ +

+ __MSG_hours-effort__ +

-

Buy a License!

- -

In the case of {addon}, alterations to the new legacy-less - model already took multiple weeks and are still ongoing, while - it is likely that we will be losing some functionality. With a - complex Add-on like this its inevitable.

+

+ __MSG_support-suggestion__ +

+
    +
  • __MSG_purchase-a-license__
  • +
  • __MSG_donate__
  • +
+ +

__MSG_purchase-heading__

- +

+ __MSG_support-preference__ + __MSG_ongoing-work__ +

+ + + +

__MSG_donate-heading__

-

Help with a Donation!

- -

Software development costs time and money - unlike many platform apps, - Add-ons are not written - by large corporations, nor are they sponsored in any way by Mozilla. - Mozilla only provide us with a platform to bring our software to you, - the end users - if we need to charge or ask for help we cannot do - this from addons.thunderbird.net - it is not an "app store" with built in - paywalls or license management like iTunes or google play.

+

+ __MSG_cost-of-independence__ + + __MSG_mozillas-basic-platform__ +

+ +

__MSG_addon-authors-struggle__

+ + -

In order to remain useful, Add-on authors have to now raise the funds - necessary to repair and rewrite so that our users can keep benefiting - from the functionality we bring to the table.

- - -

-

- - - -

+

+ + + +

diff --git a/popup/update.js b/popup/update.js index 71aac9296..c47105954 100644 --- a/popup/update.js +++ b/popup/update.js @@ -4,7 +4,6 @@ // see api/utilities/implementation.js const mxUtilties = messenger.Utilities; - debugger; // Test functions /* await messenger.Utilities.logDebug ("-------------------------------------------\n" + @@ -54,15 +53,69 @@ addEventListener("click", async (event) => { addEventListener("load", async (event) => { - //debugger; + const addonName = await browser.runtime.getManifest().name, // or mxUtilties.getAddonName()); == 'quickFilters' + hoursWorked = 250, + remindInDays = 10; const mxUtilties = messenger.Utilities; - let text = document.body.innerHTML,// - htmltext = text.replace(/{addon}/g, await browser.runtime.getManifest().name ), //oder mxUtilties.getAddonName()); - htmltext2 = htmltext.replace(/{version}/g, await mxUtilties.getAddonVersion()); //oder: browser.runtime.getManifest().version + + // force replacement for __MSG_xx__ entities + // using John's helper method (which calls i18n API) + i18n.updateDocument(); + + - htmltext = htmltext2.replace(/{appver}/g, await mxUtilties.getTBVersion()); - //same for license, let htmltext=text.replace(/{addon}/g, await mxUtilties.getAddonName()); - document.body.innerHTML=htmltext; + let h1 = document.getElementById('heading-updated'); + if (h1) { + // this api function can do replacements for us + h1.innerText = messenger.i18n.getMessage('heading-updated', addonName); + } + + let thanksInfo = document.getElementById('thanks-for-updating-intro'); + if (thanksInfo) { + thanksInfo.innerText = messenger.i18n.getMessage("thanks-for-updating-intro", addonName); + } + + let verInfo = document.getElementById('active-version-info'); + if (verInfo) { + let addonVer = await mxUtilties.getAddonVersion(), + appVer = await mxUtilties.getTBVersion(); + + // use the i18n API + // You are now running version {version} on Thunderbird {appver}. + // for multiple replacements, pass an array + verInfo.innerHTML = messenger.i18n.getMessage("active-version-info", [addonVer, appVer]) + .replace("{boldStart}","") + .replace("{boldEnd}",""); + } + + let timeAndEffort = document.getElementById('time-and-effort'); + if (timeAndEffort) { + timeAndEffort.innerText = messenger.i18n.getMessage("time-and-effort", addonName); + } + + let measuredEffort = document.getElementById('hours-effort'); + if (measuredEffort) { + measuredEffort.innerText = messenger.i18n.getMessage("hours-effort", hoursWorked); + } + + let suggestion = document.getElementById('support-suggestion'); + if (suggestion) { + suggestion.innerText = messenger.i18n.getMessage("support-suggestion", addonName); + } + + let preference = document.getElementById('support-preference'); + if (preference) { + preference.innerText = messenger.i18n.getMessage("support-preference", addonName); + } + + let remind = document.getElementById('label-remind-me'); + if (remind) { + remind.innerText = messenger.i18n.getMessage("label-remind-me", remindInDays); + + } + + let title = document.getElementById('window-title'); + title.innerText = messenger.i18n.getMessage("window-title", addonName); }); diff --git a/revision.txt b/revision.txt index a064323d6..8d879f164 100644 --- a/revision.txt +++ b/revision.txt @@ -1 +1 @@ -94 +98