summaryrefslogtreecommitdiffstats
path: root/comm/mail/base/content/specialTabs.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/base/content/specialTabs.js')
-rw-r--r--comm/mail/base/content/specialTabs.js1320
1 files changed, 1320 insertions, 0 deletions
diff --git a/comm/mail/base/content/specialTabs.js b/comm/mail/base/content/specialTabs.js
new file mode 100644
index 0000000000..15a163c981
--- /dev/null
+++ b/comm/mail/base/content/specialTabs.js
@@ -0,0 +1,1320 @@
+/* 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/. */
+
+/* global MozElements, openOptionsDialog */
+
+/* import-globals-from utilityOverlay.js */
+
+/* globals ZoomManager */ // From viewZoomOverlay.js
+/* globals PrintUtils */ // From printUtils.js
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { AddonManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+var { ExtensionParent } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionParent.sys.mjs"
+);
+var { MailE10SUtils } = ChromeUtils.import(
+ "resource:///modules/MailE10SUtils.jsm"
+);
+
+function tabProgressListener(aTab, aStartsBlank) {
+ this.mTab = aTab;
+ this.mBrowser = aTab.browser;
+ this.mBlank = aStartsBlank;
+ this.mProgressListener = null;
+}
+
+tabProgressListener.prototype = {
+ mTab: null,
+ mBrowser: null,
+ mBlank: null,
+ mProgressListener: null,
+
+ // cache flags for correct status bar update after tab switching
+ mStateFlags: 0,
+ mStatus: 0,
+ mMessage: "",
+
+ // count of open requests (should always be 0 or 1)
+ mRequestCount: 0,
+
+ addProgressListener(aProgressListener) {
+ this.mProgressListener = aProgressListener;
+ },
+
+ onProgressChange(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ ) {
+ if (this.mProgressListener) {
+ this.mProgressListener.onProgressChange(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ );
+ }
+ },
+ onProgressChange64(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ ) {
+ if (this.mProgressListener) {
+ this.mProgressListener.onProgressChange64(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ );
+ }
+ },
+ onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) {
+ if (this.mProgressListener) {
+ this.mProgressListener.onLocationChange(
+ aWebProgress,
+ aRequest,
+ aLocationURI,
+ aFlags
+ );
+ }
+ // onLocationChange is called for both the top-level content
+ // and the subframes.
+ if (aWebProgress.isTopLevel) {
+ // Don't clear the favicon if this onLocationChange was triggered
+ // by a pushState or a replaceState. See bug 550565.
+ if (
+ aWebProgress.isLoadingDocument &&
+ !(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE) &&
+ !(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
+ ) {
+ this.mTab.favIconUrl = null;
+ }
+
+ var location = aLocationURI ? aLocationURI.spec : "";
+ if (aLocationURI && !aLocationURI.schemeIs("about")) {
+ this.mTab.backButton.disabled = !this.mBrowser.canGoBack;
+ this.mTab.forwardButton.disabled = !this.mBrowser.canGoForward;
+ this.mTab.urlbar.value = location;
+ this.mTab.root.removeAttribute("collapsed");
+ } else {
+ this.mTab.root.setAttribute("collapsed", "false");
+ }
+
+ // Although we're unlikely to be loading about:blank, we'll check it
+ // anyway just in case. The second condition is for new tabs, otherwise
+ // the reload function is enabled until tab is refreshed.
+ this.mTab.reloadEnabled = !(
+ (location == "about:blank" && !this.mBrowser.browsingContext.opener) ||
+ location == ""
+ );
+ }
+ },
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (this.mProgressListener) {
+ this.mProgressListener.onStateChange(
+ aWebProgress,
+ aRequest,
+ aStateFlags,
+ aStatus
+ );
+ }
+
+ if (!aRequest) {
+ return;
+ }
+
+ let tabmail = document.getElementById("tabmail");
+
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
+ this.mRequestCount++;
+ } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ // Since we (try to) only handle STATE_STOP of the last request,
+ // the count of open requests should now be 0.
+ this.mRequestCount = 0;
+ }
+
+ if (
+ aStateFlags & Ci.nsIWebProgressListener.STATE_START &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK
+ ) {
+ if (!this.mBlank) {
+ this.mTab.title = specialTabs.contentTabType.loadingTabString;
+ this.mTab.securityIcon.setLoading(true);
+ tabmail.setTabBusy(this.mTab, true);
+ tabmail.setTabTitle(this.mTab);
+ }
+
+ // Set our unit testing variables accordingly
+ this.mTab.pageLoading = true;
+ this.mTab.pageLoaded = false;
+ } else if (
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK
+ ) {
+ this.mBlank = false;
+ this.mTab.securityIcon.setLoading(false);
+ tabmail.setTabBusy(this.mTab, false);
+ this.mTab.title = this.mTab.browser.contentTitle;
+ tabmail.setTabTitle(this.mTab);
+
+ // Set our unit testing variables accordingly
+ this.mTab.pageLoading = false;
+ this.mTab.pageLoaded = true;
+
+ // If we've finished loading, and we've not had an icon loaded from a
+ // link element, then we try using the default icon for the site.
+ if (aWebProgress.isTopLevel && !this.mTab.favIconUrl) {
+ specialTabs.useDefaultFavIcon(this.mTab);
+ }
+ }
+ },
+ onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {
+ if (this.mProgressListener) {
+ this.mProgressListener.onStatusChange(
+ aWebProgress,
+ aRequest,
+ aStatus,
+ aMessage
+ );
+ }
+ },
+ onSecurityChange(aWebProgress, aRequest, aState) {
+ if (this.mProgressListener) {
+ this.mProgressListener.onSecurityChange(aWebProgress, aRequest, aState);
+ }
+
+ const wpl = Ci.nsIWebProgressListener;
+ const wpl_security_bits =
+ wpl.STATE_IS_SECURE | wpl.STATE_IS_BROKEN | wpl.STATE_IS_INSECURE;
+ let level = "";
+ switch (aState & wpl_security_bits) {
+ case wpl.STATE_IS_SECURE:
+ level = "high";
+ break;
+ case wpl.STATE_IS_BROKEN:
+ level = "broken";
+ break;
+ }
+ this.mTab.securityIcon.setSecurityLevel(level);
+ },
+ onContentBlockingEvent(aWebProgress, aRequest, aEvent) {
+ if (this.mProgressListener) {
+ this.mProgressListener.onContentBlockingEvent(
+ aWebProgress,
+ aRequest,
+ aEvent
+ );
+ }
+ },
+ onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI) {
+ if (this.mProgressListener) {
+ return this.mProgressListener.onRefreshAttempted(
+ aWebProgress,
+ aURI,
+ aDelay,
+ aSameURI
+ );
+ }
+ return true;
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsIWebProgressListener2",
+ "nsISupportsWeakReference",
+ ]),
+};
+
+/**
+ * Handles tab icons for parent process browsers. The DOMLinkAdded event won't
+ * fire for child process browsers, that is handled by LinkHandlerParent.
+ */
+var DOMLinkHandler = {
+ handleEvent(event) {
+ switch (event.type) {
+ case "DOMLinkAdded":
+ case "DOMLinkChanged":
+ this.onLinkAdded(event);
+ break;
+ }
+ },
+ onLinkAdded(event) {
+ let link = event.target;
+ let rel = link.rel && link.rel.toLowerCase();
+ if (!link || !link.ownerDocument || !rel || !link.href) {
+ return;
+ }
+
+ if (rel.split(/\s+/).includes("icon")) {
+ if (!Services.prefs.getBoolPref("browser.chrome.site_icons")) {
+ return;
+ }
+
+ let targetDoc = link.ownerDocument;
+
+ let uri = Services.io.newURI(link.href, targetDoc.characterSet);
+
+ // Verify that the load of this icon is legal.
+ // Some error or special pages can load their favicon.
+ // To be on the safe side, only allow chrome:// favicons.
+ let isAllowedPage =
+ targetDoc.documentURI == "about:home" ||
+ ["about:neterror?", "about:blocked?", "about:certerror?"].some(
+ function (aStart) {
+ targetDoc.documentURI.startsWith(aStart);
+ }
+ );
+
+ if (!isAllowedPage || !uri.schemeIs("chrome")) {
+ // Be extra paraniod and just make sure we're not going to load
+ // something we shouldn't. Firefox does this, so we're doing the same.
+ try {
+ Services.scriptSecurityManager.checkLoadURIWithPrincipal(
+ targetDoc.nodePrincipal,
+ uri,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT
+ );
+ } catch (ex) {
+ return;
+ }
+ }
+
+ try {
+ var contentPolicy = Cc[
+ "@mozilla.org/layout/content-policy;1"
+ ].getService(Ci.nsIContentPolicy);
+ } catch (e) {
+ // Refuse to load if we can't do a security check.
+ return;
+ }
+
+ // Security says okay, now ask content policy. This is probably trying to
+ // ensure that the image loaded always obeys the content policy. There
+ // may have been a chance that it was cached and we're trying to load it
+ // direct from the cache and not the normal route.
+ let { NetUtil } = ChromeUtils.import(
+ "resource://gre/modules/NetUtil.jsm"
+ );
+ let tmpChannel = NetUtil.newChannel({
+ uri,
+ loadingNode: targetDoc,
+ securityFlags: Ci.nsILoadInfo.SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_IMAGE,
+ });
+ let tmpLoadInfo = tmpChannel.loadInfo;
+ if (
+ contentPolicy.shouldLoad(uri, tmpLoadInfo, link.type) !=
+ Ci.nsIContentPolicy.ACCEPT
+ ) {
+ return;
+ }
+
+ let tab = document
+ .getElementById("tabmail")
+ .getBrowserForDocument(targetDoc.defaultView);
+
+ // If we don't have a browser/tab, then don't load the icon.
+ if (!tab) {
+ return;
+ }
+
+ // Just set the url on the browser and we'll display the actual icon
+ // when we finish loading the page.
+ specialTabs.setFavIcon(tab, link.href);
+ }
+ },
+};
+
+var contentTabBaseType = {
+ // List of URLs that will receive special treatment when opened in a tab.
+ // Note that about:preferences is loaded via a different mechanism.
+ inContentWhitelist: [
+ "about:addons",
+ "about:addressbook",
+ "about:blank",
+ "about:profiles",
+ "about:*",
+ ],
+
+ // Code to run if a particular document is loaded in a tab.
+ // The array members (functions) are for the respective document URLs
+ // as specified in inContentWhitelist.
+ inContentOverlays: [
+ // about:addons
+ function (aDocument, aTab) {
+ Services.scriptloader.loadSubScript(
+ "chrome://messenger/content/aboutAddonsExtra.js",
+ aDocument.defaultView
+ );
+ },
+
+ // about:addressbook provides its own context menu.
+ function (aDocument, aTab) {
+ aTab.browser.removeAttribute("context");
+ },
+
+ // Let's not mess with about:blank.
+ null,
+
+ // about:profiles
+ function (aDocument, aTab) {
+ let win = aDocument.defaultView;
+ // Need a timeout to let the script run to create the needed buttons.
+ win.setTimeout(() => {
+ win.MozXULElement.insertFTLIfNeeded("messenger/aboutProfilesExtra.ftl");
+ for (let button of aDocument.querySelectorAll(
+ `[data-l10n-id="profiles-launch-profile"]`
+ )) {
+ win.document.l10n.setAttributes(
+ button,
+ "profiles-launch-profile-plain"
+ );
+ }
+ }, 500);
+ },
+
+ // Other about:* pages.
+ function (aDocument, aTab) {
+ // Provide context menu for about:* pages.
+ aTab.browser.setAttribute("context", "aboutPagesContext");
+ },
+ ],
+
+ shouldSwitchTo({ url, duplicate }) {
+ if (duplicate) {
+ return -1;
+ }
+
+ let tabmail = document.getElementById("tabmail");
+ let tabInfo = tabmail.tabInfo;
+ let uri;
+
+ try {
+ uri = Services.io.newURI(url);
+ } catch (ex) {
+ return -1;
+ }
+
+ for (
+ let selectedIndex = 0;
+ selectedIndex < tabInfo.length;
+ ++selectedIndex
+ ) {
+ // Reuse the same tab, if only the anchors differ - especially for the
+ // about: pages, we just want to re-use the same tab.
+ if (
+ tabInfo[selectedIndex].mode.name == this.name &&
+ tabInfo[selectedIndex].browser.currentURI?.specIgnoringRef ==
+ uri.specIgnoringRef
+ ) {
+ // Go to the correct location on the page, but only if it's not the
+ // current location. This should NOT cause the page to reload.
+ if (tabInfo[selectedIndex].browser.currentURI.spec != uri.spec) {
+ MailE10SUtils.loadURI(tabInfo[selectedIndex].browser, uri.spec);
+ }
+ return selectedIndex;
+ }
+ }
+ return -1;
+ },
+
+ closeTab(aTab) {
+ aTab.browser.removeEventListener(
+ "pagetitlechanged",
+ aTab.titleListener,
+ true
+ );
+ aTab.browser.removeEventListener(
+ "DOMWindowClose",
+ aTab.closeListener,
+ true
+ );
+ aTab.browser.removeEventListener("DOMLinkAdded", DOMLinkHandler);
+ aTab.browser.removeEventListener("DOMLinkChanged", DOMLinkHandler);
+ aTab.browser.webProgress.removeProgressListener(aTab.filter);
+ aTab.filter.removeProgressListener(aTab.progressListener);
+ aTab.browser.destroy();
+ },
+
+ saveTabState(aTab) {
+ aTab.browser.setAttribute("type", "content");
+ aTab.browser.removeAttribute("primary");
+ },
+
+ showTab(aTab) {
+ aTab.browser.setAttribute("type", "content");
+ aTab.browser.setAttribute("primary", "true");
+ if (aTab.browser.currentURI.spec.startsWith("about:preferences")) {
+ aTab.browser.contentDocument.documentElement.focus();
+ }
+ },
+
+ getBrowser(aTab) {
+ return aTab.browser;
+ },
+
+ _setUpLoadListener(aTab) {
+ let self = this;
+
+ function onLoad(aEvent) {
+ let doc = aEvent.target;
+ let url = doc.defaultView.location.href;
+
+ // If this document has an overlay defined, run it now.
+ let ind = self.inContentWhitelist.indexOf(url);
+ if (ind < 0) {
+ // Try a wildcard.
+ ind = self.inContentWhitelist.indexOf(url.replace(/:.*/, ":*"));
+ }
+ if (ind >= 0) {
+ let overlayFunction = self.inContentOverlays[ind];
+ if (overlayFunction) {
+ overlayFunction(doc, aTab);
+ }
+ }
+ }
+
+ aTab.loadListener = onLoad;
+ aTab.browser.addEventListener("load", aTab.loadListener, true);
+ },
+
+ // Internal function used to set up the title listener on a content tab.
+ _setUpTitleListener(aTab) {
+ function onDOMTitleChanged(aEvent) {
+ aTab.title = aTab.browser.contentTitle;
+ document.getElementById("tabmail").setTabTitle(aTab);
+ }
+ // Save the function we'll use as listener so we can remove it later.
+ aTab.titleListener = onDOMTitleChanged;
+ // Add the listener.
+ aTab.browser.addEventListener("pagetitlechanged", aTab.titleListener, true);
+ },
+
+ /**
+ * Internal function used to set up the close window listener on a content
+ * tab.
+ */
+ _setUpCloseWindowListener(aTab) {
+ function onDOMWindowClose(aEvent) {
+ if (!aEvent.isTrusted) {
+ return;
+ }
+
+ // Redirect any window.close events to closing the tab. As a 3-pane tab
+ // must be open, we don't need to worry about being the last tab open.
+ document.getElementById("tabmail").closeTab(aTab);
+ aEvent.preventDefault();
+ }
+ // Save the function we'll use as listener so we can remove it later.
+ aTab.closeListener = onDOMWindowClose;
+ // Add the listener.
+ aTab.browser.addEventListener("DOMWindowClose", aTab.closeListener, true);
+ },
+
+ supportsCommand(aCommand, aTab) {
+ switch (aCommand) {
+ case "cmd_fullZoomReduce":
+ case "cmd_fullZoomEnlarge":
+ case "cmd_fullZoomReset":
+ case "cmd_fullZoomToggle":
+ case "cmd_find":
+ case "cmd_findAgain":
+ case "cmd_findPrevious":
+ case "cmd_print":
+ case "button_print":
+ case "cmd_stop":
+ case "cmd_reload":
+ case "Browser:Back":
+ case "Browser:Forward":
+ return true;
+ default:
+ return false;
+ }
+ },
+
+ isCommandEnabled(aCommand, aTab) {
+ switch (aCommand) {
+ case "cmd_fullZoomReduce":
+ case "cmd_fullZoomEnlarge":
+ case "cmd_fullZoomReset":
+ case "cmd_fullZoomToggle":
+ case "cmd_find":
+ case "cmd_findAgain":
+ case "cmd_findPrevious":
+ return true;
+ case "cmd_print":
+ case "button_print": {
+ let uri = aTab.browser?.currentURI;
+ if (!uri || !uri.schemeIs("about")) {
+ return true;
+ }
+ return [
+ "addressbook",
+ "certificate",
+ "crashes",
+ "credits",
+ "license",
+ "profiles",
+ "support",
+ "telemetry",
+ ].includes(uri.filePath);
+ }
+ case "cmd_reload":
+ return aTab.reloadEnabled;
+ case "cmd_stop":
+ return aTab.busy;
+ case "Browser:Back":
+ return aTab.browser?.canGoBack;
+ case "Browser:Forward":
+ return aTab.browser?.canGoForward;
+ default:
+ return false;
+ }
+ },
+
+ doCommand(aCommand, aTab) {
+ switch (aCommand) {
+ case "cmd_fullZoomReduce":
+ ZoomManager.reduce();
+ break;
+ case "cmd_fullZoomEnlarge":
+ ZoomManager.enlarge();
+ break;
+ case "cmd_fullZoomReset":
+ ZoomManager.reset();
+ break;
+ case "cmd_fullZoomToggle":
+ ZoomManager.toggleZoom();
+ break;
+ case "cmd_find":
+ aTab.findbar.onFindCommand();
+ break;
+ case "cmd_findAgain":
+ aTab.findbar.onFindAgainCommand(false);
+ break;
+ case "cmd_findPrevious":
+ aTab.findbar.onFindAgainCommand(true);
+ break;
+ case "cmd_print":
+ PrintUtils.startPrintWindow(this.getBrowser(aTab).browsingContext, {});
+ break;
+ case "cmd_stop":
+ aTab.browser.stop();
+ break;
+ case "cmd_reload":
+ aTab.browser.reload();
+ break;
+ case "Browser:Back":
+ specialTabs.browserBack();
+ break;
+ case "Browser:Forward":
+ specialTabs.browserForward();
+ break;
+ }
+ },
+};
+
+/**
+ * Class that wraps the content page loading/security icon.
+ */
+// Ideally, this could be moved into a sub-class for content tabs.
+class SecurityIcon {
+ constructor(icon) {
+ this.icon = icon;
+ this.loading = false;
+ this.securityLevel = "";
+ this.updateIcon();
+ }
+
+ /**
+ * Set whether the page is loading.
+ *
+ * @param {boolean} loading - Whether the page is loading.
+ */
+ setLoading(loading) {
+ if (this.loading !== loading) {
+ this.loading = loading;
+ this.updateIcon();
+ }
+ }
+
+ /**
+ * Set the security level of the page.
+ *
+ * @param {"high"|"broken"|""} - The security level for the page, or empty if
+ * it is to be ignored.
+ */
+ setSecurityLevel(securityLevel) {
+ if (this.securityLevel !== securityLevel) {
+ this.securityLevel = securityLevel;
+ this.updateIcon();
+ }
+ }
+
+ updateIcon() {
+ let src;
+ let srcSet;
+ let l10nId;
+ let secure = false;
+ if (this.loading) {
+ src = "chrome://global/skin/icons/loading.png";
+ srcSet = "chrome://global/skin/icons/loading@2x.png 2x";
+ l10nId = "content-tab-page-loading-icon";
+ } else {
+ switch (this.securityLevel) {
+ case "high":
+ secure = true;
+ src = "chrome://messenger/skin/icons/connection-secure.svg";
+ l10nId = "content-tab-security-high-icon";
+ break;
+ case "broken":
+ src = "chrome://messenger/skin/icons/connection-insecure.svg";
+ l10nId = "content-tab-security-broken-icon";
+ break;
+ }
+ }
+ if (srcSet) {
+ this.icon.setAttribute("srcset", srcSet);
+ } else {
+ this.icon.removeAttribute("srcset");
+ }
+ if (src) {
+ this.icon.setAttribute("src", src);
+ // Set alt.
+ document.l10n.setAttributes(this.icon, l10nId);
+ } else {
+ this.icon.removeAttribute("src");
+ this.icon.removeAttribute("data-l10n-id");
+ this.icon.removeAttribute("alt");
+ }
+ this.icon.classList.toggle("secure-connection-icon", secure);
+ }
+}
+
+var specialTabs = {
+ _kAboutRightsVersion: 1,
+ get _protocolSvc() {
+ delete this._protocolSvc;
+ return (this._protocolSvc = Cc[
+ "@mozilla.org/uriloader/external-protocol-service;1"
+ ].getService(Ci.nsIExternalProtocolService));
+ },
+
+ get msgNotificationBar() {
+ if (!this._notificationBox) {
+ this._notificationBox = new MozElements.NotificationBox(element => {
+ element.setAttribute("notificationside", "bottom");
+ document
+ .getElementById("messenger-notification-bottom")
+ .append(element);
+ });
+ }
+ return this._notificationBox;
+ },
+
+ // This will open any special tabs if necessary on startup.
+ openSpecialTabsOnStartup() {
+ let tabmail = document.getElementById("tabmail");
+
+ tabmail.registerTabType(this.contentTabType);
+
+ this.showWhatsNewPage();
+
+ // Show the about rights notification if we need to.
+ if (this.shouldShowAboutRightsNotification()) {
+ this.showAboutRightsNotification();
+ }
+ if (this.shouldShowPolicyNotification()) {
+ // Do it on a timeout to workaround that open in background do not work when called too early.
+ setTimeout(this.showPolicyNotification, 10000);
+ }
+ },
+
+ /**
+ * A tab to show content pages.
+ */
+ contentTabType: {
+ __proto__: contentTabBaseType,
+ name: "contentTab",
+ perTabPanel: "vbox",
+ lastBrowserId: 0,
+ get loadingTabString() {
+ delete this.loadingTabString;
+ return (this.loadingTabString = document
+ .getElementById("bundle_messenger")
+ .getString("loadingTab"));
+ },
+
+ modes: {
+ contentTab: {
+ type: "contentTab",
+ },
+ },
+
+ /**
+ * This is the internal function used by content tabs to open a new tab. To
+ * open a contentTab, use specialTabs.openTab("contentTab", aArgs)
+ *
+ * @param {object} aArgs - The options that content tabs accept.
+ * @param {string} aArgs.url - The URL that is to be opened
+ * @param {nsIOpenWindowInfo} [aArgs.openWindowInfo] - The opener window
+ * @param {"single-site"|"single-page"|null} [aArgs.linkHandler="single-site"]
+ * Restricts navigation in the browser to be opened:
+ * - "single-site" allows only URLs in the same domain as
+ * aArgs.url (including subdomains).
+ * - "single-page" allows only URLs matching aArgs.url.
+ * - `null` applies no such restrictions.
+ * All other links are sent to an external browser.
+ * @param {Function} [aArgs.onLoad] - A function that takes an Event and a
+ * DOMNode. It is called when the content page is done loading. The
+ * first argument is the load event, and the second argument is the
+ * xul:browser that holds the page. You can access the inner tab's
+ * window object by accessing the second parameter's contentWindow
+ * property.
+ */
+ openTab(aTab, aArgs) {
+ if (!("url" in aArgs)) {
+ throw new Error("url must be specified");
+ }
+
+ // First clone the page and set up the basics.
+ let clone = document
+ .getElementById("contentTab")
+ .firstElementChild.cloneNode(true);
+
+ clone.setAttribute("id", "contentTab" + this.lastBrowserId);
+ clone.setAttribute("collapsed", false);
+
+ let toolbox = clone.firstElementChild;
+ toolbox.setAttribute("id", "contentTabToolbox" + this.lastBrowserId);
+ toolbox.firstElementChild.setAttribute(
+ "id",
+ "contentTabToolbar" + this.lastBrowserId
+ );
+
+ aTab.linkedBrowser = aTab.browser = document.createXULElement("browser");
+ aTab.browser.setAttribute("id", "contentTabBrowser" + this.lastBrowserId);
+ aTab.browser.setAttribute("type", "content");
+ aTab.browser.setAttribute("flex", "1");
+ aTab.browser.setAttribute("autocompletepopup", "PopupAutoComplete");
+ aTab.browser.setAttribute("context", "browserContext");
+ aTab.browser.setAttribute("maychangeremoteness", "true");
+ aTab.browser.setAttribute("onclick", "return contentAreaClick(event);");
+ aTab.browser.openWindowInfo = aArgs.openWindowInfo || null;
+ clone.querySelector("stack").appendChild(aTab.browser);
+
+ if (aArgs.skipLoad) {
+ clone.querySelector("browser").setAttribute("nodefaultsrc", "true");
+ }
+ if (aArgs.userContextId) {
+ aTab.browser.setAttribute("usercontextid", aArgs.userContextId);
+ }
+ aTab.panel.setAttribute("id", "contentTabWrapper" + this.lastBrowserId);
+ aTab.panel.appendChild(clone);
+ aTab.root = clone;
+
+ ExtensionParent.apiManager.emit(
+ "extension-browser-inserted",
+ aTab.browser
+ );
+
+ // For pdf.js use the aboutPagesContext context menu.
+ if (aArgs.url.includes("type=application/pdf")) {
+ aTab.browser.setAttribute("context", "aboutPagesContext");
+ }
+
+ // Start setting up the browser.
+ aTab.toolbar = aTab.panel.querySelector(".contentTabToolbar");
+ aTab.backButton = aTab.toolbar.querySelector(".back-btn");
+ aTab.backButton.addEventListener("command", () => aTab.browser.goBack());
+ aTab.forwardButton = aTab.toolbar.querySelector(".forward-btn");
+ aTab.forwardButton.addEventListener("command", () =>
+ aTab.browser.goForward()
+ );
+ aTab.securityIcon = new SecurityIcon(
+ aTab.toolbar.querySelector(".contentTabSecurity")
+ );
+ aTab.urlbar = aTab.toolbar.querySelector(".contentTabUrlInput");
+ aTab.urlbar.value = aArgs.url;
+
+ // As we're opening this tab, showTab may not get called, so set
+ // the type according to if we're opening in background or not.
+ let background = "background" in aArgs && aArgs.background;
+ if (background) {
+ aTab.browser.removeAttribute("primary");
+ } else {
+ aTab.browser.setAttribute("primary", "true");
+ }
+
+ if (aArgs.linkHandler == "single-page") {
+ aTab.browser.setAttribute("messagemanagergroup", "single-page");
+ } else if (aArgs.linkHandler === null) {
+ aTab.browser.setAttribute("messagemanagergroup", "browsers");
+ } else {
+ aTab.browser.setAttribute("messagemanagergroup", "single-site");
+ }
+
+ aTab.browser.addEventListener("DOMLinkAdded", DOMLinkHandler);
+ aTab.browser.addEventListener("DOMLinkChanged", DOMLinkHandler);
+
+ // Now initialise the find bar.
+ aTab.findbar = document.createXULElement("findbar");
+ aTab.findbar.setAttribute(
+ "browserid",
+ "contentTabBrowser" + this.lastBrowserId
+ );
+ clone.appendChild(aTab.findbar);
+
+ // Default to reload being disabled.
+ aTab.reloadEnabled = false;
+
+ // Now set up the listeners.
+ this._setUpLoadListener(aTab);
+ this._setUpTitleListener(aTab);
+ this._setUpCloseWindowListener(aTab);
+
+ /**
+ * Override the browser custom element's version, which returns gBrowser.
+ */
+ aTab.browser.getTabBrowser = function () {
+ return document.getElementById("tabmail");
+ };
+
+ if ("onLoad" in aArgs) {
+ aTab.browser.addEventListener(
+ "load",
+ function _contentTab_onLoad(event) {
+ aArgs.onLoad(event, aTab.browser);
+ aTab.browser.removeEventListener("load", _contentTab_onLoad, true);
+ },
+ true
+ );
+ }
+
+ // Create a filter and hook it up to our browser
+ let filter = Cc[
+ "@mozilla.org/appshell/component/browser-status-filter;1"
+ ].createInstance(Ci.nsIWebProgress);
+ aTab.filter = filter;
+ aTab.browser.webProgress.addProgressListener(
+ filter,
+ Ci.nsIWebProgress.NOTIFY_ALL
+ );
+
+ // Wire up a progress listener to the filter for this browser
+ aTab.progressListener = new tabProgressListener(aTab, false);
+
+ filter.addProgressListener(
+ aTab.progressListener,
+ Ci.nsIWebProgress.NOTIFY_ALL
+ );
+
+ if ("onListener" in aArgs) {
+ aArgs.onListener(aTab.browser, aTab.progressListener);
+ }
+
+ // Initialize our unit testing variables.
+ aTab.pageLoading = false;
+ aTab.pageLoaded = false;
+
+ // Now start loading the content.
+ aTab.title = this.loadingTabString;
+
+ if (!aArgs.skipLoad) {
+ MailE10SUtils.loadURI(aTab.browser, aArgs.url, {
+ csp: aArgs.csp,
+ referrerInfo: aArgs.referrerInfo,
+ triggeringPrincipal: aArgs.triggeringPrincipal,
+ });
+ }
+
+ this.lastBrowserId++;
+ },
+ tryCloseTab(aTab) {
+ return aTab.browser.permitUnload();
+ },
+ persistTab(aTab) {
+ if (aTab.browser.currentURI.spec == "about:blank") {
+ return null;
+ }
+
+ // Extension pages of temporarily installed extensions cannot be restored.
+ if (
+ aTab.browser.currentURI.scheme == "moz-extension" &&
+ WebExtensionPolicy.getByHostname(aTab.browser.currentURI.host)
+ ?.temporarilyInstalled
+ ) {
+ return null;
+ }
+
+ return {
+ tabURI: aTab.browser.currentURI.spec,
+ linkHandler: aTab.browser.getAttribute("messagemanagergroup"),
+ userContextId: `${
+ aTab.browser.getAttribute("usercontextid") ||
+ Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID
+ }`,
+ };
+ },
+ restoreTab(aTabmail, aPersistedState) {
+ let tab = aTabmail.openTab("contentTab", {
+ background: true,
+ duplicate: aPersistedState.duplicate,
+ linkHandler: aPersistedState.linkHandler,
+ url: aPersistedState.tabURI,
+ userContextId: aPersistedState.userContextId,
+ });
+
+ if (aPersistedState.tabURI == "about:addons") {
+ // Also in `openAddonsMgr` in mailCore.js.
+ tab.browser.droppedLinkHandler = event =>
+ tab.browser.contentWindow.gDragDrop.onDrop(event);
+ }
+ },
+ },
+
+ /**
+ * Shows the what's new page in the system browser if we should.
+ * Will update the mstone pref to a new version if needed.
+ *
+ * @see {BrowserContentHandler.needHomepageOverride}
+ */
+ showWhatsNewPage() {
+ let old_mstone = Services.prefs.getCharPref(
+ "mailnews.start_page_override.mstone",
+ ""
+ );
+
+ let mstone = Services.appinfo.version;
+ if (mstone != old_mstone) {
+ Services.prefs.setCharPref("mailnews.start_page_override.mstone", mstone);
+ }
+
+ if (AppConstants.MOZ_UPDATER) {
+ let update = Cc["@mozilla.org/updates/update-manager;1"].getService(
+ Ci.nsIUpdateManager
+ ).readyUpdate;
+
+ if (update && Services.vc.compare(update.appVersion, old_mstone) > 0) {
+ let overridePage = Services.urlFormatter.formatURLPref(
+ "mailnews.start_page.override_url"
+ );
+ overridePage = this.getPostUpdateOverridePage(update, overridePage);
+ overridePage = overridePage.replace("%OLD_VERSION%", old_mstone);
+ if (overridePage) {
+ openLinkExternally(overridePage);
+ }
+ }
+ }
+ },
+
+ /**
+ * Gets the override page for the first run after the application has been
+ * updated.
+ *
+ * @param {nsIUpdate} update - The nsIUpdate for the update that has been applied.
+ * @param {string} defaultOverridePage - The default override page.
+ * @returns {string} The override page.
+ */
+ getPostUpdateOverridePage(update, defaultOverridePage) {
+ update = update.QueryInterface(Ci.nsIWritablePropertyBag);
+ let actions = update.getProperty("actions");
+ // When the update doesn't specify actions fallback to the original behavior
+ // of displaying the default override page.
+ if (!actions) {
+ return defaultOverridePage;
+ }
+
+ // The existence of silent or the non-existence of showURL in the actions both
+ // mean that an override page should not be displayed.
+ if (actions.includes("silent") || !actions.includes("showURL")) {
+ return "";
+ }
+
+ // If a policy was set to not allow the update.xml-provided
+ // URL to be used, use the default fallback (which will also
+ // be provided by the policy).
+ if (!Services.policies.isAllowed("postUpdateCustomPage")) {
+ return defaultOverridePage;
+ }
+
+ return update.getProperty("openURL") || defaultOverridePage;
+ },
+
+ /**
+ * Looks at the existing prefs and determines if we should show the policy or not.
+ */
+ shouldShowPolicyNotification() {
+ let dataSubmissionEnabled = Services.prefs.getBoolPref(
+ "datareporting.policy.dataSubmissionEnabled",
+ true
+ );
+ let dataSubmissionPolicyBypassNotification = Services.prefs.getBoolPref(
+ "datareporting.policy.dataSubmissionPolicyBypassNotification",
+ false
+ );
+ let dataSubmissionPolicyAcceptedVersion = Services.prefs.getIntPref(
+ "datareporting.policy.dataSubmissionPolicyAcceptedVersion",
+ 0
+ );
+ let currentPolicyVersion = Services.prefs.getIntPref(
+ "datareporting.policy.currentPolicyVersion",
+ 1
+ );
+ if (
+ !AppConstants.MOZ_DATA_REPORTING ||
+ !dataSubmissionEnabled ||
+ dataSubmissionPolicyBypassNotification
+ ) {
+ return false;
+ }
+ if (dataSubmissionPolicyAcceptedVersion >= currentPolicyVersion) {
+ return false;
+ }
+ return true;
+ },
+
+ showPolicyNotification() {
+ try {
+ let firstRunURL = Services.prefs.getStringPref(
+ "datareporting.policy.firstRunURL"
+ );
+ document.getElementById("tabmail").openTab("contentTab", {
+ background: true,
+ url: firstRunURL,
+ });
+ } catch (e) {
+ // Show the infobar if it fails to show the privacy policy in the new tab.
+ this.showTelemetryNotification();
+ }
+ let currentPolicyVersion = Services.prefs.getIntPref(
+ "datareporting.policy.currentPolicyVersion",
+ 1
+ );
+ Services.prefs.setIntPref(
+ "datareporting.policy.dataSubmissionPolicyAcceptedVersion",
+ currentPolicyVersion
+ );
+ Services.prefs.setStringPref(
+ "datareporting.policy.dataSubmissionPolicyNotifiedTime",
+ new Date().getTime().toString()
+ );
+ },
+
+ showTelemetryNotification() {
+ let brandBundle = Services.strings.createBundle(
+ "chrome://branding/locale/brand.properties"
+ );
+ let telemetryBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/telemetry.properties"
+ );
+
+ let productName = brandBundle.GetStringFromName("brandFullName");
+ let serverOwner = Services.prefs.getCharPref(
+ "toolkit.telemetry.server_owner"
+ );
+ let telemetryText = telemetryBundle.formatStringFromName("telemetryText", [
+ productName,
+ serverOwner,
+ ]);
+
+ // TODO: sync up this bar with Firefox:
+ // https://searchfox.org/mozilla-central/rev/227f22acef5c4865503bde9f835452bf38332c8e/browser/locales/en-US/chrome/browser/browser.properties#697-698
+ let buttons = [
+ {
+ label: telemetryBundle.GetStringFromName("telemetryLinkLabel"),
+ popup: null,
+ callback: () => {
+ openOptionsDialog("panePrivacy", "privacyDataCollectionCategory");
+ },
+ },
+ ];
+
+ let notification = this.msgNotificationBar.appendNotification(
+ "telemetry",
+ {
+ label: telemetryText,
+ priority: this.msgNotificationBar.PRIORITY_INFO_LOW,
+ },
+ buttons
+ );
+ // Arbitrary number, just so bar sticks around for a bit.
+ notification.persistence = 3;
+ },
+
+ /**
+ * Looks at the existing prefs and determines if we should show about:rights
+ * or not.
+ *
+ * This is controlled by two prefs:
+ *
+ * mail.rights.override
+ * If this pref is set to false, always show the about:rights
+ * notification.
+ * If this pref is set to true, never show the about:rights notification.
+ * If the pref doesn't exist, then we fallback to checking
+ * mail.rights.version.
+ *
+ * mail.rights.version
+ * If this pref isn't set or the value is less than the current version
+ * then we show the about:rights notification.
+ */
+ shouldShowAboutRightsNotification() {
+ try {
+ return !Services.prefs.getBoolPref("mail.rights.override");
+ } catch (e) {}
+
+ return (
+ Services.prefs.getIntPref("mail.rights.version") <
+ this._kAboutRightsVersion
+ );
+ },
+
+ async showAboutRightsNotification() {
+ var rightsBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/aboutRights.properties"
+ );
+
+ var buttons = [
+ {
+ label: rightsBundle.GetStringFromName("buttonLabel"),
+ accessKey: rightsBundle.GetStringFromName("buttonAccessKey"),
+ popup: null,
+ callback(aNotificationBar, aButton) {
+ // Show the about:rights tab
+ document.getElementById("tabmail").openTab("contentTab", {
+ url: "about:rights",
+ });
+ },
+ },
+ ];
+
+ let notifyRightsText = await document.l10n.formatValue(
+ "about-rights-notification-text"
+ );
+ let notification = this.msgNotificationBar.appendNotification(
+ "about-rights",
+ {
+ label: notifyRightsText,
+ priority: this.msgNotificationBar.PRIORITY_INFO_LOW,
+ },
+ buttons
+ );
+ // Arbitrary number, just so bar sticks around for a bit.
+ notification.persistence = 3;
+
+ // Set the pref to say we've displayed the notification.
+ Services.prefs.setIntPref("mail.rights.version", this._kAboutRightsVersion);
+ },
+
+ /**
+ * Determine if we should load fav icons or not.
+ *
+ * @param aURI An nsIURI containing the current url.
+ */
+ _shouldLoadFavIcon(aURI) {
+ return (
+ aURI &&
+ Services.prefs.getBoolPref("browser.chrome.site_icons") &&
+ Services.prefs.getBoolPref("browser.chrome.favicons") &&
+ "schemeIs" in aURI &&
+ (aURI.schemeIs("http") || aURI.schemeIs("https"))
+ );
+ },
+
+ /**
+ * Tries to use the default favicon for a webpage for the specified tab.
+ * We'll use the site's favicon.ico if prefs allow us to.
+ */
+ useDefaultFavIcon(aTab) {
+ // Use documentURI in the check for shouldLoadFavIcon so that we do the
+ // right thing with about:-style error pages.
+ let docURIObject = aTab.browser.documentURI;
+ let icon = null;
+ if (this._shouldLoadFavIcon(docURIObject)) {
+ icon = docURIObject.prePath + "/favicon.ico";
+ }
+
+ this.setFavIcon(aTab, icon);
+ },
+
+ /**
+ * This sets the specified tab to load and display the given icon for the
+ * page shown in the browser. It is assumed that the preferences have already
+ * been checked before calling this function appropriately.
+ *
+ * @param aTab The tab to set the icon for.
+ * @param aIcon A string based URL of the icon to try and load.
+ */
+ setFavIcon(aTab, aIcon) {
+ if (aIcon) {
+ PlacesUtils.favicons.setAndFetchFaviconForPage(
+ aTab.browser.currentURI,
+ Services.io.newURI(aIcon),
+ false,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ null,
+ aTab.browser.contentPrincipal
+ );
+ }
+ document
+ .getElementById("tabmail")
+ .setTabFavIcon(
+ aTab,
+ aIcon,
+ "chrome://messenger/skin/icons/new/compact/draft.svg"
+ );
+ },
+
+ browserForward() {
+ let tabmail = document.getElementById("tabmail");
+ if (
+ !["contentTab", "mail3PaneTab"].includes(
+ tabmail?.currentTabInfo.mode.name
+ )
+ ) {
+ return;
+ }
+ let browser = tabmail.getBrowserForSelectedTab();
+ if (!browser) {
+ return;
+ }
+ if (browser.webNavigation) {
+ browser.webNavigation.goForward();
+ }
+ },
+
+ browserBack() {
+ let tabmail = document.getElementById("tabmail");
+ if (
+ !["contentTab", "mail3PaneTab"].includes(
+ tabmail?.currentTabInfo.mode.name
+ )
+ ) {
+ return;
+ }
+ let browser = tabmail.getBrowserForSelectedTab();
+ if (!browser) {
+ return;
+ }
+ if (browser.webNavigation) {
+ browser.webNavigation.goBack();
+ }
+ },
+};