tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 4083a78ab5313b17512d3086c6e1eacdf524d995
parent 69d2d8755f9faec7d0d4b83c20209acd9c2105e7
Author: Dan Ballard <dan@mindstab.net>
Date:   Fri, 31 Mar 2023 12:35:17 -0500

TB 40701: Add security warning when downloading a file

Shown in the downloads panel, about:downloads and places.xhtml.

Diffstat:
Abrowser/components/downloads/DownloadsTorWarning.sys.mjs | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/downloads/content/contentAreaDownloadsView.js | 40+++++++++++++++++++++++++++++++++++++---
Mbrowser/components/downloads/content/contentAreaDownloadsView.xhtml | 3+++
Mbrowser/components/downloads/content/downloads.css | 11+++++++++++
Mbrowser/components/downloads/content/downloads.js | 62+++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mbrowser/components/downloads/content/downloadsPanel.inc.xhtml | 19+++++++++++++++++++
Mbrowser/components/downloads/moz.build | 1+
Mbrowser/components/places/content/places.css | 4++++
Mbrowser/components/places/content/places.js | 30+++++++++++++++++++++++++++++-
Mbrowser/components/places/content/places.xhtml | 3+++
Mbrowser/themes/shared/downloads/contentAreaDownloadsView.css | 4++++
11 files changed, 314 insertions(+), 15 deletions(-)

