diff --git a/chrome/content/api/LegacyPrefs/README.md b/chrome/content/api/LegacyPrefs/README.md index 88d08af89..9bdf5e1c3 100644 --- a/chrome/content/api/LegacyPrefs/README.md +++ b/chrome/content/api/LegacyPrefs/README.md @@ -4,31 +4,46 @@ Use this API to access Thunderbird's system preferences or to migrate your own p ## Usage -### API Functions +Add the [LegacyPrefs API](https://github.com/thundernest/addon-developer-support/tree/master/auxiliary-apis/LegacyPrefs) to your add-on. Your `manifest.json` needs an entry like this: + +``` + "experiment_apis": { + "LegacyPrefs": { + "schema": "api/LegacyPrefs/schema.json", + "parent": { + "scopes": ["addon_parent"], + "paths": [["LegacyPrefs"]], + "script": "api/LegacyPrefs/implementation.js" + } + } + }, +``` + +## API Functions This API provides the following functions: -#### async getPref(aName, [aFallback]) +### async getPref(aName, [aFallback]) Returns the value for the ``aName`` preference. If it is not defined or has no default value assigned, ``aFallback`` will be returned (which defaults to ``null``). -#### async getUserPref(aName) +### async getUserPref(aName) Returns the user defined value for the ``aName`` preference. This will ignore any defined default value and will only return an explicitly set value, which differs from the default. Otherwise it will return ``null``. -#### clearUserPref(aName) +### clearUserPref(aName) Clears the user defined value for preference ``aName``. Subsequent calls to ``getUserPref(aName)`` will return ``null``. -#### async setPref(aName, aValue) +### async setPref(aName, aValue) Set the ``aName`` preference to the given value. Will return false and log an error to the console, if the type of ``aValue`` does not match the type of the preference. -### API Events +## API Events This API provides the following events: -#### onChanged.addListener(listener, branch) +### onChanged.addListener(listener, branch) Register a listener which is notified each time a value in the specified branch is changed. The listener returns the name and the new value of the changed preference. diff --git a/chrome/content/api/LegacyPrefs/implementation.js b/chrome/content/api/LegacyPrefs/implementation.js index 528688e4f..18372288f 100644 --- a/chrome/content/api/LegacyPrefs/implementation.js +++ b/chrome/content/api/LegacyPrefs/implementation.js @@ -2,28 +2,31 @@ * This file is provided by the addon-developer-support repository at * https://github.com/thundernest/addon-developer-support * - * Version: 1.9 - * fixed fallback issue reported by Axel Grude + * Version 1.10 + * - adjusted to Thunderbird Supernova (Services is now in globalThis) * - * Version: 1.8 - * reworked onChanged event to allow registering multiple branches + * Version 1.9 + * - fixed fallback issue reported by Axel Grude * - * Version: 1.7 - * add onChanged event + * Version 1.8 + * - reworked onChanged event to allow registering multiple branches * - * Version: 1.6 - * add setDefaultPref() + * Version 1.7 + * - add onChanged event * - * Version: 1.5 - * replace set/getCharPref by set/getStringPref to fix encoding issue + * Version 1.6 + * - add setDefaultPref() * - * Version: 1.4 + * Version 1.5 + * - replace set/getCharPref by set/getStringPref to fix encoding issue + * + * Version 1.4 * - setPref() function returns true if the value could be set, otherwise false * - * Version: 1.3 + * Version 1.3 * - add setPref() function * - * Version: 1.2 + * Version 1.2 * - add getPref() function * * Author: John Bieling (john@thunderbird.net) @@ -39,10 +42,12 @@ var { ExtensionCommon } = ChromeUtils.import( var { ExtensionUtils } = ChromeUtils.import( "resource://gre/modules/ExtensionUtils.jsm" ); -var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); - var { ExtensionError } = ExtensionUtils; +var Services = globalThis.Services || + ChromeUtils.import("resource://gre/modules/Services.jsm").Services; + + var LegacyPrefs = class extends ExtensionCommon.ExtensionAPI { getAPI(context) { diff --git a/chrome/content/api/NotifyTools/README.md b/chrome/content/api/NotifyTools/README.md index 16c5653ac..0e4a6ef83 100644 --- a/chrome/content/api/NotifyTools/README.md +++ b/chrome/content/api/NotifyTools/README.md @@ -148,4 +148,4 @@ You must remove all added listeners when your add-on is disabled/reloaded. Inste ### setAddOnId(add-on-id) -The `notifyTools.js` script needs to know the ID of your add-on to be able to listen for messages. You may either define the ID directly in the first line of the script, or set it using `setAddOnId()`. \ No newline at end of file +The `notifyTools.js` script needs to know the ID of your add-on to be able to listen for messages. You may either define the ID directly in the first line of the script, or set it using `setAddOnId()`. diff --git a/chrome/content/api/NotifyTools/implementation.js b/chrome/content/api/NotifyTools/implementation.js index 3159c2f28..e284298f9 100644 --- a/chrome/content/api/NotifyTools/implementation.js +++ b/chrome/content/api/NotifyTools/implementation.js @@ -2,6 +2,9 @@ * This file is provided by the addon-developer-support repository at * https://github.com/thundernest/addon-developer-support * + * Version 1.5 + * - adjusted to Thunderbird Supernova (Services is now in globalThis) + * * Version 1.4 * - updated implementation to not assign this anymore * @@ -26,7 +29,8 @@ // Get various parts of the WebExtension framework that we need. var { ExtensionCommon } = ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm"); - var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + var Services = globalThis.Services || + ChromeUtils.import("resource://gre/modules/Services.jsm").Services; var observerTracker = new Set(); diff --git a/chrome/content/api/NotifyTools/schema.json b/chrome/content/api/NotifyTools/schema.json index 3d0e4e75f..b25bbc284 100644 --- a/chrome/content/api/NotifyTools/schema.json +++ b/chrome/content/api/NotifyTools/schema.json @@ -1,6 +1,6 @@ [ { - "namespace": "NotifyTools", + "namespace": "NotifyTools", "events": [ { "name": "onNotifyBackground", @@ -31,4 +31,4 @@ } ] } -] +] \ No newline at end of file diff --git a/chrome/content/api/Utilities/implementation.js b/chrome/content/api/Utilities/implementation.js index 6624acd9b..fcea9e67c 100644 --- a/chrome/content/api/Utilities/implementation.js +++ b/chrome/content/api/Utilities/implementation.js @@ -1,7 +1,6 @@ /* eslint-disable object-shorthand */ var { ExtensionCommon } = ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm"); -var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); // might be better to get the parent window of the current window // because we may be screwed otherwise. diff --git a/chrome/content/api/WindowListener/changelog.md b/chrome/content/api/WindowListener/changelog.md index 0e7690a27..57054d320 100644 --- a/chrome/content/api/WindowListener/changelog.md +++ b/chrome/content/api/WindowListener/changelog.md @@ -1,3 +1,33 @@ +Version: 1.62 +------------- +- fix bug in fullyLoaded() + +Version: 1.61 +------------- +- adjusted to Thunderbird Supernova (Services is now in globalThis) + +Version: 1.60 +------------- +- explicitly set hasAddonManagerEventListeners flag to false on uninstall + +Version: 1.59 +------------- +- store hasAddonManagerEventListeners flag in add-on scope instead on the global + window again, and clear it upon add-on removal + +Version: 1.58 +------------- +- hard fork WindowListener v1.57 implementation and continue to serve it for + Thunderbird 111 and older +- WindowListener v1.58 supports injection into nested browsers of the new + mailTab front end of Thunderbird Supernova and allows "about:message" and + "about:3pane" to be valid injection targets. More information can be found here: + https://developer.thunderbird.net/thunderbird-development/codebase-overview/mail-front-end + +Version: 1.57 +------------- +- fix race condition which could prevent the AOM tab to be monkey patched correctly + Version: 1.56 ------------- - be precise on which revision the wrench symbol should be displayed, instead of diff --git a/chrome/content/api/WindowListener/implementation.js b/chrome/content/api/WindowListener/implementation.js index 642f6b29f..9f29f441e 100644 --- a/chrome/content/api/WindowListener/implementation.js +++ b/chrome/content/api/WindowListener/implementation.js @@ -2,7 +2,7 @@ * This file is provided by the addon-developer-support repository at * https://github.com/thundernest/addon-developer-support * - * Version: 1.56 + * Version 1.62 * * Author: John Bieling (john@thunderbird.net) * @@ -18,32 +18,32 @@ var { ExtensionCommon } = ChromeUtils.import( var { ExtensionSupport } = ChromeUtils.import( "resource:///modules/ExtensionSupport.jsm" ); -var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var Services = globalThis.Services || + ChromeUtils.import("resource://gre/modules/Services.jsm").Services; + +function getThunderbirdVersion() { + let parts = Services.appinfo.version.split("."); + return { + major: parseInt(parts[0]), + minor: parseInt(parts[1]), + } +} -var WindowListener = class extends ExtensionCommon.ExtensionAPI { +var WindowListener_102 = 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) { + if (getThunderbirdVersion().major < 86) { let ownerDoc = e.document || e.target.ownerDocument; doc = ownerDoc.getElementById("html-view-browser").contentDocument; - } else if (this.getThunderbirdVersion().major < 87) { + } else if (getThunderbirdVersion().major < 87) { let ownerDoc = e.document || e.target; doc = ownerDoc.getElementById("html-view-browser").contentDocument; } else { @@ -67,11 +67,11 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { 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"]'); @@ -133,10 +133,10 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { // 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); + let optionsMenu = + (getThunderbirdVersion().major > 78 && getThunderbirdVersion().major < 88) || + (getThunderbirdVersion().major == 78 && getThunderbirdVersion().minor < 10) || + (getThunderbirdVersion().major == 78 && getThunderbirdVersion().minor == 10 && getThunderbirdVersion().revision < 2); if (optionsMenu) { // Options menu in 78.0-78.10 and 79-87 let addonOptionsLegacyEntry = card.querySelector( @@ -200,7 +200,7 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { // 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) { + if (tab.browser && tab.mode.name == "contentTab") { let win = tab.browser.contentWindow; if (win && win.location.href == "about:addons") { return win; @@ -211,9 +211,28 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { getAddonManagerFromWindow(window) { let tabMail = this.getTabMail(window); for (let tab of tabMail.tabInfo) { - let win = this.getAddonManagerFromTab(tab); - if (win) { - return win; + 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; + } } } } @@ -229,7 +248,7 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { )) { managerWindow.document.addEventListener("ViewChanged", this); managerWindow.document.addEventListener("update", this); - managerWindow.document.addEventListener("view-loaded", this); + managerWindow.document.addEventListener("view-loaded", this); managerWindow[this.uniqueRandomID] = {}; managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners = true; } @@ -329,41 +348,20 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { // TabMonitor to detect opening of tabs, to setup the options button in the add-on manager. this.tabMonitor = { - onTabTitleChanged(aTab) {}, - onTabClosing(aTab) {}, - onTabPersist(aTab) {}, - onTabRestored(aTab) {}, - onTabSwitched(aNewTab, aOldTab) { - //self.setupAddonManager(self.getAddonManagerFromTab(aNewTab)); - }, - async onTabOpened(aTab) { - if (aTab.browser) { - if (!aTab.pageLoaded) { - // await a location change if browser is not loaded yet - await new Promise((resolve) => { - let reporterListener = { - QueryInterface: ChromeUtils.generateQI([ - "nsIWebProgressListener", - "nsISupportsWeakReference", - ]), - onStateChange() {}, - onProgressChange() {}, - onLocationChange( - /* in nsIWebProgress*/ aWebProgress, - /* in nsIRequest*/ aRequest, - /* in nsIURI*/ aLocation - ) { - aTab.browser.removeProgressListener(reporterListener); - resolve(); - }, - onStatusChange() {}, - onSecurityChange() {}, - onContentBlockingEvent() {}, - }; - aTab.browser.addProgressListener(reporterListener); - }); + 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(aTab)); + self.setupAddonManager(self.getAddonManagerFromTab(tab)); } }, }; @@ -382,8 +380,8 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { aDocumentExistsAt(uriString) { self.log( "Checking if document at <" + - uriString + - "> used in registration actually exists." + uriString + + "> used in registration actually exists." ); try { let uriObject = Services.io.newURI(uriString); @@ -405,9 +403,8 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { let url = context.extension.rootURI.resolve(defaultUrl); let prefsObj = {}; - prefsObj.Services = ChromeUtils.import( - "resource://gre/modules/Services.jsm" - ).Services; + prefsObj.Services = globalThis.Services|| + ChromeUtils.import("resource://gre/modules/Services.jsm").Services; prefsObj.pref = function (aName, aDefault) { let defaults = Services.prefs.getDefaultBranch(""); switch (typeof aDefault) { @@ -423,10 +420,10 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { default: throw new Error( "Preference <" + - aName + - "> has an unsupported type <" + - typeof aDefault + - ">. Allowed are string, number and boolean." + aName + + "> has an unsupported type <" + + typeof aDefault + + ">. Allowed are string, number and boolean." ); } }; @@ -475,7 +472,7 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { if (self.debug && !this.aDocumentExistsAt(windowHref)) { self.error( "Attempt to register an injector script for non-existent window: " + - windowHref + windowHref ); return; } @@ -538,8 +535,8 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { // delay startup until startup has been finished self.log( "Waiting for async startup() in <" + - self.pathToStartupScript + - "> to finish." + self.pathToStartupScript + + "> to finish." ); if (startupJS.startup) { await startupJS.startup(); @@ -580,12 +577,12 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { // Special action #1: If this is the main messenger window if ( window.location.href == - "chrome://messenger/content/messenger.xul" || + "chrome://messenger/content/messenger.xul" || window.location.href == - "chrome://messenger/content/messenger.xhtml" + "chrome://messenger/content/messenger.xhtml" ) { if (self.pathToOptionsPage) { - if (self.getThunderbirdVersion().major < 78) { + if (getThunderbirdVersion().major < 78) { let element_addonPrefs = window.document.getElementById( self.menu_addonPrefs_id ); @@ -594,16 +591,14 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { self ); } else { - // Setup the options button/menu in the add-on manager, if it is already open. - self.setupAddonManager( - self.getAddonManagerFromWindow(window), - true - ); // 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); } } } @@ -638,7 +633,7 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { if ( targetWindow && targetWindow.location.href == - mutation.target.getAttribute("src") && + mutation.target.getAttribute("src") && targetWindow.document.readyState == "complete" ) { loaded = true; @@ -810,10 +805,10 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { if (debug) console.log( elements[i].tagName + - "#" + - elements[i].id + - ": insertafter " + - insertAfterElement.id + "#" + + elements[i].id + + ": insertafter " + + insertAfterElement.id ); if ( debug && @@ -822,8 +817,8 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { ) { console.error( "The id <" + - elements[i].id + - "> of the injected element already exists in the document!" + elements[i].id + + "> of the injected element already exists in the document!" ); } elements[i].setAttribute("wlapi_autoinjected", uniqueRandomID); @@ -842,10 +837,10 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { if (debug) console.log( elements[i].tagName + - "#" + - elements[i].id + - ": insertbefore " + - insertBeforeElement.id + "#" + + elements[i].id + + ": insertbefore " + + insertBeforeElement.id ); if ( debug && @@ -854,8 +849,8 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { ) { console.error( "The id <" + - elements[i].id + - "> of the injected element already exists in the document!" + elements[i].id + + "> of the injected element already exists in the document!" ); } elements[i].setAttribute("wlapi_autoinjected", uniqueRandomID); @@ -871,10 +866,10 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { if (debug) console.log( elements[i].tagName + - "#" + - elements[i].id + - " is an existing container, injecting into " + - elements[i].id + "#" + + elements[i].id + + " is an existing container, injecting into " + + elements[i].id ); injectChildren( Array.from(elements[i].children), @@ -918,10 +913,10 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { if (debug) console.log( elements[i].tagName + - "#" + - elements[i].id + - ": append to " + - container.id + "#" + + elements[i].id + + ": append to " + + container.id ); elements[i].setAttribute("wlapi_autoinjected", uniqueRandomID); container.appendChild(elements[i]); @@ -1046,7 +1041,7 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { if (isAppShutdown) { return; // the application gets unloaded anyway } - + // Unload from all still open windows let urls = Object.keys(this.registeredWindows); if (urls.length > 0) { @@ -1056,9 +1051,9 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { this.pathToOptionsPage && (window.location.href == "chrome://messenger/content/messenger.xul" || window.location.href == - "chrome://messenger/content/messenger.xhtml") + "chrome://messenger/content/messenger.xhtml") ) { - if (this.getThunderbirdVersion().major < 78) { + if (getThunderbirdVersion().major < 78) { let element_addonPrefs = window.document.getElementById( this.menu_addonPrefs_id ); @@ -1086,9 +1081,10 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { managerWindow.document.removeEventListener("ViewChanged", this); managerWindow.document.removeEventListener("view-loaded", this); managerWindow.document.removeEventListener("update", this); + managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners = false; let cards = this.getCards(managerWindow); - if (this.getThunderbirdVersion().major < 88) { + if (getThunderbirdVersion().major < 88) { // Remove options menu in 78-87 for (let card of cards) { let addonOptionsLegacyEntry = card.querySelector( @@ -1184,3 +1180,991 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { } } }; + +var WindowListener_115 = class extends ExtensionCommon.ExtensionAPI { + log(msg) { + if (this.debug) console.log("WindowListener API: " + msg); + } + + 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 = e.document || e.target; + return doc.querySelectorAll("addon-card"); + } + + + // Event handler for the addon manager, to update the state of the options button. + handleEvent(e) { + switch (e.type) { + 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; + + // 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 + if (card.addon.id == this.extension.id) { + // Add-on button + let addonOptionsButton = card.querySelector( + ".windowlistener-options-button" + ); + if (card.addon.isActive && !addonOptionsButton) { + let origAddonOptionsButton = card.querySelector(".extension-options-button") + origAddonOptionsButton.setAttribute("hidden", "true"); + + 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) { + let win = tab.browser.contentWindow; + if (win && win.location.href == "about:addons") { + return win; + } + } + } + + getAddonManagerFromWindow(window) { + let tabMail = this.getTabMail(window); + for (let tab of tabMail.tabInfo) { + let win = this.getAddonManagerFromTab(tab); + if (win) { + return win; + } + } + } + + setupAddonManager(managerWindow, forceLoad = false) { + if (!managerWindow) { + return; + } + if (!this.pathToOptionsPage) { + return; + } + if ( + managerWindow && + managerWindow[this.uniqueRandomID] && + managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners + ) { + return; + } + + 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(aTab) { }, + onTabClosing(aTab) { }, + onTabPersist(aTab) { }, + onTabRestored(aTab) { }, + onTabSwitched(aNewTab, aOldTab) { + //self.setupAddonManager(self.getAddonManagerFromTab(aNewTab)); + }, + async onTabOpened(aTab) { + if (aTab.browser) { + if (!aTab.pageLoaded) { + // await a location change if browser is not loaded yet + await new Promise((resolve) => { + let reporterListener = { + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + onStateChange() { }, + onProgressChange() { }, + onLocationChange( + /* in nsIWebProgress*/ aWebProgress, + /* in nsIRequest*/ aRequest, + /* in nsIURI*/ aLocation + ) { + aTab.browser.removeProgressListener(reporterListener); + resolve(); + }, + onStatusChange() { }, + onSecurityChange() { }, + onContentBlockingEvent() { }, + }; + aTab.browser.addProgressListener(reporterListener); + }); + } + self.setupAddonManager(self.getAddonManagerFromTab(aTab)); + self._loadIntoWindow(aTab.browser.contentWindow, false); + } + + if (aTab.chromeBrowser) { + self._loadIntoWindow(aTab.chromeBrowser.contentWindow, false); + } + }, + }; + + 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) { + // No sane way yet to detect if about:urls exists, maybe lookup the definition? + if (uriString.startsWith("about:")) { + return true; + } + + 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 = globalThis.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); + + 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) { + // Load JS into window + await 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"); + } + }, + }, + }; + } + + _loadIntoNestedBrowsers(window, isAddonActivation) { + let elements = []; + elements = elements.concat(...window.document.getElementsByTagName("browser")); + elements = elements.concat(...window.document.getElementsByTagName("xul:browser")); + for (let element of elements) { + this._loadIntoWindow(element.contentWindow, isAddonActivation); + } + + } + + async _loadIntoWindow(window, isAddonActivation) { + const fullyLoaded = async window => { + for (let i = 0; i < 20; i++) { + await this.sleep(50); + if ( + window && + window.location.href != "about:blank" && + window.document.readyState == "complete" + ) { + return; + } + } + throw new Error("Window ignored"); + } + + try { + await fullyLoaded(window); + } catch(ex) { + return; + } + + if (!window || window.hasOwnProperty(this.uniqueRandomID)) { + return; + } + + // Special action if this is the main messenger window. + if (window.location.href == "chrome://messenger/content/messenger.xhtml") { + // Add a tab monitor. The tabMonitor checks newly opened tabs and injects us. + this.getTabMail(window).registerTabMonitor(this.tabMonitor); + window[this.uniqueRandomID] = {}; + window[this.uniqueRandomID].hasTabMonitor = true; + + // Setup the options button/menu in the add-on manager, if it is already open. + this.setupAddonManager(this.getAddonManagerFromWindow(window), true); + } + + // Load into nested browsers + this._loadIntoNestedBrowsers(window, isAddonActivation); + + if (this.registeredWindows.hasOwnProperty(window.location.href)) { + if (!window.hasOwnProperty(this.uniqueRandomID)) { + window[this.uniqueRandomID] = {}; + } + + 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. + let elements = []; + elements = elements.concat(...window.document.getElementsByTagName("browser")); + elements = elements.concat(...window.document.getElementsByTagName("xul:browser")); + for (let element of elements) { + if (element.contentWindow) { + this._unloadFromWindow( + element.contentWindow, + 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 (window.location.href == "chrome://messenger/content/messenger.xhtml") { + // 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); + managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners = false; + + let buttons = managerWindow.document.getElementsByClassName("extension-options-button"); + for (let button of buttons) { + button.removeAttribute("hidden"); + } + let cards = this.getCards(managerWindow); + // Remove options button in 88+ + for (let card of cards) { + if (card.addon.id == this.extension.id) { + let origAddonOptionsButton = card.querySelector(".extension-options-button") + origAddonOptionsButton.removeAttribute("hidden"); + + 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; + } + } +}; + +var WindowListener = getThunderbirdVersion().major < 111 + ? WindowListener_102 + : WindowListener_115; diff --git a/chrome/content/api/WindowListener/schema.json b/chrome/content/api/WindowListener/schema.json index a33c705fa..5d3bbb28d 100644 --- a/chrome/content/api/WindowListener/schema.json +++ b/chrome/content/api/WindowListener/schema.json @@ -1,6 +1,6 @@ [ { - "namespace": "WindowListener", + "namespace": "WindowListener", "functions": [ { "name": "registerDefaultPrefs", @@ -34,7 +34,7 @@ "type": "array", "items": { "type": "array", - "items" : { + "items": { "type": "string" } }, @@ -105,4 +105,4 @@ } ] } -] +] \ No newline at end of file diff --git a/chrome/content/help.js b/chrome/content/help.js index 5f29fd441..be39fa380 100644 --- a/chrome/content/help.js +++ b/chrome/content/help.js @@ -1,5 +1,3 @@ -var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); - function containerClick(el, evt) { var code = evt.target; diff --git a/chrome/content/scripts/notifyTools.js b/chrome/content/scripts/notifyTools.js index f5e6afa27..7a48ed91b 100644 --- a/chrome/content/scripts/notifyTools.js +++ b/chrome/content/scripts/notifyTools.js @@ -1,4 +1,4 @@ -// Set this to the ID of your add-on. +// Set this to the ID of your add-on, or call notifyTools.setAddonID(). var ADDON_ID = "smarttemplate4@thunderbird.extension"; /* @@ -8,7 +8,17 @@ var ADDON_ID = "smarttemplate4@thunderbird.extension"; * For usage descriptions, please check: * https://github.com/thundernest/addon-developer-support/tree/master/scripts/notifyTools * - * Version: 1.3 + * Version 1.6 + * - adjusted to Thunderbird Supernova (Services is now in globalThis) + * + * Version 1.5 + * - deprecate enable(), disable() and registerListener() + * - add setAddOnId() + * + * Version 1.4 + * - auto enable/disable + * + * Version 1.3 * - registered listeners for notifyExperiment can return a value * - remove WindowListener from name of observer * @@ -19,21 +29,30 @@ var ADDON_ID = "smarttemplate4@thunderbird.extension"; * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var Services = globalThis.Services || + ChromeUtils.import("resource://gre/modules/Services.jsm").Services; var notifyTools = { registeredCallbacks: {}, registeredCallbacksNextId: 1, + addOnId: ADDON_ID, + + setAddOnId: function (addOnId) { + this.addOnId = addOnId; + }, onNotifyExperimentObserver: { observe: async function (aSubject, aTopic, aData) { - if (ADDON_ID == "") { + if (notifyTools.addOnId == "") { throw new Error("notifyTools: ADDON_ID is empty!"); } - if (aData != ADDON_ID) { + if (aData != notifyTools.addOnId) { return; } let payload = aSubject.wrappedJSObject; + + // Make sure payload has a resolve function, which we use to resolve the + // observer notification. if (payload.resolve) { let observerTrackerPromises = []; // Push listener into promise array, so they can run in parallel @@ -61,17 +80,26 @@ var notifyTools = { payload.resolve(results[0]); } } else { - // Just call the listener. + // Older version of NotifyTools, which is not sending a resolve function, deprecated. + console.log("Please update the notifyTools API to at least v1.5"); for (let registeredCallback of Object.values( notifyTools.registeredCallbacks )) { registeredCallback(payload.data); } - } + } }, }, - registerListener: function (listener) { + addListener: function (listener) { + if (Object.values(this.registeredCallbacks).length == 0) { + Services.obs.addObserver( + this.onNotifyExperimentObserver, + "NotifyExperimentObserver", + false + ); + } + let id = this.registeredCallbacksNextId++; this.registeredCallbacks[id] = listener; return id; @@ -79,50 +107,61 @@ var notifyTools = { removeListener: function (id) { delete this.registeredCallbacks[id]; + if (Object.values(this.registeredCallbacks).length == 0) { + Services.obs.removeObserver( + this.onNotifyExperimentObserver, + "NotifyExperimentObserver" + ); + } + }, + + removeAllListeners: function () { + if (Object.values(this.registeredCallbacks).length != 0) { + Services.obs.removeObserver( + this.onNotifyExperimentObserver, + "NotifyExperimentObserver" + ); + } + this.registeredCallbacks = {}; }, notifyBackground: function (data) { - if (ADDON_ID == "") { + if (this.addOnId == "") { throw new Error("notifyTools: ADDON_ID is empty!"); } return new Promise((resolve) => { Services.obs.notifyObservers( { data, resolve }, "NotifyBackgroundObserver", - ADDON_ID + this.addOnId ); }); }, - - enable: function() { - Services.obs.addObserver( - this.onNotifyExperimentObserver, - "NotifyExperimentObserver", - false - ); + + + // Deprecated. + + enable: function () { + console.log("Manually calling notifyTools.enable() is no longer needed."); + }, + + disable: function () { + console.log("notifyTools.disable() has been deprecated, use notifyTools.removeAllListeners() instead."); + this.removeAllListeners(); }, - disable: function() { - Services.obs.removeObserver( - this.onNotifyExperimentObserver, - "NotifyExperimentObserver" - ); + registerListener: function (listener) { + console.log("notifyTools.registerListener() has been deprecated, use notifyTools.addListener() instead."); + this.addListener(listener); }, -}; +}; -if (window) { +if (typeof window != "undefined" && window) { window.addEventListener( - "load", + "unload", function (event) { - notifyTools.enable(); - window.addEventListener( - "unload", - function (event) { - notifyTools.disable(); - }, - false - ); + notifyTools.removeAllListeners(); }, false ); diff --git a/chrome/content/scripts/st-am-adressing.js b/chrome/content/scripts/st-am-adressing.js index 91e7bffeb..439d47bd0 100644 --- a/chrome/content/scripts/st-am-adressing.js +++ b/chrome/content/scripts/st-am-adressing.js @@ -1,7 +1,5 @@ // Likely Obsolete for THunderbird 78 // Web Extensions will probably not be allowed to modify Thunderbird Options. :( -var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); - Services.scriptloader.loadSubScript("chrome://smarttemplate4/content/smartTemplate-main.js", window, "UTF-8"); Services.scriptloader.loadSubScript("chrome://smarttemplate4/content/smartTemplate-accounts.js", window, "UTF-8"); diff --git a/chrome/content/scripts/st-composer.js b/chrome/content/scripts/st-composer.js index 8b92b9158..b4ab6170f 100644 --- a/chrome/content/scripts/st-composer.js +++ b/chrome/content/scripts/st-composer.js @@ -1,5 +1,3 @@ -var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); - //original lds this after xul!! Services.scriptloader.loadSubScript("chrome://smarttemplate4/content/smartTemplate-main.js", window, "UTF-8"); diff --git a/chrome/content/scripts/st-messageWindow.js b/chrome/content/scripts/st-messageWindow.js index fcf81d4ad..4d1bfb018 100644 --- a/chrome/content/scripts/st-messageWindow.js +++ b/chrome/content/scripts/st-messageWindow.js @@ -1,5 +1,3 @@ -var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); - //original lds this after xul!! Services.scriptloader.loadSubScript("chrome://smarttemplate4/content/smartTemplate-main.js", window, "UTF-8"); Services.scriptloader.loadSubScript("chrome://smarttemplate4/content/scripts/hackToolbarbutton.js", window.SmartTemplate4, "UTF-8"); diff --git a/chrome/content/scripts/st-messenger.js b/chrome/content/scripts/st-messenger.js index 4ddbc3fbc..f05dff93d 100644 --- a/chrome/content/scripts/st-messenger.js +++ b/chrome/content/scripts/st-messenger.js @@ -1,6 +1,3 @@ -var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); - - //original lds this after xul!! Services.scriptloader.loadSubScript("chrome://smarttemplate4/content/smartTemplate-main.js", window, "UTF-8"); diff --git a/chrome/content/settings.js b/chrome/content/settings.js index 008d3c0dc..ce05c0e31 100644 --- a/chrome/content/settings.js +++ b/chrome/content/settings.js @@ -9,7 +9,6 @@ END LICENSE BLOCK */ -var { Services } = ChromeUtils.import('resource://gre/modules/Services.jsm'); var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); var LastInput = { diff --git a/chrome/content/smartTemplate-compose.js b/chrome/content/smartTemplate-compose.js index 983839605..4ce9938ec 100644 --- a/chrome/content/smartTemplate-compose.js +++ b/chrome/content/smartTemplate-compose.js @@ -9,9 +9,6 @@ BEGIN LICENSE BLOCK END LICENSE BLOCK */ -var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); - - // ------------------------------------------------------------------- // Insert template message and edit quote header // ------------------------------------------------------------------- diff --git a/chrome/content/smartTemplate-composer.js b/chrome/content/smartTemplate-composer.js index e950a0e81..6aedb4ae7 100644 --- a/chrome/content/smartTemplate-composer.js +++ b/chrome/content/smartTemplate-composer.js @@ -1,6 +1,5 @@ "use strict"; -var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); SmartTemplate4.composer = { load: function st4_composerLoad() { const Ci = Components.interfaces, diff --git a/chrome/content/smartTemplate-fileTemplates.js b/chrome/content/smartTemplate-fileTemplates.js index 679b5a571..c7ef16751 100644 --- a/chrome/content/smartTemplate-fileTemplates.js +++ b/chrome/content/smartTemplate-fileTemplates.js @@ -12,8 +12,6 @@ // Support external HTML files that can be selected during the button press // write / reply and forward. -var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); - SmartTemplate4.fileTemplates = { Entries: { templatesNew : [], diff --git a/chrome/content/smartTemplate-main.js b/chrome/content/smartTemplate-main.js index cd4506fb3..518ea9874 100644 --- a/chrome/content/smartTemplate-main.js +++ b/chrome/content/smartTemplate-main.js @@ -275,8 +275,6 @@ END LICENSE BLOCK */ -var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); - var SmartTemplate4 = { // definitions for whatIsX (time of %A-Za-z%) XisToday : 0, diff --git a/chrome/content/smartTemplate-overlay.js b/chrome/content/smartTemplate-overlay.js index a78a7fff9..4f3670238 100644 --- a/chrome/content/smartTemplate-overlay.js +++ b/chrome/content/smartTemplate-overlay.js @@ -9,7 +9,6 @@ BEGIN LICENSE BLOCK END LICENSE BLOCK */ -var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); var { VCardProperties } = ChromeUtils.import( "resource:///modules/VCardUtils.jsm"); diff --git a/chrome/content/smartTemplate-prefs.js b/chrome/content/smartTemplate-prefs.js index b349d1e2d..a61464321 100644 --- a/chrome/content/smartTemplate-prefs.js +++ b/chrome/content/smartTemplate-prefs.js @@ -9,8 +9,6 @@ BEGIN LICENSE BLOCK END LICENSE BLOCK */ -var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); - SmartTemplate4.Preferences = { Prefix: "extensions.smartTemplate4.", service: Services.prefs, diff --git a/chrome/content/smartTemplate-util.js b/chrome/content/smartTemplate-util.js index 4c470fc58..b0739cf00 100644 --- a/chrome/content/smartTemplate-util.js +++ b/chrome/content/smartTemplate-util.js @@ -10,7 +10,6 @@ BEGIN LICENSE BLOCK END LICENSE BLOCK */ -var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); var SmartTemplate4_TabURIregexp = { diff --git a/scripts/st-parser.mjs.js b/scripts/st-parser.mjs.js index a3c69f45d..f3771008a 100644 --- a/scripts/st-parser.mjs.js +++ b/scripts/st-parser.mjs.js @@ -2235,8 +2235,6 @@ export class Parser { } } catch(ex) { - var { Services } = ChromeUtils.import('resource://gre/modules/Services.jsm'); - Util.logException("FAILED: insertFileLink(" + txt + ") \n You may get more information if you enable debug mode.",ex ); Services.prompt.alert(null, "SmartTemplates", "Something went wrong trying to read a file: " + txt + "\n" + "Please check Javascript error console for detailed error message.");