diff options
Diffstat (limited to 'content/api/BootstrapLoader')
-rw-r--r-- | content/api/BootstrapLoader/CHANGELOG.md | 75 | ||||
-rw-r--r-- | content/api/BootstrapLoader/README.md | 1 | ||||
-rw-r--r-- | content/api/BootstrapLoader/implementation.js | 917 | ||||
-rw-r--r-- | content/api/BootstrapLoader/schema.json | 61 |
4 files changed, 1054 insertions, 0 deletions
diff --git a/content/api/BootstrapLoader/CHANGELOG.md b/content/api/BootstrapLoader/CHANGELOG.md new file mode 100644 index 0000000..5006ecf --- /dev/null +++ b/content/api/BootstrapLoader/CHANGELOG.md @@ -0,0 +1,75 @@ +Version: 1.21 +------------- +- Explicitly set hasAddonManagerEventListeners flag to false on uninstall + +Version: 1.20 +------------- +- hard fork BootstrapLoader v1.19 implementation and continue to serve it for + Thunderbird 111 and older +- BootstrapLoader v1.20 has removed a lot of unnecessary code used for backward + compatibility + +Version: 1.19 +------------- +- fix race condition which could prevent the AOM tab to be monkey patched correctly + +Version: 1.18 +------------- +- be precise on which revision the wrench symbol should be displayed, instead of + the options button + +Version: 1.17 +------------- +- fix "ownerDoc.getElementById() is undefined" bug + +Version: 1.16 +------------- +- fix "tab.browser is undefined" bug + +Version 1.15 +------------ +- clear cache only if add-on is uninstalled/updated, not on app shutdown + +Version 1.14 +------------ +- fix for TB90 ("view-loaded" event) and TB78.10 (wrench icon for options) + +Version 1.13 +------------ +- removed notifyTools and move it into its own NotifyTools API + +Version 1.12 +------------ +- add support for notifyExperiment and onNotifyBackground + +Version 1.11 +------------ +- add openOptionsDialog() + +Version 1.10 +------------ +- fix for 68 + +Version 1.7 +----------- +- fix for beta 87 + +Version 1.6 +----------- +- add support for options button/menu in add-on manager and fix 68 double menu entry + +Version 1.5 +----------- +- fix for e10s + +Version 1.4 +----------- +- add registerOptionsPage + +Version 1.3 +----------- +- flush cache + +Version 1.2 +----------- +- add support for resource urls diff --git a/content/api/BootstrapLoader/README.md b/content/api/BootstrapLoader/README.md new file mode 100644 index 0000000..7e8fe2a --- /dev/null +++ b/content/api/BootstrapLoader/README.md @@ -0,0 +1 @@ +Usage description can be found in the [wiki](https://github.com/thundernest/addon-developer-support/wiki/Using-the-BootstrapLoader-API-to-convert-a-Legacy-Bootstrap-WebExtension-into-a-MailExtension-for-Thunderbird-78). diff --git a/content/api/BootstrapLoader/implementation.js b/content/api/BootstrapLoader/implementation.js new file mode 100644 index 0000000..03e6b76 --- /dev/null +++ b/content/api/BootstrapLoader/implementation.js @@ -0,0 +1,917 @@ +/* + * This file is provided by the addon-developer-support repository at + * https://github.com/thundernest/addon-developer-support + * + * Version: 1.21 + * + * 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/. + */ + +// Get various parts of the WebExtension framework that we need. +var { ExtensionCommon } = ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm"); +var { ExtensionSupport } = ChromeUtils.import("resource:///modules/ExtensionSupport.jsm"); +var { AddonManager } = ChromeUtils.import("resource://gre/modules/AddonManager.jsm"); +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +function getThunderbirdVersion() { + let parts = Services.appinfo.version.split("."); + return { + major: parseInt(parts[0]), + minor: parseInt(parts[1]), + revision: parts.length > 2 ? parseInt(parts[2]) : 0, + } +} + +function 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; +} + +var BootstrapLoader_102 = class extends ExtensionCommon.ExtensionAPI { + 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 (getThunderbirdVersion().major < 86) { + let ownerDoc = e.document || e.target.ownerDocument; + doc = ownerDoc.getElementById("html-view-browser").contentDocument; + } else if (getThunderbirdVersion().major < 87) { + let ownerDoc = e.document || e.target; + doc = ownerDoc.getElementById("html-view-browser").contentDocument; + } else { + doc = e.document || e.target; + } + return doc.querySelectorAll("addon-card"); + } + + // Add pref entry to 68 + add68PrefsEntry(event) { + let id = this.menu_addonPrefs_id + "_" + this.uniqueRandomID; + + // Get the best size of the icon (16px or bigger) + let iconSizes = this.extension.manifest.icons + ? Object.keys(this.extension.manifest.icons) + : []; + iconSizes.sort((a, b) => a - b); + let bestSize = iconSizes.filter(e => parseInt(e) >= 16).shift(); + let icon = bestSize ? this.extension.manifest.icons[bestSize] : ""; + + let name = this.extension.manifest.name; + let entry = icon + ? event.target.ownerGlobal.MozXULElement.parseXULToFragment( + `<menuitem class="menuitem-iconic" id="${id}" image="${icon}" label="${name}" />`) + : event.target.ownerGlobal.MozXULElement.parseXULToFragment( + `<menuitem id="${id}" label="${name}" />`); + + event.target.appendChild(entry); + let noPrefsElem = event.target.querySelector('[disabled="true"]'); + // using collapse could be undone by core, so we use display none + // noPrefsElem.setAttribute("collapsed", "true"); + noPrefsElem.style.display = "none"; + event.target.ownerGlobal.document.getElementById(id).addEventListener("command", this); + } + + // Event handler for the addon manager, to update the state of the options button. + handleEvent(e) { + switch (e.type) { + // 68 add-on options menu showing + case "popupshowing": { + this.add68PrefsEntry(e); + } + break; + + // 78/88 add-on options menu/button click + case "click": { + e.preventDefault(); + e.stopPropagation(); + let BL = {} + BL.extension = this.extension; + BL.messenger = getMessenger(this.context); + let w = Services.wm.getMostRecentWindow("mail:3pane"); + w.openDialog(this.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", BL); + } + break; + + // 68 add-on options menu command + case "command": { + let BL = {} + BL.extension = this.extension; + BL.messenger = getMessenger(this.context); + e.target.ownerGlobal.openDialog(this.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", BL); + } + 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) { + 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(".extension-options-legacy"); + if (card.addon.isActive && !addonOptionsLegacyEntry) { + let addonOptionsEntry = card.querySelector("addon-options panel-list panel-item[action='preferences']"); + addonOptionsLegacyEntry = card.ownerDocument.createElement("panel-item"); + addonOptionsLegacyEntry.setAttribute("data-l10n-id", "preferences-addon-button"); + addonOptionsLegacyEntry.classList.add("extension-options-legacy"); + addonOptionsEntry.parentNode.insertBefore( + addonOptionsLegacyEntry, + addonOptionsEntry + ); + card.querySelector(".extension-options-legacy").addEventListener("click", this); + } else if (!card.addon.isActive && addonOptionsLegacyEntry) { + addonOptionsLegacyEntry.remove(); + } + } else { + // Add-on button in 88 + let addonOptionsButton = card.querySelector(".extension-options-button2"); + if (card.addon.isActive && !addonOptionsButton) { + addonOptionsButton = card.ownerDocument.createElement("button"); + addonOptionsButton.classList.add("extension-options-button2"); + addonOptionsButton.style["min-width"] = "auto"; + addonOptionsButton.style["min-height"] = "auto"; + addonOptionsButton.style["width"] = "24px"; + addonOptionsButton.style["height"] = "24px"; + addonOptionsButton.style["margin"] = "0"; + addonOptionsButton.style["margin-inline-start"] = "8px"; + addonOptionsButton.style["-moz-context-properties"] = "fill"; + addonOptionsButton.style["fill"] = "currentColor"; + addonOptionsButton.style["background-image"] = "url('chrome://messenger/skin/icons/developer.svg')"; + addonOptionsButton.style["background-repeat"] = "no-repeat"; + addonOptionsButton.style["background-position"] = "center center"; + addonOptionsButton.style["padding"] = "1px"; + addonOptionsButton.style["display"] = "flex"; + addonOptionsButton.style["justify-content"] = "flex-end"; + card.optionsButton.parentNode.insertBefore( + addonOptionsButton, + card.optionsButton + ); + card.querySelector(".extension-options-button2").addEventListener("click", this); + } else if (!card.addon.isActive && addonOptionsButton) { + addonOptionsButton.remove(); + } + } + } + } + } + } + } + + // Some tab/add-on-manager related functions + getTabMail(window) { + return window.document.getElementById("tabmail"); + } + + // returns the outer browser, not the nested browser of the add-on manager + // events must be attached to the outer browser + getAddonManagerFromTab(tab) { + if (tab.browser && tab.mode.name == "contentTab") { + let win = tab.browser.contentWindow; + if (win && win.location.href == "about:addons") { + return win; + } + } + } + + getAddonManagerFromWindow(window) { + let tabMail = this.getTabMail(window); + for (let tab of tabMail.tabInfo) { + let managerWindow = this.getAddonManagerFromTab(tab); + if (managerWindow) { + return managerWindow; + } + } + } + + async getAddonManagerFromWindowWaitForLoad(window) { + let { setTimeout } = Services.wm.getMostRecentWindow("mail:3pane"); + + let tabMail = this.getTabMail(window); + for (let tab of tabMail.tabInfo) { + if (tab.browser && tab.mode.name == "contentTab") { + // Instead of registering a load observer, wait until its loaded. Not nice, + // but gets aroud a lot of edge cases. + while (!tab.pageLoaded) { + await new Promise(r => setTimeout(r, 150)); + } + let managerWindow = this.getAddonManagerFromTab(tab); + if (managerWindow) { + return managerWindow; + } + } + } + } + + setupAddonManager(managerWindow, forceLoad = false) { + if (!managerWindow) { + return; + } + if ( + managerWindow && + managerWindow[this.uniqueRandomID] && + managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners + ) { + 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); + } + } + + getAPI(context) { + this.uniqueRandomID = "AddOnNS" + context.extension.instanceId; + this.menu_addonPrefs_id = "addonPrefs"; + + + this.pathToBootstrapScript = null; + this.pathToOptionsPage = null; + this.chromeHandle = null; + this.chromeData = null; + this.resourceData = null; + this.bootstrappedObj = {}; + + // make the extension object and the messenger object available inside + // the bootstrapped scope + this.bootstrappedObj.extension = context.extension; + this.bootstrappedObj.messenger = getMessenger(this.context); + + this.BOOTSTRAP_REASONS = { + APP_STARTUP: 1, + APP_SHUTDOWN: 2, + ADDON_ENABLE: 3, + ADDON_DISABLE: 4, + ADDON_INSTALL: 5, + ADDON_UNINSTALL: 6, // not supported + ADDON_UPGRADE: 7, + ADDON_DOWNGRADE: 8, + }; + + const aomStartup = Cc["@mozilla.org/addons/addon-manager-startup;1"].getService(Ci.amIAddonManagerStartup); + const resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsISubstitutingProtocolHandler); + + let self = this; + + // TabMonitor to detect opening of tabs, to setup the options button in the add-on manager. + this.tabMonitor = { + onTabTitleChanged(tab) { }, + onTabClosing(tab) { }, + onTabPersist(tab) { }, + onTabRestored(tab) { }, + onTabSwitched(aNewTab, aOldTab) { }, + async onTabOpened(tab) { + if (tab.browser && tab.mode.name == "contentTab") { + let { setTimeout } = Services.wm.getMostRecentWindow("mail:3pane"); + // Instead of registering a load observer, wait until its loaded. Not nice, + // but gets aroud a lot of edge cases. + while (!tab.pageLoaded) { + await new Promise(r => setTimeout(r, 150)); + } + self.setupAddonManager(self.getAddonManagerFromTab(tab)); + } + }, + }; + + return { + BootstrapLoader: { + + registerOptionsPage(optionsUrl) { + self.pathToOptionsPage = optionsUrl.startsWith("chrome://") + ? optionsUrl + : context.extension.rootURI.resolve(optionsUrl); + }, + + openOptionsDialog(windowId) { + let window = context.extension.windowManager.get(windowId, context).window + let BL = {} + BL.extension = self.extension; + BL.messenger = getMessenger(self.context); + window.openDialog(self.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", BL); + }, + + 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; + }, + + registerBootstrapScript: async function (aPath) { + self.pathToBootstrapScript = aPath.startsWith("chrome://") + ? aPath + : context.extension.rootURI.resolve(aPath); + + // Get the addon object belonging to this extension. + let addon = await AddonManager.getAddonByID(context.extension.id); + //make the addon globally available in the bootstrapped scope + self.bootstrappedObj.addon = addon; + + // add BOOTSTRAP_REASONS to scope + for (let reason of Object.keys(self.BOOTSTRAP_REASONS)) { + self.bootstrappedObj[reason] = self.BOOTSTRAP_REASONS[reason]; + } + + // Load registered bootstrap scripts and execute its startup() function. + try { + if (self.pathToBootstrapScript) Services.scriptloader.loadSubScript(self.pathToBootstrapScript, self.bootstrappedObj, "UTF-8"); + if (self.bootstrappedObj.startup) self.bootstrappedObj.startup.call(self.bootstrappedObj, self.extension.addonData, self.BOOTSTRAP_REASONS[self.extension.startupReason]); + } catch (e) { + Components.utils.reportError(e) + } + + // Register window listener for main TB window + if (self.pathToOptionsPage) { + ExtensionSupport.registerWindowListener("injectListener_" + self.uniqueRandomID, { + chromeURLs: [ + "chrome://messenger/content/messenger.xul", + "chrome://messenger/content/messenger.xhtml", + ], + async onLoadWindow(window) { + if (getThunderbirdVersion().major < 78) { + let element_addonPrefs = window.document.getElementById(self.menu_addonPrefs_id); + element_addonPrefs.addEventListener("popupshowing", self); + } else { + // Add a tabmonitor, to be able to setup the options button/menu in the add-on manager. + self.getTabMail(window).registerTabMonitor(self.tabMonitor); + window[self.uniqueRandomID] = {}; + 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); + } + }, + + onUnloadWindow(window) { + } + }); + } + } + } + }; + } + + onShutdown(isAppShutdown) { + if (isAppShutdown) { + return; // the application gets unloaded anyway + } + + //remove our entry in the add-on options menu + if (this.pathToOptionsPage) { + for (let window of Services.wm.getEnumerator("mail:3pane")) { + if (getThunderbirdVersion().major < 78) { + let element_addonPrefs = window.document.getElementById(this.menu_addonPrefs_id); + element_addonPrefs.removeEventListener("popupshowing", this); + // Remove our entry. + let entry = window.document.getElementById(this.menu_addonPrefs_id + "_" + this.uniqueRandomID); + if (entry) entry.remove(); + // Do we have to unhide the noPrefsElement? + if (element_addonPrefs.children.length == 1) { + let noPrefsElem = element_addonPrefs.querySelector('[disabled="true"]'); + noPrefsElem.style.display = "inline"; + } + } else { + // Remove event listener for addon manager view changes + let managerWindow = this.getAddonManagerFromWindow(window); + if ( + managerWindow && + managerWindow[this.uniqueRandomID] && + managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners + ) { + managerWindow.document.removeEventListener("ViewChanged", this); + managerWindow.document.removeEventListener("update", this); + managerWindow.document.removeEventListener("view-loaded", this); + managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners = false; + + let cards = this.getCards(managerWindow); + if (getThunderbirdVersion().major < 88) { + // Remove options menu in 78-87 + for (let card of cards) { + let addonOptionsLegacyEntry = card.querySelector(".extension-options-legacy"); + if (addonOptionsLegacyEntry) addonOptionsLegacyEntry.remove(); + } + } else { + // Remove options button in 88 + for (let card of cards) { + if (card.addon.id == this.extension.id) { + let addonOptionsButton = card.querySelector(".extension-options-button2"); + if (addonOptionsButton) addonOptionsButton.remove(); + break; + } + } + } + } + + // Remove tabmonitor + if (window[this.uniqueRandomID].hasTabMonitor) { + this.getTabMail(window).unregisterTabMonitor(this.tabMonitor); + window[this.uniqueRandomID].hasTabMonitor = false; + } + + } + } + // Stop listening for new windows. + ExtensionSupport.unregisterWindowListener("injectListener_" + this.uniqueRandomID); + } + + // Execute registered shutdown() + try { + if (this.bootstrappedObj.shutdown) { + this.bootstrappedObj.shutdown( + this.extension.addonData, + isAppShutdown + ? this.BOOTSTRAP_REASONS.APP_SHUTDOWN + : this.BOOTSTRAP_REASONS.ADDON_DISABLE); + } + } catch (e) { + Components.utils.reportError(e) + } + + 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; + } + // Flush all caches + Services.obs.notifyObservers(null, "startupcache-invalidate"); + console.log("BootstrapLoader for " + this.extension.id + " unloaded!"); + } +}; + +// Removed all extra code for backward compatibility for better maintainability. +var BootstrapLoader_115 = class extends ExtensionCommon.ExtensionAPI { + 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 BL = {} + BL.extension = this.extension; + BL.messenger = getMessenger(this.context); + let w = Services.wm.getMostRecentWindow("mail:3pane"); + w.openDialog( + this.pathToOptionsPage, + "AddonOptions", + "chrome,resizable,centerscreen", + BL + ); + } + 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 && tab.mode.name == "contentTab") { + let win = tab.browser.contentWindow; + if (win && win.location.href == "about:addons") { + return win; + } + } + } + + getAddonManagerFromWindow(window) { + let tabMail = this.getTabMail(window); + for (let tab of tabMail.tabInfo) { + let managerWindow = this.getAddonManagerFromTab(tab); + if (managerWindow) { + return managerWindow; + } + } + } + + async getAddonManagerFromWindowWaitForLoad(window) { + let { setTimeout } = Services.wm.getMostRecentWindow("mail:3pane"); + + let tabMail = this.getTabMail(window); + for (let tab of tabMail.tabInfo) { + if (tab.browser && tab.mode.name == "contentTab") { + // Instead of registering a load observer, wait until its loaded. Not nice, + // but gets aroud a lot of edge cases. + while (!tab.pageLoaded) { + await new Promise(r => setTimeout(r, 150)); + } + let managerWindow = this.getAddonManagerFromTab(tab); + if (managerWindow) { + return managerWindow; + } + } + } + } + + setupAddonManager(managerWindow, forceLoad = false) { + if (!managerWindow) { + return; + } + if (!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); + } + } + + getAPI(context) { + this.uniqueRandomID = "AddOnNS" + context.extension.instanceId; + this.menu_addonPrefs_id = "addonPrefs"; + + + this.pathToBootstrapScript = null; + this.pathToOptionsPage = null; + this.chromeHandle = null; + this.chromeData = null; + this.resourceData = null; + this.bootstrappedObj = {}; + + // make the extension object and the messenger object available inside + // the bootstrapped scope + this.bootstrappedObj.extension = context.extension; + this.bootstrappedObj.messenger = getMessenger(this.context); + + this.BOOTSTRAP_REASONS = { + APP_STARTUP: 1, + APP_SHUTDOWN: 2, + ADDON_ENABLE: 3, + ADDON_DISABLE: 4, + ADDON_INSTALL: 5, + ADDON_UNINSTALL: 6, // not supported + ADDON_UPGRADE: 7, + ADDON_DOWNGRADE: 8, + }; + + const aomStartup = Cc["@mozilla.org/addons/addon-manager-startup;1"].getService(Ci.amIAddonManagerStartup); + const resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsISubstitutingProtocolHandler); + + let self = this; + + // TabMonitor to detect opening of tabs, to setup the options button in the add-on manager. + this.tabMonitor = { + onTabTitleChanged(tab) { }, + onTabClosing(tab) { }, + onTabPersist(tab) { }, + onTabRestored(tab) { }, + onTabSwitched(aNewTab, aOldTab) { }, + async onTabOpened(tab) { + if (tab.browser && tab.mode.name == "contentTab") { + let { setTimeout } = Services.wm.getMostRecentWindow("mail:3pane"); + // Instead of registering a load observer, wait until its loaded. Not nice, + // but gets aroud a lot of edge cases. + while (!tab.pageLoaded) { + await new Promise(r => setTimeout(r, 150)); + } + self.setupAddonManager(self.getAddonManagerFromTab(tab)); + } + }, + }; + + return { + BootstrapLoader: { + + registerOptionsPage(optionsUrl) { + self.pathToOptionsPage = optionsUrl.startsWith("chrome://") + ? optionsUrl + : context.extension.rootURI.resolve(optionsUrl); + }, + + openOptionsDialog(windowId) { + let window = context.extension.windowManager.get(windowId, context).window + let BL = {} + BL.extension = self.extension; + BL.messenger = getMessenger(self.context); + window.openDialog(self.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", BL); + }, + + 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; + }, + + registerBootstrapScript: async function (aPath) { + self.pathToBootstrapScript = aPath.startsWith("chrome://") + ? aPath + : context.extension.rootURI.resolve(aPath); + + // Get the addon object belonging to this extension. + let addon = await AddonManager.getAddonByID(context.extension.id); + //make the addon globally available in the bootstrapped scope + self.bootstrappedObj.addon = addon; + + // add BOOTSTRAP_REASONS to scope + for (let reason of Object.keys(self.BOOTSTRAP_REASONS)) { + self.bootstrappedObj[reason] = self.BOOTSTRAP_REASONS[reason]; + } + + // Load registered bootstrap scripts and execute its startup() function. + try { + if (self.pathToBootstrapScript) Services.scriptloader.loadSubScript(self.pathToBootstrapScript, self.bootstrappedObj, "UTF-8"); + if (self.bootstrappedObj.startup) self.bootstrappedObj.startup.call(self.bootstrappedObj, self.extension.addonData, self.BOOTSTRAP_REASONS[self.extension.startupReason]); + } catch (e) { + Components.utils.reportError(e) + } + + // Register window listener for main TB window + if (self.pathToOptionsPage) { + ExtensionSupport.registerWindowListener("injectListener_" + self.uniqueRandomID, { + chromeURLs: [ + "chrome://messenger/content/messenger.xul", + "chrome://messenger/content/messenger.xhtml", + ], + async onLoadWindow(window) { + if (getThunderbirdVersion().major < 78) { + let element_addonPrefs = window.document.getElementById(self.menu_addonPrefs_id); + element_addonPrefs.addEventListener("popupshowing", self); + } else { + // Add a tabmonitor, to be able to setup the options button/menu in the add-on manager. + self.getTabMail(window).registerTabMonitor(self.tabMonitor); + window[self.uniqueRandomID] = {}; + 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); + } + }, + + onUnloadWindow(window) { + } + }); + } + } + } + }; + } + + onShutdown(isAppShutdown) { + if (isAppShutdown) { + return; // the application gets unloaded anyway + } + + //remove our entry in the add-on options menu + if (this.pathToOptionsPage) { + for (let window of Services.wm.getEnumerator("mail:3pane")) { + if (getThunderbirdVersion().major < 78) { + let element_addonPrefs = window.document.getElementById(this.menu_addonPrefs_id); + element_addonPrefs.removeEventListener("popupshowing", this); + // Remove our entry. + let entry = window.document.getElementById(this.menu_addonPrefs_id + "_" + this.uniqueRandomID); + if (entry) entry.remove(); + // Do we have to unhide the noPrefsElement? + if (element_addonPrefs.children.length == 1) { + let noPrefsElem = element_addonPrefs.querySelector('[disabled="true"]'); + noPrefsElem.style.display = "inline"; + } + } else { + // Remove event listener for addon manager view changes + let managerWindow = this.getAddonManagerFromWindow(window); + if ( + managerWindow && + managerWindow[this.uniqueRandomID] && + managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners + ) { + managerWindow.document.removeEventListener("ViewChanged", this); + managerWindow.document.removeEventListener("update", this); + managerWindow.document.removeEventListener("view-loaded", this); + managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners = false; + + let cards = this.getCards(managerWindow); + if (getThunderbirdVersion().major < 88) { + // Remove options menu in 78-87 + for (let card of cards) { + let addonOptionsLegacyEntry = card.querySelector(".extension-options-legacy"); + if (addonOptionsLegacyEntry) addonOptionsLegacyEntry.remove(); + } + } else { + // Remove options button in 88 + for (let card of cards) { + if (card.addon.id == this.extension.id) { + let addonOptionsButton = card.querySelector(".extension-options-button2"); + if (addonOptionsButton) addonOptionsButton.remove(); + break; + } + } + } + } + + // Remove tabmonitor + if (window[this.uniqueRandomID].hasTabMonitor) { + this.getTabMail(window).unregisterTabMonitor(this.tabMonitor); + window[this.uniqueRandomID].hasTabMonitor = false; + } + + } + } + // Stop listening for new windows. + ExtensionSupport.unregisterWindowListener("injectListener_" + this.uniqueRandomID); + } + + // Execute registered shutdown() + try { + if (this.bootstrappedObj.shutdown) { + this.bootstrappedObj.shutdown( + this.extension.addonData, + isAppShutdown + ? this.BOOTSTRAP_REASONS.APP_SHUTDOWN + : this.BOOTSTRAP_REASONS.ADDON_DISABLE); + } + } catch (e) { + Components.utils.reportError(e) + } + + 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; + } + // Flush all caches + Services.obs.notifyObservers(null, "startupcache-invalidate"); + console.log("BootstrapLoader for " + this.extension.id + " unloaded!"); + } +}; + +var BootstrapLoader = getThunderbirdVersion().major < 111 + ? BootstrapLoader_102 + : BootstrapLoader_115; diff --git a/content/api/BootstrapLoader/schema.json b/content/api/BootstrapLoader/schema.json new file mode 100644 index 0000000..fe48fb6 --- /dev/null +++ b/content/api/BootstrapLoader/schema.json @@ -0,0 +1,61 @@ +[ + { + "namespace": "BootstrapLoader", + "functions": [ + { + "name": "registerOptionsPage", + "type": "function", + "parameters": [ + { + "name": "aPath", + "type": "string", + "description": "Path to the options page, which should be made accessible in the (legacy) Add-On Options menu." + } + ] + }, + { + "name": "openOptionsDialog", + "type": "function", + "parameters": [ + { + "name": "windowId", + "type": "integer", + "description": "Id of the window the dialog should be opened from." + } + ] + }, + { + "name": "registerChromeUrl", + "type": "function", + "description": "Register folders which should be available as chrome:// urls (as defined in the legacy chrome.manifest)", + "async": true, + "parameters": [ + { + "name": "chromeData", + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Array of ChromeData Arrays." + } + ] + }, + { + "name": "registerBootstrapScript", + "type": "function", + "description": "Register a bootstrap.js style script", + "async": true, + "parameters": [ + { + "name": "aPath", + "type": "string", + "description": "Either the chrome:// path to the script or its relative location from the root of the extension," + } + ] + } + ] + } +]
\ No newline at end of file |