diff --git a/browser/components/downloads/DownloadsTorWarning.sys.mjs b/browser/components/downloads/DownloadsTorWarning.sys.mjs @@ -0,0 +1,152 @@ +/* import-globals-from /browser/base/content/utilityOverlay.js */ + +const PREF_SHOW_DOWNLOAD_WARNING = "browser.download.showTorWarning"; + +/** + * Manages an instance of a tor warning. + */ +export class DownloadsTorWarning { + /** + * Observer for showing or hiding the warning. + * + * @type {function} + */ + #torWarningPrefObserver; + + /** + * Whether the warning is active. + * + * @type {boolean} + */ + #active = false; + + /** + * The moz-message-bar element that should show the warning. + * + * @type {MozMessageBar} + */ + #warningElement; + + /** + * The dismiss button for the warning. + * + * @type {HTMLButton} + */ + #dismissButton; + + /** + * Attach to an instance of the tor warning. + * + * @param {MozMessageBar} warningElement - The warning element to initialize + * and attach to. + * @param {boolean} isChrome - Whether the element belongs to the chrome. + * Otherwise it belongs to content. + * @param {function} moveFocus - Callback to move the focus out of the warning + * when it is hidden. + * @param {function} [onLinkClick] - Callback that is called when a link is + * about to open. + */ + constructor(warningElement, isChrome, moveFocus, onLinkClick) { + const doc = warningElement.ownerDocument; + this.#warningElement = warningElement; + warningElement.setAttribute( + "data-l10n-id", + "downloads-tor-warning-message-bar" + ); + warningElement.setAttribute("data-l10n-attrs", "heading, message"); + + // Observe changes to the tor warning pref. + this.#torWarningPrefObserver = () => { + if (Services.prefs.getBoolPref(PREF_SHOW_DOWNLOAD_WARNING)) { + warningElement.hidden = false; + } else { + const hadFocus = warningElement.contains(doc.activeElement); + warningElement.hidden = true; + if (hadFocus) { + moveFocus(); + } + } + }; + + const tailsLink = doc.createElement("a"); + tailsLink.setAttribute("slot", "support-link"); + tailsLink.href = "https://tails.net/"; + tailsLink.target = "_blank"; + tailsLink.setAttribute("data-l10n-id", "downloads-tor-warning-tails-link"); + if (isChrome) { + // Intercept clicks on the tails link. + tailsLink.addEventListener("click", event => { + event.preventDefault(); + onLinkClick?.(); + doc.defaultView.openWebLinkIn(tailsLink.href, "tab"); + }); + } + + const dismissButton = doc.createElement("button"); + dismissButton.setAttribute("slot", "actions"); + dismissButton.setAttribute( + "data-l10n-id", + "downloads-tor-warning-dismiss-button" + ); + if (isChrome) { + dismissButton.classList.add("footer-button"); + } + + dismissButton.addEventListener("click", () => { + Services.prefs.setBoolPref(PREF_SHOW_DOWNLOAD_WARNING, false); + }); + + warningElement.append(tailsLink); + warningElement.append(dismissButton); + + this.#dismissButton = dismissButton; + } + + /** + * Whether the warning is hidden by the preference. + * + * @type {boolean} + */ + get hidden() { + return this.#warningElement.hidden; + } + + /** + * The dismiss button for the warning. + * + * @type {HTMLButton} + */ + get dismissButton() { + return this.#dismissButton; + } + + /** + * Activate the instance. + */ + activate() { + if (this.#active) { + return; + } + this.#active = true; + Services.prefs.addObserver( + PREF_SHOW_DOWNLOAD_WARNING, + this.#torWarningPrefObserver + ); + // Initialize. + this.#torWarningPrefObserver(); + } + + /** + * Deactivate the instance. + */ + deactivate() { + if (!this.#active) { + return; + } + this.#active = false; + Services.prefs.removeObserver( + PREF_SHOW_DOWNLOAD_WARNING, + this.#torWarningPrefObserver + ); + } +} diff --git a/browser/components/downloads/content/contentAreaDownloadsView.js b/browser/components/downloads/content/contentAreaDownloadsView.js @@ -8,18 +8,52 @@ const { PrivateBrowsingUtils } = ChromeUtils.importESModule( "resource://gre/modules/PrivateBrowsingUtils.sys.mjs" ); +const { DownloadsTorWarning } = ChromeUtils.importESModule( + "moz-src:///browser/components/downloads/DownloadsTorWarning.sys.mjs" +); + var ContentAreaDownloadsView = { init() { let box = document.getElementById("downloadsListBox"); + + const torWarning = new DownloadsTorWarning( + document.getElementById("aboutDownloadsTorWarning"), + false, + () => { + // Try and focus the downloads list. + // NOTE: If #downloadsListBox is still hidden, this will do nothing. + // But in this case there are no other focusable targets within the + // view, so we just leave it up to the focus handler. + box.focus({ preventFocusRing: true }); + } + ); + torWarning.activate(); + window.addEventListener("unload", () => { + torWarning.deactivate(); + }); + let suppressionFlag = DownloadsCommon.SUPPRESS_CONTENT_AREA_DOWNLOADS_OPEN; box.addEventListener( "InitialDownloadsLoaded", () => { // Set focus to Downloads list once it is created // And prevent it from showing the focus ring around the richlistbox (Bug 1702694) - document - .getElementById("downloadsListBox") - .focus({ focusVisible: false }); + // Prevent focusing the list whilst the tor browser warning is shown. + // Some screen readers (tested with Orca and NVDA) will not read out + // alerts if they are already present on page load. In that case, a + // screen reader user may not be aware of the warning before they + // interact with the downloads list, which we do not want. + // Some hacky workarounds were tested with Orca to get it to read back + // the alert before the focus is read, but this was inconsistent and the + // experience was bad. + // Without auto-focusing the downloads list, a screen reader should not + // skip beyond the alert's content. + if (torWarning.hidden) { + document + .getElementById("downloadsListBox") + .focus({ focusVisible: false }); + } + // Pause the indicator if the browser is active. if (document.visibilityState === "visible") { DownloadsCommon.getIndicatorData(window).attentionSuppressed |= diff --git a/browser/components/downloads/content/contentAreaDownloadsView.xhtml b/browser/components/downloads/content/contentAreaDownloadsView.xhtml @@ -34,6 +34,7 @@ <html:link rel="localization" href="toolkit/global/textActions.ftl"/> <html:link rel="localization" href="browser/downloads.ftl" /> + <html:link rel="localization" href="toolkit/global/tor-browser.ftl" /> </linkset> <script src="chrome://global/content/globalOverlay.js"/> @@ -49,6 +50,8 @@ </keyset> #endif + <html:moz-message-bar id="aboutDownloadsTorWarning"></html:moz-message-bar> + <richlistbox flex="1" seltype="multiple" id="downloadsListBox" diff --git a/browser/components/downloads/content/downloads.css b/browser/components/downloads/content/downloads.css @@ -95,6 +95,17 @@ padding: 0.62em; } +#downloadsPanelTorWarning { + margin-block-end: var(--arrowpanel-menuitem-padding-block); +} + +#downloadsPanelTorWarningWrapper { + /* The wrapper element has its `width` attribute set by mozilla localisers. + * We want to ensure the element occupies the available width when the + * localiser width is smaller. See tor-browser#43312. */ + min-width: 100%; +} + #downloadsHistory, #downloadsFooterButtons { margin: 0; diff --git a/browser/components/downloads/content/downloads.js b/browser/components/downloads/content/downloads.js @@ -41,6 +41,8 @@ ChromeUtils.defineESModuleGetters(this, { NetUtil: "resource://gre/modules/NetUtil.sys.mjs", PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + DownloadsTorWarning: + "moz-src:///browser/components/downloads/DownloadsTorWarning.sys.mjs", }); const { Integration } = ChromeUtils.importESModule( @@ -75,6 +77,13 @@ var DownloadsPanel = { _waitingDataForOpen: false, /** + * Tracks whether to show the tor warning or not. + * + * @type {?DownloadsTorWarning} + */ + _torWarning: null, + + /** * Starts loading the download data in background, without opening the panel. * Use showPanel instead to load the data and open the panel at the same time. */ @@ -90,6 +99,21 @@ var DownloadsPanel = { ); } + if (!this._torWarning) { + this._torWarning = new DownloadsTorWarning( + document.getElementById("downloadsPanelTorWarning"), + true, + () => { + // Re-assign focus that was lost. + this._focusPanel(true); + }, + () => { + this.hidePanel(); + } + ); + } + this._torWarning.activate(); + if (this._initialized) { DownloadsCommon.log("DownloadsPanel is already initialized."); return; @@ -162,6 +186,8 @@ var DownloadsPanel = { DownloadIntegration.downloadSpamProtection.unregister(window); } + this._torWarning?.deactivate(); + this._initialized = false; DownloadsSummary.active = false; @@ -567,24 +593,38 @@ var DownloadsPanel = { /** * Move focus to the main element in the downloads panel, unless another * element in the panel is already focused. + * + * @param {bool} [forceFocus=false] - Whether to force move the focus. */ - _focusPanel() { - // We may be invoked while the panel is still waiting to be shown. - if (this.panel.state != "open") { - return; - } + _focusPanel(forceFocus = false) { + if (!forceFocus) { + // We may be invoked while the panel is still waiting to be shown. + if (this.panel.state != "open") { + return; + } - if ( - document.activeElement && - (this.panel.contains(document.activeElement) || - this.panel.shadowRoot.contains(document.activeElement)) - ) { - return; + if ( + document.activeElement && + (this.panel.contains(document.activeElement) || + this.panel.shadowRoot.contains(document.activeElement)) + ) { + return; + } } + let focusOptions = {}; if (this._preventFocusRing) { focusOptions.focusVisible = false; } + + // Focus the "Got it" button if it is visible. + // This should ensure that the alert is read aloud by Orca when the + // downloads panel is opened. See tor-browser#42642. + if (!this._torWarning?.hidden) { + this._torWarning.dismissButton.focus(focusOptions); + return; + } + if (DownloadsView.richListBox.itemCount > 0) { if (DownloadsView.canChangeSelectedItem) { DownloadsView.richListBox.selectedIndex = 0; diff --git a/browser/components/downloads/content/downloadsPanel.inc.xhtml b/browser/components/downloads/content/downloadsPanel.inc.xhtml @@ -103,6 +103,25 @@ disablekeynav="true"> <panelview id="downloadsPanel-mainView"> + <!-- We add a wrapper around the #downloadsPanelTorWarning and give it the + - same Fluent ID as #downloadsListBox. This Fluent message allows + - Firefox localisers to set the width of the #downloadsListBox using + - the style attribute. We want the same width set for our downloads + - warning. Otherwise the warning will occupy its max-content width. + - NOTE: We require a wrapper element since #downloadsPanelTorWarning + - needs its own Fluent attributes. + - NOTE: This only works if #downloadsPanelTorWarningWrapper and + - #downloadsListBox share the same padding relative to their common + - ancestor. + - See tor-browser#43312. --> + <html:div + id="downloadsPanelTorWarningWrapper" + data-l10n-id="downloads-panel-items" + data-l10n-attrs="style" + > + <html:moz-message-bar id="downloadsPanelTorWarning"> + </html:moz-message-bar> + </html:div> <vbox class="panel-view-body-unscrollable"> <richlistbox id="downloadsListBox" data-l10n-id="downloads-panel-items" diff --git a/browser/components/downloads/moz.build b/browser/components/downloads/moz.build @@ -15,6 +15,7 @@ MOZ_SRC_FILES += [ "DownloadsCommon.sys.mjs", "DownloadSpamProtection.sys.mjs", "DownloadsTaskbar.sys.mjs", + "DownloadsTorWarning.sys.mjs", "DownloadsViewableInternally.sys.mjs", "DownloadsViewUI.sys.mjs", ] diff --git a/browser/components/places/content/places.css b/browser/components/places/content/places.css @@ -40,3 +40,7 @@ tree[is="places-tree"] > treechildren::-moz-tree-cell { .places-tooltip-box { display: block; } + +#placesDownloadsTorWarning:not(.downloads-visible) { + display: none; +} diff --git a/browser/components/places/content/places.js b/browser/components/places/content/places.js @@ -17,6 +17,8 @@ ChromeUtils.defineESModuleGetters(this, { PlacesBackups: "resource://gre/modules/PlacesBackups.sys.mjs", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs", + DownloadsTorWarning: + "moz-src:///browser/components/downloads/DownloadsTorWarning.sys.mjs", }); XPCOMUtils.defineLazyScriptGetter( this, @@ -155,6 +157,20 @@ var PlacesOrganizer = { "&sort=" + Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING; + const torWarning = new DownloadsTorWarning( + document.getElementById("placesDownloadsTorWarning"), + true, + () => { + document + .getElementById("downloadsListBox") + .focus({ preventFocusRing: true }); + } + ); + torWarning.activate(); + window.addEventListener("unload", () => { + torWarning.deactivate(); + }); + ContentArea.setContentViewForQueryString( DOWNLOADS_QUERY, () => @@ -1514,9 +1530,21 @@ var ContentArea = { oldView.associatedElement.hidden = true; aNewView.associatedElement.hidden = false; + // Hide the Tor warning when not in the downloads view. + const isDownloads = aNewView.associatedElement.id === "downloadsListBox"; + const torWarningMessage = document.getElementById( + "placesDownloadsTorWarning" + ); + const torWarningLoosingFocus = + torWarningMessage.contains(document.activeElement) && !isDownloads; + torWarningMessage.classList.toggle("downloads-visible", isDownloads); + // If the content area inactivated view was focused, move focus // to the new view. - if (document.activeElement == oldView.associatedElement) { + if ( + document.activeElement == oldView.associatedElement || + torWarningLoosingFocus + ) { aNewView.associatedElement.focus(); } } diff --git a/browser/components/places/content/places.xhtml b/browser/components/places/content/places.xhtml @@ -63,6 +63,7 @@ <html:link rel="localization" href="browser/places.ftl"/> <html:link rel="localization" href="browser/downloads.ftl"/> <html:link rel="localization" href="browser/editBookmarkOverlay.ftl"/> + <html:link rel="localization" href="toolkit/global/tor-browser.ftl"/> </linkset> <script src="chrome://browser/content/places/places.js"/> @@ -319,6 +320,8 @@ </tree> <splitter collapse="none" persist="state"></splitter> <vbox id="contentView"> + <html:moz-message-bar id="placesDownloadsTorWarning"> + </html:moz-message-bar> <vbox id="placesViewsBox" flex="1"> <tree id="placeContent" class="placesTree" diff --git a/browser/themes/shared/downloads/contentAreaDownloadsView.css b/browser/themes/shared/downloads/contentAreaDownloadsView.css @@ -24,3 +24,7 @@ text-align: center; color: var(--text-color-deemphasized); } + +#aboutDownloadsTorWarning { + margin-block-end: 8px; +}