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:
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;
+}