Commit b074431c authored by Dan Ballard's avatar Dan Ballard Committed by Pier Angelo Vendrame
Browse files

TB 40701: Add security warning when downloading a file

Shown in the downloads panel, about:downloads and places.xhtml.
parent 36cec1ad
Loading
Loading
Loading
Loading
+152 −0
Original line number Diff line number Diff line
/* 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
    );
  }
}
+37 −3
Original line number Diff line number Diff line
@@ -8,18 +8,52 @@ const { PrivateBrowsingUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/PrivateBrowsingUtils.sys.mjs"
);

const { DownloadsTorWarning } = ChromeUtils.importESModule(
  "resource:///modules/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)
        // 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 |=
+3 −0
Original line number Diff line number Diff line
@@ -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"
+11 −0
Original line number Diff line number Diff line
@@ -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;
+50 −11
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ ChromeUtils.defineESModuleGetters(this, {
  FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
  NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
  DownloadsTorWarning: "resource:///modules/DownloadsTorWarning.sys.mjs",
});

const { Integration } = ChromeUtils.importESModule(
@@ -73,6 +74,13 @@ var DownloadsPanel = {
  /** The panel will be shown as soon as data is available. */
  _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.
@@ -89,6 +97,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;
@@ -161,6 +184,8 @@ var DownloadsPanel = {
      DownloadIntegration.downloadSpamProtection.unregister(window);
    }

    this._torWarning?.deactivate();

    this._initialized = false;

    DownloadsSummary.active = false;
@@ -570,8 +595,11 @@ 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() {
  _focusPanel(forceFocus = false) {
    if (!forceFocus) {
      // We may be invoked while the panel is still waiting to be shown.
      if (this.panel.state != "open") {
        return;
@@ -584,10 +612,21 @@ var DownloadsPanel = {
      ) {
        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;
Loading