tor-browser

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

downloads.js (59057B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
      2 /* vim: set ts=2 et sw=2 tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 /**
      8 * Handles the Downloads panel user interface for each browser window.
      9 *
     10 * This file includes the following constructors and global objects:
     11 *
     12 * DownloadsPanel
     13 * Main entry point for the downloads panel interface.
     14 *
     15 * DownloadsView
     16 * Builds and updates the downloads list widget, responding to changes in the
     17 * download state and real-time data.  In addition, handles part of the user
     18 * interaction events raised by the downloads list widget.
     19 *
     20 * DownloadsViewItem
     21 * Builds and updates a single item in the downloads list widget, responding to
     22 * changes in the download state and real-time data, and handles the user
     23 * interaction events related to a single item in the downloads list widgets.
     24 *
     25 * DownloadsViewController
     26 * Handles part of the user interaction events raised by the downloads list
     27 * widget, in particular the "commands" that apply to multiple items, and
     28 * dispatches the commands that apply to individual items.
     29 */
     30 
     31 "use strict";
     32 
     33 var { XPCOMUtils } = ChromeUtils.importESModule(
     34  "resource://gre/modules/XPCOMUtils.sys.mjs"
     35 );
     36 
     37 ChromeUtils.defineESModuleGetters(this, {
     38  DownloadsViewUI:
     39    "moz-src:///browser/components/downloads/DownloadsViewUI.sys.mjs",
     40  FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
     41  NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
     42  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     43  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     44  DownloadsTorWarning:
     45    "moz-src:///browser/components/downloads/DownloadsTorWarning.sys.mjs",
     46 });
     47 
     48 const { Integration } = ChromeUtils.importESModule(
     49  "resource://gre/modules/Integration.sys.mjs"
     50 );
     51 
     52 /* global DownloadIntegration */
     53 Integration.downloads.defineESModuleGetter(
     54  this,
     55  "DownloadIntegration",
     56  "resource://gre/modules/DownloadIntegration.sys.mjs"
     57 );
     58 
     59 // DownloadsPanel
     60 
     61 /**
     62 * Main entry point for the downloads panel interface.
     63 */
     64 var DownloadsPanel = {
     65  // Initialization and termination
     66 
     67  /**
     68   * Timeout that re-enables previously disabled download items in the downloads panel
     69   * after some time has passed.
     70   */
     71  _delayTimeout: null,
     72 
     73  /** The panel is not linked to downloads data yet. */
     74  _initialized: false,
     75 
     76  /** The panel will be shown as soon as data is available. */
     77  _waitingDataForOpen: false,
     78 
     79  /**
     80   * Tracks whether to show the tor warning or not.
     81   *
     82   * @type {?DownloadsTorWarning}
     83   */
     84  _torWarning: null,
     85 
     86  /**
     87   * Starts loading the download data in background, without opening the panel.
     88   * Use showPanel instead to load the data and open the panel at the same time.
     89   */
     90  initialize() {
     91    DownloadsCommon.log(
     92      "Attempting to initialize DownloadsPanel for a window."
     93    );
     94 
     95    if (DownloadIntegration.downloadSpamProtection) {
     96      DownloadIntegration.downloadSpamProtection.register(
     97        DownloadsView,
     98        window
     99      );
    100    }
    101 
    102    if (!this._torWarning) {
    103      this._torWarning = new DownloadsTorWarning(
    104        document.getElementById("downloadsPanelTorWarning"),
    105        true,
    106        () => {
    107          // Re-assign focus that was lost.
    108          this._focusPanel(true);
    109        },
    110        () => {
    111          this.hidePanel();
    112        }
    113      );
    114    }
    115    this._torWarning.activate();
    116 
    117    if (this._initialized) {
    118      DownloadsCommon.log("DownloadsPanel is already initialized.");
    119      return;
    120    }
    121    this._initialized = true;
    122 
    123    window.addEventListener("unload", this.onWindowUnload);
    124    const downloadPanelCommands = document.getElementById(
    125      "downloadPanelCommands"
    126    );
    127    downloadPanelCommands.addEventListener("command", this);
    128    downloadPanelCommands.addEventListener("commandupdate", () => {
    129      goUpdateCommand("cmd_delete");
    130    });
    131 
    132    // Load and resume active downloads if required.  If there are downloads to
    133    // be shown in the panel, they will be loaded asynchronously.
    134    DownloadsCommon.initializeAllDataLinks();
    135 
    136    // Now that data loading has eventually started, load the required XUL
    137    // elements and initialize our views.
    138 
    139    this.panel.hidden = false;
    140    DownloadsViewController.initialize();
    141    DownloadsCommon.log("Attaching DownloadsView...");
    142    DownloadsCommon.getData(window).addView(DownloadsView);
    143    DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit).addView(
    144      DownloadsSummary
    145    );
    146 
    147    DownloadsCommon.log(
    148      "DownloadsView attached - the panel for this window",
    149      "should now see download items come in."
    150    );
    151    DownloadsPanel._attachEventListeners();
    152    DownloadsCommon.log("DownloadsPanel initialized.");
    153  },
    154 
    155  /**
    156   * Closes the downloads panel and frees the internal resources related to the
    157   * downloads.  The downloads panel can be reopened later, even after this
    158   * function has been called.
    159   */
    160  terminate() {
    161    DownloadsCommon.log("Attempting to terminate DownloadsPanel for a window.");
    162    if (!this._initialized) {
    163      DownloadsCommon.log(
    164        "DownloadsPanel was never initialized. Nothing to do."
    165      );
    166      return;
    167    }
    168 
    169    window.removeEventListener("unload", this.onWindowUnload);
    170    document
    171      .getElementById("downloadPanelCommands")
    172      .removeEventListener("command", this);
    173 
    174    // Ensure that the panel is closed before shutting down.
    175    this.hidePanel();
    176 
    177    DownloadsViewController.terminate();
    178    DownloadsCommon.getData(window).removeView(DownloadsView);
    179    DownloadsCommon.getSummary(
    180      window,
    181      DownloadsView.kItemCountLimit
    182    ).removeView(DownloadsSummary);
    183    this._unattachEventListeners();
    184 
    185    if (DownloadIntegration.downloadSpamProtection) {
    186      DownloadIntegration.downloadSpamProtection.unregister(window);
    187    }
    188 
    189    this._torWarning?.deactivate();
    190 
    191    this._initialized = false;
    192 
    193    DownloadsSummary.active = false;
    194    DownloadsCommon.log("DownloadsPanel terminated.");
    195  },
    196 
    197  // Panel interface
    198 
    199  /**
    200   * Main panel element in the browser window.
    201   */
    202  get panel() {
    203    delete this.panel;
    204    return (this.panel = document.getElementById("downloadsPanel"));
    205  },
    206 
    207  /**
    208   * Starts opening the downloads panel interface, anchored to the downloads
    209   * button of the browser window.  The list of downloads to display is
    210   * initialized the first time this method is called, and the panel is shown
    211   * only when data is ready.
    212   */
    213  showPanel(openedManually = false, isKeyPress = false) {
    214    Glean.downloads.panelShown.add(1);
    215 
    216    DownloadsCommon.log("Opening the downloads panel.");
    217 
    218    this._openedManually = openedManually;
    219    this._preventFocusRing = !openedManually || !isKeyPress;
    220 
    221    if (this.isPanelShowing) {
    222      DownloadsCommon.log("Panel is already showing - focusing instead.");
    223      this._focusPanel();
    224      return;
    225    }
    226 
    227    // As a belt-and-suspenders check, ensure the button is not hidden.
    228    DownloadsButton.unhide();
    229 
    230    this.initialize();
    231    // Delay displaying the panel because this function will sometimes be
    232    // called while another window is closing (like the window for selecting
    233    // whether to save or open the file), and that would cause the panel to
    234    // close immediately.
    235    setTimeout(() => this._openPopupIfDataReady(), 0);
    236 
    237    DownloadsCommon.log("Waiting for the downloads panel to appear.");
    238    this._waitingDataForOpen = true;
    239  },
    240 
    241  /**
    242   * Hides the downloads panel, if visible, but keeps the internal state so that
    243   * the panel can be reopened quickly if required.
    244   */
    245  hidePanel() {
    246    DownloadsCommon.log("Closing the downloads panel.");
    247 
    248    if (!this.isPanelShowing) {
    249      DownloadsCommon.log("Downloads panel is not showing - nothing to do.");
    250      return;
    251    }
    252 
    253    PanelMultiView.hidePopup(this.panel);
    254    DownloadsCommon.log("Downloads panel is now closed.");
    255  },
    256 
    257  /**
    258   * Indicates whether the panel is showing.
    259   *
    260   * Note: this includes the hiding state.
    261   */
    262  get isPanelShowing() {
    263    return this._waitingDataForOpen || this.panel.state != "closed";
    264  },
    265 
    266  handleEvent(aEvent) {
    267    switch (aEvent.type) {
    268      case "click":
    269        DownloadsPanel.showDownloadsHistory();
    270        break;
    271      case "command":
    272        if (aEvent.currentTarget == DownloadsView.downloadsHistory) {
    273          DownloadsPanel.showDownloadsHistory();
    274          return;
    275        }
    276 
    277        if (
    278          aEvent.currentTarget == DownloadsBlockedSubview.elements.deleteButton
    279        ) {
    280          DownloadsBlockedSubview.confirmBlock();
    281          return;
    282        }
    283 
    284        // Handle the commands defined in downloadsPanel.inc.xhtml.
    285        // Every command "id" is also its corresponding command.
    286        goDoCommand(aEvent.target.id);
    287        break;
    288      case "mousemove":
    289        if (
    290          !DownloadsView.contextMenuOpen &&
    291          !DownloadsView.subViewOpen &&
    292          this.panel.contains(document.activeElement)
    293        ) {
    294          // Let mouse movement remove focus rings and reset focus in the panel.
    295          // This behavior is copied from PanelMultiView.
    296          document.activeElement.blur();
    297          DownloadsView.richListBox.removeAttribute("force-focus-visible");
    298          this._preventFocusRing = true;
    299          this._focusPanel();
    300        }
    301        break;
    302      case "mouseover":
    303        DownloadsView._onDownloadMouseOver(aEvent);
    304        break;
    305      case "mouseout":
    306        DownloadsView._onDownloadMouseOut(aEvent);
    307        break;
    308      case "contextmenu":
    309        DownloadsView._onDownloadContextMenu(aEvent);
    310        break;
    311      case "dragstart":
    312        DownloadsView._onDownloadDragStart(aEvent);
    313        break;
    314      case "mousedown":
    315        if (DownloadsView.richListBox.hasAttribute("disabled")) {
    316          this._handlePotentiallySpammyDownloadActivation(aEvent);
    317        }
    318        break;
    319 
    320      case "keydown":
    321        if (aEvent.currentTarget == DownloadsSummary._summaryNode) {
    322          DownloadsSummary._onKeyDown(aEvent);
    323          return;
    324        }
    325 
    326        this._onKeyDown(aEvent);
    327        break;
    328      case "keypress":
    329        this._onKeyPress(aEvent);
    330        break;
    331      case "focus":
    332      case "select":
    333        this._onSelect(aEvent);
    334        break;
    335      case "popupshown":
    336        this._onPopupShown(aEvent);
    337        break;
    338      case "popuphidden":
    339        this._onPopupHidden(aEvent);
    340        break;
    341    }
    342  },
    343 
    344  // Callback functions from DownloadsView
    345 
    346  /**
    347   * Called after data loading finished.
    348   */
    349  onViewLoadCompleted() {
    350    this._openPopupIfDataReady();
    351  },
    352 
    353  // User interface event functions
    354 
    355  onWindowUnload() {
    356    // This function is registered as an event listener, we can't use "this".
    357    DownloadsPanel.terminate();
    358  },
    359 
    360  _onPopupShown(aEvent) {
    361    // Ignore events raised by nested popups.
    362    if (aEvent.target != this.panel) {
    363      return;
    364    }
    365 
    366    DownloadsCommon.log("Downloads panel has shown.");
    367 
    368    // Since at most one popup is open at any given time, we can set globally.
    369    DownloadsCommon.getIndicatorData(window).attentionSuppressed |=
    370      DownloadsCommon.SUPPRESS_PANEL_OPEN;
    371 
    372    // Ensure that the first item is selected when the panel is focused.
    373    if (DownloadsView.richListBox.itemCount > 0) {
    374      DownloadsView.richListBox.selectedIndex = 0;
    375    }
    376 
    377    this._focusPanel();
    378  },
    379 
    380  _onPopupHidden(aEvent) {
    381    // Ignore events raised by nested popups.
    382    if (aEvent.target != this.panel) {
    383      return;
    384    }
    385 
    386    DownloadsCommon.log("Downloads panel has hidden.");
    387 
    388    if (this._delayTimeout) {
    389      DownloadsView.richListBox.removeAttribute("disabled");
    390      clearTimeout(this._delayTimeout);
    391      this._stopWatchingForSpammyDownloadActivation();
    392      this._delayTimeout = null;
    393    }
    394 
    395    DownloadsView.richListBox.removeAttribute("force-focus-visible");
    396 
    397    // Since at most one popup is open at any given time, we can set globally.
    398    DownloadsCommon.getIndicatorData(window).attentionSuppressed &=
    399      ~DownloadsCommon.SUPPRESS_PANEL_OPEN;
    400 
    401    // Allow the anchor to be hidden.
    402    DownloadsButton.releaseAnchor();
    403  },
    404 
    405  // Related operations
    406 
    407  /**
    408   * Shows or focuses the user interface dedicated to downloads history.
    409   */
    410  showDownloadsHistory() {
    411    DownloadsCommon.log("Showing download history.");
    412    // Hide the panel before showing another window, otherwise focus will return
    413    // to the browser window when the panel closes automatically.
    414    this.hidePanel();
    415 
    416    BrowserCommands.downloadsUI();
    417  },
    418 
    419  // Internal functions
    420 
    421  /**
    422   * Attach event listeners to a panel element. These listeners should be
    423   * removed in _unattachEventListeners. This is called automatically after the
    424   * panel has successfully loaded.
    425   */
    426  _attachEventListeners() {
    427    // Handle keydown to support accel-V.
    428    this.panel.addEventListener("keydown", this);
    429    // Handle keypress to be able to preventDefault() events before they reach
    430    // the richlistbox, for keyboard navigation.
    431    this.panel.addEventListener("keypress", this);
    432    // Handle mousedown to be able to notice clicks on disabled items.
    433    this.panel.addEventListener("mousedown", this);
    434    this.panel.addEventListener("mousemove", this);
    435    this.panel.addEventListener("popupshown", this);
    436    this.panel.addEventListener("popuphidden", this);
    437    DownloadsView.richListBox.addEventListener("focus", this);
    438    DownloadsView.richListBox.addEventListener("select", this);
    439    DownloadsView.richListBox.addEventListener("mouseover", this);
    440    DownloadsView.richListBox.addEventListener("mouseout", this);
    441    DownloadsView.richListBox.addEventListener("contextmenu", this);
    442    DownloadsView.richListBox.addEventListener("dragstart", this);
    443 
    444    DownloadsView.downloadsHistory.addEventListener("command", this);
    445    DownloadsBlockedSubview.elements.deleteButton.addEventListener(
    446      "command",
    447      this
    448    );
    449    DownloadsSummary._summaryNode.addEventListener("click", this);
    450    DownloadsSummary._summaryNode.addEventListener("keydown", this);
    451  },
    452 
    453  /**
    454   * Unattach event listeners that were added in _attachEventListeners. This
    455   * is called automatically on panel termination.
    456   */
    457  _unattachEventListeners() {
    458    this.panel.removeEventListener("keydown", this);
    459    this.panel.removeEventListener("keypress", this);
    460    this.panel.removeEventListener("mousedown", this);
    461    this.panel.removeEventListener("mousemove", this);
    462    this.panel.removeEventListener("popupshown", this);
    463    this.panel.removeEventListener("popuphidden", this);
    464    DownloadsView.richListBox.removeEventListener("focus", this);
    465    DownloadsView.richListBox.removeEventListener("select", this);
    466    DownloadsView.richListBox.removeEventListener("mouseover", this);
    467    DownloadsView.richListBox.removeEventListener("mouseout", this);
    468    DownloadsView.richListBox.removeEventListener("contextmenu", this);
    469    DownloadsView.richListBox.removeEventListener("dragstart", this);
    470    DownloadsView.downloadsHistory.removeEventListener("command", this);
    471    DownloadsBlockedSubview.elements.deleteButton.removeEventListener(
    472      "command",
    473      this
    474    );
    475    DownloadsSummary._summaryNode.removeEventListener("click", this);
    476    DownloadsSummary._summaryNode.removeEventListener("keydown", this);
    477  },
    478 
    479  _onKeyPress(aEvent) {
    480    // Handle unmodified keys only.
    481    if (aEvent.altKey || aEvent.ctrlKey || aEvent.shiftKey || aEvent.metaKey) {
    482      return;
    483    }
    484 
    485    // Pass keypress events to the richlistbox view when it's focused.
    486    if (document.activeElement === DownloadsView.richListBox) {
    487      DownloadsView.onDownloadKeyPress(aEvent);
    488    }
    489  },
    490 
    491  /**
    492   * Keydown listener that listens for the keys to start key focusing, as well
    493   * as the the accel-V "paste" event, which initiates a file download if the
    494   * pasted item can be resolved to a URI.
    495   */
    496  _onKeyDown(aEvent) {
    497    if (DownloadsView.richListBox.hasAttribute("disabled")) {
    498      this._handlePotentiallySpammyDownloadActivation(aEvent);
    499      return;
    500    }
    501 
    502    let richListBox = DownloadsView.richListBox;
    503 
    504    // If the user has pressed the up or down cursor key, force-enable focus
    505    // indicators for the richlistbox.  :focus-visible doesn't work in this case
    506    // because the the focused element may not change here if the richlistbox
    507    // already had focus.  The force-focus-visible attribute will be removed
    508    // again if the user moves the mouse on the panel or if the panel is closed.
    509    if (
    510      aEvent.keyCode == aEvent.DOM_VK_UP ||
    511      aEvent.keyCode == aEvent.DOM_VK_DOWN
    512    ) {
    513      richListBox.setAttribute("force-focus-visible", "true");
    514    }
    515 
    516    // If the footer is focused and the downloads list has at least 1 element
    517    // in it, focus the last element in the list when going up.
    518    if (aEvent.keyCode == aEvent.DOM_VK_UP && richListBox.firstElementChild) {
    519      if (
    520        document
    521          .getElementById("downloadsFooter")
    522          .contains(document.activeElement)
    523      ) {
    524        richListBox.selectedItem = richListBox.lastElementChild;
    525        richListBox.focus();
    526        aEvent.preventDefault();
    527        return;
    528      }
    529    }
    530 
    531    if (aEvent.keyCode == aEvent.DOM_VK_DOWN) {
    532      // If the last element in the list is selected, or the footer is already
    533      // focused, focus the footer.
    534      if (
    535        DownloadsView.canChangeSelectedItem &&
    536        (richListBox.selectedItem === richListBox.lastElementChild ||
    537          document
    538            .getElementById("downloadsFooter")
    539            .contains(document.activeElement))
    540      ) {
    541        richListBox.selectedIndex = -1;
    542        DownloadsFooter.focus();
    543        aEvent.preventDefault();
    544        return;
    545      }
    546    }
    547 
    548    let pasting =
    549      aEvent.keyCode == aEvent.DOM_VK_V && aEvent.getModifierState("Accel");
    550 
    551    if (!pasting) {
    552      return;
    553    }
    554 
    555    DownloadsCommon.log("Received a paste event.");
    556 
    557    let trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
    558      Ci.nsITransferable
    559    );
    560    trans.init(null);
    561    let flavors = ["text/x-moz-url", "text/plain"];
    562    flavors.forEach(trans.addDataFlavor);
    563    Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard);
    564    // Getting the data or creating the nsIURI might fail
    565    try {
    566      let data = {};
    567      trans.getAnyTransferData({}, data);
    568      let [url, name] = data.value
    569        .QueryInterface(Ci.nsISupportsString)
    570        .data.split("\n");
    571      if (!url) {
    572        return;
    573      }
    574 
    575      let uri = NetUtil.newURI(url);
    576      DownloadsCommon.log("Pasted URL seems valid. Starting download.");
    577      DownloadURL(uri.spec, name, document);
    578    } catch (ex) {}
    579  },
    580 
    581  _onSelect() {
    582    let richlistbox = DownloadsView.richListBox;
    583    richlistbox.itemChildren.forEach(item => {
    584      let button = item.querySelector("button");
    585      if (item.selected) {
    586        button.removeAttribute("tabindex");
    587      } else {
    588        button.setAttribute("tabindex", -1);
    589      }
    590    });
    591  },
    592 
    593  /**
    594   * Move focus to the main element in the downloads panel, unless another
    595   * element in the panel is already focused.
    596   *
    597   * @param {bool} [forceFocus=false] - Whether to force move the focus.
    598   */
    599  _focusPanel(forceFocus = false) {
    600    if (!forceFocus) {
    601      // We may be invoked while the panel is still waiting to be shown.
    602      if (this.panel.state != "open") {
    603        return;
    604      }
    605 
    606      if (
    607        document.activeElement &&
    608        (this.panel.contains(document.activeElement) ||
    609          this.panel.shadowRoot.contains(document.activeElement))
    610      ) {
    611        return;
    612      }
    613    }
    614 
    615    let focusOptions = {};
    616    if (this._preventFocusRing) {
    617      focusOptions.focusVisible = false;
    618    }
    619 
    620    // Focus the "Got it" button if it is visible.
    621    // This should ensure that the alert is read aloud by Orca when the
    622    // downloads panel is opened. See tor-browser#42642.
    623    if (!this._torWarning?.hidden) {
    624      this._torWarning.dismissButton.focus(focusOptions);
    625      return;
    626    }
    627 
    628    if (DownloadsView.richListBox.itemCount > 0) {
    629      if (DownloadsView.canChangeSelectedItem) {
    630        DownloadsView.richListBox.selectedIndex = 0;
    631      }
    632      DownloadsView.richListBox.focus(focusOptions);
    633    } else {
    634      DownloadsFooter.focus(focusOptions);
    635    }
    636  },
    637 
    638  _delayPopupItems() {
    639    DownloadsView.richListBox.setAttribute("disabled", true);
    640    this._startWatchingForSpammyDownloadActivation();
    641 
    642    this._refreshDelayTimer();
    643  },
    644 
    645  _refreshDelayTimer() {
    646    // If timeout already exists, overwrite it to avoid multiple timeouts.
    647    if (this._delayTimeout) {
    648      clearTimeout(this._delayTimeout);
    649    }
    650 
    651    let delay = Services.prefs.getIntPref("security.dialog_enable_delay");
    652    this._delayTimeout = setTimeout(() => {
    653      DownloadsView.richListBox.removeAttribute("disabled");
    654      this._stopWatchingForSpammyDownloadActivation();
    655      this._focusPanel();
    656      this._delayTimeout = null;
    657    }, delay);
    658  },
    659 
    660  _startWatchingForSpammyDownloadActivation() {
    661    window.addEventListener("keydown", this, {
    662      capture: true,
    663      mozSystemGroup: true,
    664    });
    665  },
    666 
    667  _lastBeepTime: 0,
    668  _handlePotentiallySpammyDownloadActivation(aEvent) {
    669    let isSpammyKey =
    670      aEvent.type.startsWith("key") &&
    671      (aEvent.key == "Enter" || aEvent.key == " ");
    672    let isSpammyMouse = aEvent.type.startsWith("mouse") && aEvent.button == 0;
    673    if (isSpammyKey || isSpammyMouse) {
    674      // Throttle our beeping to a maximum of once per second, otherwise it
    675      // appears on Win10 that beeps never make it through at all.
    676      if (Date.now() - this._lastBeepTime > 1000) {
    677        Cc["@mozilla.org/sound;1"].getService(Ci.nsISound).beep();
    678        this._lastBeepTime = Date.now();
    679      }
    680 
    681      this._refreshDelayTimer();
    682    }
    683  },
    684 
    685  _stopWatchingForSpammyDownloadActivation() {
    686    window.removeEventListener("keydown", this, {
    687      capture: true,
    688      mozSystemGroup: true,
    689    });
    690  },
    691 
    692  /**
    693   * Opens the downloads panel when data is ready to be displayed.
    694   */
    695  _openPopupIfDataReady() {
    696    // We don't want to open the popup if we already displayed it, or if we are
    697    // still loading data.
    698    if (!this._waitingDataForOpen || DownloadsView.loading) {
    699      return;
    700    }
    701    this._waitingDataForOpen = false;
    702 
    703    // At this point, if the window is minimized, opening the panel could fail
    704    // without any notification, and there would be no way to either open or
    705    // close the panel any more.  To prevent this, check if the window is
    706    // minimized and in that case force the panel to the closed state.
    707    if (window.windowState == window.STATE_MINIMIZED) {
    708      return;
    709    }
    710 
    711    // Ensure the anchor is visible.  If that is not possible, show the panel
    712    // anchored to the top area of the window, near the default anchor position.
    713    let anchor = DownloadsButton.getAnchor();
    714 
    715    if (!anchor) {
    716      DownloadsCommon.error("Downloads button cannot be found.");
    717      return;
    718    }
    719 
    720    let onBookmarksToolbar = !!anchor.closest("#PersonalToolbar");
    721    this.panel.classList.toggle("bookmarks-toolbar", onBookmarksToolbar);
    722 
    723    // When the panel is opened, we check if the target files of visible items
    724    // still exist, and update the allowed items interactions accordingly.  We
    725    // do these checks on a background thread, and don't prevent the panel to
    726    // be displayed while these checks are being performed.
    727    for (let viewItem of DownloadsView._visibleViewItems.values()) {
    728      viewItem.download.refresh().catch(console.error);
    729    }
    730 
    731    DownloadsCommon.log("Opening downloads panel popup.");
    732 
    733    // Delay displaying the panel because this function will sometimes be
    734    // called while another window is closing (like the window for selecting
    735    // whether to save or open the file), and that would cause the panel to
    736    // close immediately.
    737    setTimeout(() => {
    738      PanelMultiView.openPopup(
    739        this.panel,
    740        anchor,
    741        "bottomright topright",
    742        0,
    743        0,
    744        false,
    745        null
    746      ).then(() => {
    747        if (!this._openedManually) {
    748          this._delayPopupItems();
    749        }
    750 
    751        let isPrivate =
    752          window && PrivateBrowsingUtils.isContentWindowPrivate(window);
    753 
    754        if (
    755          // If private, show message asking whether to delete files at end of session
    756          isPrivate &&
    757          Services.prefs.getBoolPref(
    758            "browser.download.enableDeletePrivate",
    759            false
    760          ) &&
    761          !Services.prefs.getBoolPref(
    762            "browser.download.deletePrivate.chosen",
    763            false
    764          )
    765        ) {
    766          PrivateDownloadsSubview.openWhenReady();
    767        }
    768      }, console.error);
    769    }, 0);
    770  },
    771 };
    772 
    773 XPCOMUtils.defineConstant(this, "DownloadsPanel", DownloadsPanel);
    774 
    775 // DownloadsView
    776 
    777 /**
    778 * Builds and updates the downloads list widget, responding to changes in the
    779 * download state and real-time data.  In addition, handles part of the user
    780 * interaction events raised by the downloads list widget.
    781 */
    782 var DownloadsView = {
    783  // Functions handling download items in the list
    784 
    785  /**
    786   * Maximum number of items shown by the list at any given time.
    787   */
    788  kItemCountLimit: 5,
    789 
    790  /**
    791   * Indicates whether there is a DownloadsBlockedSubview open.
    792   */
    793  subViewOpen: false,
    794 
    795  /**
    796   * Indicates whether we are still loading downloads data asynchronously.
    797   */
    798  loading: false,
    799 
    800  /**
    801   * Ordered array of all Download objects.  We need to keep this array because
    802   * only a limited number of items are shown at once, and if an item that is
    803   * currently visible is removed from the list, we might need to take another
    804   * item from the array and make it appear at the bottom.
    805   */
    806  _downloads: [],
    807 
    808  /**
    809   * Associates the visible Download objects with their corresponding
    810   * DownloadsViewItem object.  There is a limited number of view items in the
    811   * panel at any given time.
    812   */
    813  _visibleViewItems: new Map(),
    814 
    815  /**
    816   * Called when the number of items in the list changes.
    817   */
    818  _itemCountChanged() {
    819    DownloadsCommon.log(
    820      "The downloads item count has changed - we are tracking",
    821      this._downloads.length,
    822      "downloads in total."
    823    );
    824    let count = this._downloads.length;
    825    let hiddenCount = count - this.kItemCountLimit;
    826 
    827    if (count > 0) {
    828      DownloadsCommon.log(
    829        "Setting the panel's hasdownloads attribute to true."
    830      );
    831      DownloadsPanel.panel.setAttribute("hasdownloads", "true");
    832    } else {
    833      DownloadsCommon.log("Removing the panel's hasdownloads attribute.");
    834      DownloadsPanel.panel.removeAttribute("hasdownloads");
    835    }
    836 
    837    // If we've got some hidden downloads, we should activate the
    838    // DownloadsSummary. The DownloadsSummary will determine whether or not
    839    // it's appropriate to actually display the summary.
    840    DownloadsSummary.active = hiddenCount > 0;
    841  },
    842 
    843  /**
    844   * Element corresponding to the list of downloads.
    845   */
    846  get richListBox() {
    847    delete this.richListBox;
    848    return (this.richListBox = document.getElementById("downloadsListBox"));
    849  },
    850 
    851  /**
    852   * Element corresponding to the button for showing more downloads.
    853   */
    854  get downloadsHistory() {
    855    delete this.downloadsHistory;
    856    return (this.downloadsHistory =
    857      document.getElementById("downloadsHistory"));
    858  },
    859 
    860  // Callback functions from DownloadsData
    861 
    862  /**
    863   * Called before multiple downloads are about to be loaded.
    864   */
    865  onDownloadBatchStarting() {
    866    DownloadsCommon.log("onDownloadBatchStarting called for DownloadsView.");
    867    this.loading = true;
    868  },
    869 
    870  /**
    871   * Called after data loading finished.
    872   */
    873  onDownloadBatchEnded() {
    874    DownloadsCommon.log("onDownloadBatchEnded called for DownloadsView.");
    875 
    876    this.loading = false;
    877 
    878    // We suppressed item count change notifications during the batch load, at
    879    // this point we should just call the function once.
    880    this._itemCountChanged();
    881 
    882    // Notify the panel that all the initially available downloads have been
    883    // loaded.  This ensures that the interface is visible, if still required.
    884    DownloadsPanel.onViewLoadCompleted();
    885  },
    886 
    887  /**
    888   * Called when a new download data item is available, either during the
    889   * asynchronous data load or when a new download is started.
    890   *
    891   * @param aDownload
    892   *        Download object that was just added.
    893   */
    894  onDownloadAdded(download) {
    895    DownloadsCommon.log("A new download data item was added");
    896 
    897    this._downloads.unshift(download);
    898 
    899    // The newly added item is visible in the panel and we must add the
    900    // corresponding element. If the list overflows, remove the last item from
    901    // the panel to make room for the new one that we just added at the top.
    902    this._addViewItem(download, true);
    903    if (this._downloads.length > this.kItemCountLimit) {
    904      this._removeViewItem(this._downloads[this.kItemCountLimit]);
    905    }
    906 
    907    // For better performance during batch loads, don't update the count for
    908    // every item, because the interface won't be visible until load finishes.
    909    if (!this.loading) {
    910      this._itemCountChanged();
    911    }
    912  },
    913 
    914  onDownloadChanged(download) {
    915    let viewItem = this._visibleViewItems.get(download);
    916    if (viewItem) {
    917      viewItem.onChanged();
    918    }
    919  },
    920 
    921  /**
    922   * Called when a data item is removed.  Ensures that the widget associated
    923   * with the view item is removed from the user interface.
    924   *
    925   * @param download
    926   *        Download object that is being removed.
    927   */
    928  onDownloadRemoved(download) {
    929    DownloadsCommon.log("A download data item was removed.");
    930 
    931    let itemIndex = this._downloads.indexOf(download);
    932    this._downloads.splice(itemIndex, 1);
    933 
    934    if (itemIndex < this.kItemCountLimit) {
    935      // The item to remove is visible in the panel.
    936      this._removeViewItem(download);
    937      if (this._downloads.length >= this.kItemCountLimit) {
    938        // Reinsert the next item into the panel.
    939        this._addViewItem(this._downloads[this.kItemCountLimit - 1], false);
    940      }
    941    }
    942 
    943    this._itemCountChanged();
    944  },
    945 
    946  /**
    947   * Associates each richlistitem for a download with its corresponding
    948   * DownloadsViewItem object.
    949   */
    950  _itemsForElements: new Map(),
    951 
    952  itemForElement(element) {
    953    return this._itemsForElements.get(element);
    954  },
    955 
    956  /**
    957   * Creates a new view item associated with the specified data item, and adds
    958   * it to the top or the bottom of the list.
    959   */
    960  _addViewItem(download, aNewest) {
    961    DownloadsCommon.log(
    962      "Adding a new DownloadsViewItem to the downloads list.",
    963      "aNewest =",
    964      aNewest
    965    );
    966 
    967    let element = document.createXULElement("richlistitem");
    968    element.setAttribute("align", "center");
    969 
    970    let viewItem = new DownloadsViewItem(download, element);
    971    this._visibleViewItems.set(download, viewItem);
    972    this._itemsForElements.set(element, viewItem);
    973    if (aNewest) {
    974      this.richListBox.insertBefore(
    975        element,
    976        this.richListBox.firstElementChild
    977      );
    978    } else {
    979      this.richListBox.appendChild(element);
    980    }
    981    viewItem.ensureActive();
    982  },
    983 
    984  /**
    985   * Removes the view item associated with the specified data item.
    986   */
    987  _removeViewItem(download) {
    988    DownloadsCommon.log(
    989      "Removing a DownloadsViewItem from the downloads list."
    990    );
    991    let element = this._visibleViewItems.get(download).element;
    992    let previousSelectedIndex = this.richListBox.selectedIndex;
    993    this.richListBox.removeChild(element);
    994    if (previousSelectedIndex != -1) {
    995      this.richListBox.selectedIndex = Math.min(
    996        previousSelectedIndex,
    997        this.richListBox.itemCount - 1
    998      );
    999    }
   1000    this._visibleViewItems.delete(download);
   1001    this._itemsForElements.delete(element);
   1002  },
   1003 
   1004  // User interface event functions
   1005 
   1006  onDownloadClick(aEvent) {
   1007    // Handle primary clicks in the main area only:
   1008    if (aEvent.button == 0 && aEvent.target.closest(".downloadMainArea")) {
   1009      let target = aEvent.target.closest("richlistitem");
   1010      // Ignore clicks if the box is disabled.
   1011      if (target.closest("richlistbox").hasAttribute("disabled")) {
   1012        return;
   1013      }
   1014      let download = DownloadsView.itemForElement(target).download;
   1015      if (download.succeeded) {
   1016        download._launchedFromPanel = true;
   1017      }
   1018      let command = "downloadsCmd_open";
   1019      if (download.hasBlockedData) {
   1020        command = "downloadsCmd_showBlockedInfo";
   1021      } else if (aEvent.shiftKey || aEvent.ctrlKey || aEvent.metaKey) {
   1022        // We adjust the command for supported modifiers to suggest where the download
   1023        // may be opened
   1024        let openWhere = BrowserUtils.whereToOpenLink(aEvent, false, true);
   1025        if (["tab", "window", "tabshifted"].includes(openWhere)) {
   1026          command += ":" + openWhere;
   1027        }
   1028      }
   1029      // Toggle opening the file after the download has completed
   1030      if (!download.stopped && command.startsWith("downloadsCmd_open")) {
   1031        download.launchWhenSucceeded = !download.launchWhenSucceeded;
   1032        download._launchedFromPanel = download.launchWhenSucceeded;
   1033      }
   1034 
   1035      DownloadsCommon.log("onDownloadClick, resolved command: ", command);
   1036      goDoCommand(command);
   1037    }
   1038  },
   1039 
   1040  onDownloadButton(event) {
   1041    let target = event.target.closest("richlistitem");
   1042    DownloadsView.itemForElement(target).onButton();
   1043  },
   1044 
   1045  /**
   1046   * Handles keypress events on a download item.
   1047   */
   1048  onDownloadKeyPress(aEvent) {
   1049    // Pressing the key on buttons should not invoke the action because the
   1050    // event has already been handled by the button itself.
   1051    if (
   1052      aEvent.originalTarget.hasAttribute("command") ||
   1053      aEvent.originalTarget.hasAttribute("oncommand")
   1054    ) {
   1055      return;
   1056    }
   1057 
   1058    if (aEvent.charCode == " ".charCodeAt(0)) {
   1059      aEvent.preventDefault();
   1060      goDoCommand("downloadsCmd_pauseResume");
   1061      return;
   1062    }
   1063 
   1064    if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) {
   1065      let readyToDownload = !DownloadsView.richListBox.disabled;
   1066      if (readyToDownload) {
   1067        goDoCommand("downloadsCmd_doDefault");
   1068      }
   1069    }
   1070  },
   1071 
   1072  get contextMenu() {
   1073    let menu = document.getElementById("downloadsContextMenu");
   1074    if (menu) {
   1075      delete this.contextMenu;
   1076      this.contextMenu = menu;
   1077    }
   1078    return menu;
   1079  },
   1080 
   1081  /**
   1082   * Indicates whether there is an open contextMenu for a download item.
   1083   */
   1084  get contextMenuOpen() {
   1085    return this.contextMenu.state != "closed";
   1086  },
   1087 
   1088  /**
   1089   * Whether it's possible to change the currently selected item.
   1090   */
   1091  get canChangeSelectedItem() {
   1092    // When the context menu or a subview are open, the selected item should
   1093    // not change.
   1094    return !this.contextMenuOpen && !this.subViewOpen;
   1095  },
   1096 
   1097  /**
   1098   * Mouse listeners to handle selection on hover.
   1099   */
   1100  _onDownloadMouseOver(aEvent) {
   1101    let item = aEvent.target.closest("richlistitem,richlistbox");
   1102    if (item.localName != "richlistitem") {
   1103      return;
   1104    }
   1105 
   1106    if (aEvent.target.classList.contains("downloadButton")) {
   1107      item.classList.add("downloadHoveringButton");
   1108    }
   1109 
   1110    item.classList.toggle(
   1111      "hoveringMainArea",
   1112      aEvent.target.closest(".downloadMainArea")
   1113    );
   1114 
   1115    if (this.canChangeSelectedItem) {
   1116      this.richListBox.selectedItem = item;
   1117    }
   1118  },
   1119 
   1120  _onDownloadMouseOut(aEvent) {
   1121    let item = aEvent.target.closest("richlistitem,richlistbox");
   1122    if (item.localName != "richlistitem") {
   1123      return;
   1124    }
   1125 
   1126    if (aEvent.target.classList.contains("downloadButton")) {
   1127      item.classList.remove("downloadHoveringButton");
   1128    }
   1129 
   1130    // If the destination element is outside of the richlistitem, clear the
   1131    // selection.
   1132    if (this.canChangeSelectedItem && !item.contains(aEvent.relatedTarget)) {
   1133      this.richListBox.selectedIndex = -1;
   1134    }
   1135  },
   1136 
   1137  _onDownloadContextMenu(aEvent) {
   1138    let element = aEvent.originalTarget.closest("richlistitem");
   1139    if (!element) {
   1140      aEvent.preventDefault();
   1141      return;
   1142    }
   1143    // Ensure the selected item is the expected one, so commands and the
   1144    // context menu are updated appropriately.
   1145    this.richListBox.selectedItem = element;
   1146    DownloadsViewController.updateCommands();
   1147 
   1148    DownloadsViewUI.updateContextMenuForElement(this.contextMenu, element);
   1149    // Hide the copy location item if there is somehow no URL. We have to do
   1150    // this here instead of in DownloadsViewUI because DownloadsPlacesView
   1151    // allows selecting multiple downloads, so in that view the menuitem will be
   1152    // shown according to whether at least one of the selected items has a URL.
   1153    this.contextMenu.querySelector(".downloadCopyLocationMenuItem").hidden =
   1154      !element._shell.download.source?.url;
   1155  },
   1156 
   1157  _onDownloadDragStart(aEvent) {
   1158    let element = aEvent.target.closest("richlistitem");
   1159    if (!element) {
   1160      return;
   1161    }
   1162 
   1163    // We must check for existence synchronously because this is a DOM event.
   1164    let file = new FileUtils.File(
   1165      DownloadsView.itemForElement(element).download.target.path
   1166    );
   1167    if (!file.exists()) {
   1168      return;
   1169    }
   1170 
   1171    let dataTransfer = aEvent.dataTransfer;
   1172    dataTransfer.mozSetDataAt("application/x-moz-file", file, 0);
   1173    dataTransfer.effectAllowed = "copyMove";
   1174    let spec = NetUtil.newURI(file).spec;
   1175    dataTransfer.setData("text/uri-list", spec);
   1176    dataTransfer.addElement(element);
   1177 
   1178    aEvent.stopPropagation();
   1179  },
   1180 };
   1181 
   1182 XPCOMUtils.defineConstant(this, "DownloadsView", DownloadsView);
   1183 
   1184 // DownloadsViewItem
   1185 
   1186 /**
   1187 * Builds and updates a single item in the downloads list widget, responding to
   1188 * changes in the download state and real-time data, and handles the user
   1189 * interaction events related to a single item in the downloads list widgets.
   1190 *
   1191 * @param download
   1192 *        Download object to be associated with the view item.
   1193 * @param aElement
   1194 *        XUL element corresponding to the single download item in the view.
   1195 */
   1196 
   1197 class DownloadsViewItem extends DownloadsViewUI.DownloadElementShell {
   1198  constructor(download, aElement) {
   1199    super();
   1200 
   1201    this.download = download;
   1202    this.element = aElement;
   1203    this.element._shell = this;
   1204 
   1205    this.element.setAttribute("type", "download");
   1206    this.element.classList.add("download-state");
   1207 
   1208    this.isPanel = true;
   1209  }
   1210 
   1211  onChanged() {
   1212    let newState = DownloadsCommon.stateOfDownload(this.download);
   1213    if (this.downloadState !== newState) {
   1214      this.downloadState = newState;
   1215      this._updateState();
   1216    } else {
   1217      this._updateStateInner();
   1218    }
   1219  }
   1220 
   1221  isCommandEnabled(aCommand) {
   1222    switch (aCommand) {
   1223      case "downloadsCmd_open":
   1224      case "downloadsCmd_open:current":
   1225      case "downloadsCmd_open:tab":
   1226      case "downloadsCmd_open:tabshifted":
   1227      case "downloadsCmd_open:window":
   1228      case "downloadsCmd_alwaysOpenSimilarFiles": {
   1229        if (!this.download.succeeded) {
   1230          return false;
   1231        }
   1232 
   1233        let file = new FileUtils.File(this.download.target.path);
   1234        return file.exists();
   1235      }
   1236      case "downloadsCmd_show": {
   1237        let file = new FileUtils.File(this.download.target.path);
   1238        if (file.exists()) {
   1239          return true;
   1240        }
   1241 
   1242        if (!this.download.target.partFilePath) {
   1243          return false;
   1244        }
   1245 
   1246        let partFile = new FileUtils.File(this.download.target.partFilePath);
   1247        return partFile.exists();
   1248      }
   1249      case "downloadsCmd_copyLocation":
   1250        return !!this.download.source?.url;
   1251      case "cmd_delete":
   1252      case "downloadsCmd_doDefault":
   1253        return true;
   1254      case "downloadsCmd_showBlockedInfo":
   1255        return this.download.hasBlockedData;
   1256    }
   1257    return DownloadsViewUI.DownloadElementShell.prototype.isCommandEnabled.call(
   1258      this,
   1259      aCommand
   1260    );
   1261  }
   1262 
   1263  doCommand(aCommand) {
   1264    if (this.isCommandEnabled(aCommand)) {
   1265      let [command, modifier] = aCommand.split(":");
   1266      // split off an optional command "modifier" into an argument,
   1267      // e.g. "downloadsCmd_open:window"
   1268      this[command](modifier);
   1269    }
   1270  }
   1271 
   1272  // Item commands
   1273 
   1274  downloadsCmd_unblock() {
   1275    DownloadsPanel.hidePanel();
   1276    this.confirmUnblock(window, "unblock");
   1277  }
   1278 
   1279  downloadsCmd_chooseUnblock() {
   1280    DownloadsPanel.hidePanel();
   1281    this.confirmUnblock(window, "chooseUnblock");
   1282  }
   1283 
   1284  downloadsCmd_unblockAndOpen() {
   1285    DownloadsPanel.hidePanel();
   1286    this.unblockAndOpenDownload().catch(console.error);
   1287  }
   1288  downloadsCmd_unblockAndSave() {
   1289    DownloadsPanel.hidePanel();
   1290    this.unblockAndSave();
   1291  }
   1292 
   1293  downloadsCmd_open(openWhere) {
   1294    super.downloadsCmd_open(openWhere);
   1295 
   1296    // We explicitly close the panel here to give the user the feedback that
   1297    // their click has been received, and we're handling the action.
   1298    // Otherwise, we'd have to wait for the file-type handler to execute
   1299    // before the panel would close. This also helps to prevent the user from
   1300    // accidentally opening a file several times.
   1301    DownloadsPanel.hidePanel();
   1302  }
   1303 
   1304  downloadsCmd_openInSystemViewer() {
   1305    super.downloadsCmd_openInSystemViewer();
   1306 
   1307    // We explicitly close the panel here to give the user the feedback that
   1308    // their click has been received, and we're handling the action.
   1309    DownloadsPanel.hidePanel();
   1310  }
   1311 
   1312  downloadsCmd_alwaysOpenInSystemViewer() {
   1313    super.downloadsCmd_alwaysOpenInSystemViewer();
   1314 
   1315    // We explicitly close the panel here to give the user the feedback that
   1316    // their click has been received, and we're handling the action.
   1317    DownloadsPanel.hidePanel();
   1318  }
   1319 
   1320  downloadsCmd_alwaysOpenSimilarFiles() {
   1321    super.downloadsCmd_alwaysOpenSimilarFiles();
   1322 
   1323    // We explicitly close the panel here to give the user the feedback that
   1324    // their click has been received, and we're handling the action.
   1325    DownloadsPanel.hidePanel();
   1326  }
   1327 
   1328  downloadsCmd_show() {
   1329    let file = new FileUtils.File(this.download.target.path);
   1330    DownloadsCommon.showDownloadedFile(file);
   1331 
   1332    // We explicitly close the panel here to give the user the feedback that
   1333    // their click has been received, and we're handling the action.
   1334    // Otherwise, we'd have to wait for the operating system file manager
   1335    // window to open before the panel closed. This also helps to prevent the
   1336    // user from opening the containing folder several times.
   1337    DownloadsPanel.hidePanel();
   1338  }
   1339 
   1340  async downloadsCmd_deleteFile() {
   1341    await super.downloadsCmd_deleteFile();
   1342    // Protects against an unusual edge case where the user:
   1343    // 1) downloads a file with Firefox; 2) deletes the file from outside of Firefox, e.g., a file manager;
   1344    // 3) downloads the same file from the same source; 4) opens the downloads panel and uses the menuitem to delete one of those 2 files;
   1345    // Under those conditions, Firefox will make 2 view items even though there's only 1 file.
   1346    // Using this method will only delete the view item it was called on, because this instance is not aware of other view items with identical targets.
   1347    // So the remaining view item needs to be refreshed to hide the "Delete" option.
   1348    // That example only concerns 2 duplicate view items but you can have an arbitrary number, so iterate over all items...
   1349    for (let viewItem of DownloadsView._visibleViewItems.values()) {
   1350      viewItem.download.refresh().catch(console.error);
   1351    }
   1352    // Don't use DownloadsPanel.hidePanel for this method because it will remove
   1353    // the view item from the list, which is already sufficient feedback.
   1354  }
   1355 
   1356  downloadsCmd_showBlockedInfo() {
   1357    DownloadsBlockedSubview.toggle(
   1358      this.element,
   1359      ...this.rawBlockedTitleAndDetails
   1360    );
   1361  }
   1362 
   1363  downloadsCmd_openReferrer() {
   1364    openURL(this.download.source.referrerInfo.originalReferrer);
   1365  }
   1366 
   1367  downloadsCmd_copyLocation() {
   1368    DownloadsCommon.copyDownloadLink(this.download);
   1369  }
   1370 
   1371  downloadsCmd_doDefault() {
   1372    let defaultCommand = this.currentDefaultCommandName;
   1373    if (defaultCommand && this.isCommandEnabled(defaultCommand)) {
   1374      this.doCommand(defaultCommand);
   1375    }
   1376  }
   1377 }
   1378 
   1379 // DownloadsViewController
   1380 
   1381 /**
   1382 * Handles part of the user interaction events raised by the downloads list
   1383 * widget, in particular the "commands" that apply to multiple items, and
   1384 * dispatches the commands that apply to individual items.
   1385 */
   1386 var DownloadsViewController = {
   1387  // Initialization and termination
   1388 
   1389  initialize() {
   1390    window.controllers.insertControllerAt(0, this);
   1391  },
   1392 
   1393  terminate() {
   1394    window.controllers.removeController(this);
   1395  },
   1396 
   1397  // nsIController
   1398 
   1399  supportsCommand(aCommand) {
   1400    if (
   1401      aCommand === "downloadsCmd_clearList" ||
   1402      aCommand === "downloadsCmd_deletePrivate" ||
   1403      aCommand === "downloadsCmd_dismissDeletePrivate"
   1404    ) {
   1405      return true;
   1406    }
   1407    // Firstly, determine if this is a command that we can handle.
   1408    if (!DownloadsViewUI.isCommandName(aCommand)) {
   1409      return false;
   1410    }
   1411    // Strip off any :modifier suffix before checking if the command name is
   1412    // a method on our view
   1413    let [command] = aCommand.split(":");
   1414    if (!(command in this) && !(command in DownloadsViewItem.prototype)) {
   1415      return false;
   1416    }
   1417    // The currently supported commands depend on whether the blocked subview is
   1418    // showing.  If it is, then take the following path.
   1419    if (DownloadsView.subViewOpen) {
   1420      let blockedSubviewCmds = [
   1421        "downloadsCmd_unblockAndOpen",
   1422        "cmd_delete",
   1423        "downloadsCmd_unblockAndSave",
   1424      ];
   1425      return blockedSubviewCmds.includes(aCommand);
   1426    }
   1427    // If the blocked subview is not showing, then determine if focus is on a
   1428    // control in the downloads list.
   1429    let element = document.commandDispatcher.focusedElement;
   1430    while (element && element != DownloadsView.richListBox) {
   1431      element = element.parentNode;
   1432    }
   1433    // We should handle the command only if the downloads list is among the
   1434    // ancestors of the focused element.
   1435    return !!element;
   1436  },
   1437 
   1438  isCommandEnabled(aCommand) {
   1439    // Handle commands that are not selection-specific.
   1440    switch (aCommand) {
   1441      case "downloadsCmd_clearList": {
   1442        return DownloadsCommon.getData(window).canRemoveFinished;
   1443      }
   1444      case "downloadsCmd_deletePrivate":
   1445      case "downloadsCmd_dismissDeletePrivate":
   1446        return true;
   1447      default: {
   1448        // Other commands are selection-specific.
   1449        let element = DownloadsView.richListBox.selectedItem;
   1450        return (
   1451          element &&
   1452          DownloadsView.itemForElement(element).isCommandEnabled(aCommand)
   1453        );
   1454      }
   1455    }
   1456  },
   1457 
   1458  doCommand(aCommand) {
   1459    // If this command is not selection-specific, execute it.
   1460    if (aCommand in this) {
   1461      this[aCommand]();
   1462      return;
   1463    }
   1464 
   1465    // Other commands are selection-specific.
   1466    let element = DownloadsView.richListBox.selectedItem;
   1467    if (element) {
   1468      // The doCommand function also checks if the command is enabled.
   1469      DownloadsView.itemForElement(element).doCommand(aCommand);
   1470    }
   1471  },
   1472 
   1473  onEvent() {},
   1474 
   1475  // Other functions
   1476 
   1477  updateCommands() {
   1478    function updateCommandsForObject(object) {
   1479      for (let name in object) {
   1480        if (DownloadsViewUI.isCommandName(name)) {
   1481          goUpdateCommand(name);
   1482        }
   1483      }
   1484    }
   1485    updateCommandsForObject(this);
   1486    updateCommandsForObject(DownloadsViewItem.prototype);
   1487  },
   1488 
   1489  // Selection-independent commands
   1490 
   1491  downloadsCmd_clearList() {
   1492    DownloadsCommon.getData(window).removeFinished();
   1493  },
   1494 
   1495  downloadsCmd_deletePrivate() {
   1496    PrivateDownloadsSubview.choose(true /* deletePrivate */);
   1497  },
   1498 
   1499  downloadsCmd_dismissDeletePrivate() {
   1500    PrivateDownloadsSubview.choose(false /* deletePrivate */);
   1501  },
   1502 };
   1503 
   1504 XPCOMUtils.defineConstant(
   1505  this,
   1506  "DownloadsViewController",
   1507  DownloadsViewController
   1508 );
   1509 
   1510 // DownloadsSummary
   1511 
   1512 /**
   1513 * Manages the summary at the bottom of the downloads panel list if the number
   1514 * of items in the list exceeds the panels limit.
   1515 */
   1516 var DownloadsSummary = {
   1517  /**
   1518   * Sets the active state of the summary. When active, the summary subscribes
   1519   * to the DownloadsCommon DownloadsSummaryData singleton.
   1520   *
   1521   * @param aActive
   1522   *        Set to true to activate the summary.
   1523   */
   1524  set active(aActive) {
   1525    if (aActive == this._active || !this._summaryNode) {
   1526      return;
   1527    }
   1528    if (aActive) {
   1529      DownloadsCommon.getSummary(
   1530        window,
   1531        DownloadsView.kItemCountLimit
   1532      ).refreshView(this);
   1533    } else {
   1534      DownloadsFooter.showingSummary = false;
   1535    }
   1536 
   1537    this._active = aActive;
   1538  },
   1539 
   1540  /**
   1541   * Returns the active state of the downloads summary.
   1542   */
   1543  get active() {
   1544    return this._active;
   1545  },
   1546 
   1547  _active: false,
   1548 
   1549  /**
   1550   * Sets whether or not we show the progress bar.
   1551   *
   1552   * @param aShowingProgress
   1553   *        True if we should show the progress bar.
   1554   */
   1555  set showingProgress(aShowingProgress) {
   1556    if (aShowingProgress) {
   1557      this._summaryNode.setAttribute("inprogress", "true");
   1558    } else {
   1559      this._summaryNode.removeAttribute("inprogress");
   1560    }
   1561    // If progress isn't being shown, then we simply do not show the summary.
   1562    DownloadsFooter.showingSummary = aShowingProgress;
   1563  },
   1564 
   1565  /**
   1566   * Sets the amount of progress that is visible in the progress bar.
   1567   *
   1568   * @param aValue
   1569   *        A value between 0 and 100 to represent the progress of the
   1570   *        summarized downloads.
   1571   */
   1572  set percentComplete(aValue) {
   1573    if (this._progressNode) {
   1574      this._progressNode.setAttribute("value", aValue);
   1575    }
   1576  },
   1577 
   1578  /**
   1579   * Sets the description for the download summary.
   1580   *
   1581   * @param aValue
   1582   *        A string representing the description of the summarized
   1583   *        downloads.
   1584   */
   1585  set description(aValue) {
   1586    if (this._descriptionNode) {
   1587      this._descriptionNode.setAttribute("value", aValue);
   1588      this._descriptionNode.setAttribute("tooltiptext", aValue);
   1589    }
   1590  },
   1591 
   1592  /**
   1593   * Sets the details for the download summary, such as the time remaining,
   1594   * the amount of bytes transferred, etc.
   1595   *
   1596   * @param aValue
   1597   *        A string representing the details of the summarized
   1598   *        downloads.
   1599   */
   1600  set details(aValue) {
   1601    if (this._detailsNode) {
   1602      this._detailsNode.setAttribute("value", aValue);
   1603      this._detailsNode.setAttribute("tooltiptext", aValue);
   1604    }
   1605  },
   1606 
   1607  /**
   1608   * Focuses the root element of the summary.
   1609   */
   1610  focus(focusOptions) {
   1611    if (this._summaryNode) {
   1612      this._summaryNode.focus(focusOptions);
   1613    }
   1614  },
   1615 
   1616  /**
   1617   * Respond to keydown events on the Downloads Summary node.
   1618   *
   1619   * @param aEvent
   1620   *        The keydown event being handled.
   1621   */
   1622  _onKeyDown(aEvent) {
   1623    if (
   1624      aEvent.charCode == " ".charCodeAt(0) ||
   1625      aEvent.keyCode == KeyEvent.DOM_VK_RETURN
   1626    ) {
   1627      DownloadsPanel.showDownloadsHistory();
   1628    }
   1629  },
   1630 
   1631  /**
   1632   * Element corresponding to the root of the downloads summary.
   1633   */
   1634  get _summaryNode() {
   1635    let node = document.getElementById("downloadsSummary");
   1636    if (!node) {
   1637      return null;
   1638    }
   1639    delete this._summaryNode;
   1640    return (this._summaryNode = node);
   1641  },
   1642 
   1643  /**
   1644   * Element corresponding to the progress bar in the downloads summary.
   1645   */
   1646  get _progressNode() {
   1647    let node = document.getElementById("downloadsSummaryProgress");
   1648    if (!node) {
   1649      return null;
   1650    }
   1651    delete this._progressNode;
   1652    return (this._progressNode = node);
   1653  },
   1654 
   1655  /**
   1656   * Element corresponding to the main description of the downloads
   1657   * summary.
   1658   */
   1659  get _descriptionNode() {
   1660    let node = document.getElementById("downloadsSummaryDescription");
   1661    if (!node) {
   1662      return null;
   1663    }
   1664    delete this._descriptionNode;
   1665    return (this._descriptionNode = node);
   1666  },
   1667 
   1668  /**
   1669   * Element corresponding to the secondary description of the downloads
   1670   * summary.
   1671   */
   1672  get _detailsNode() {
   1673    let node = document.getElementById("downloadsSummaryDetails");
   1674    if (!node) {
   1675      return null;
   1676    }
   1677    delete this._detailsNode;
   1678    return (this._detailsNode = node);
   1679  },
   1680 };
   1681 
   1682 XPCOMUtils.defineConstant(this, "DownloadsSummary", DownloadsSummary);
   1683 
   1684 // DownloadsFooter
   1685 
   1686 /**
   1687 * Manages events sent to to the footer vbox, which contains both the
   1688 * DownloadsSummary as well as the "Show all downloads" button.
   1689 */
   1690 var DownloadsFooter = {
   1691  /**
   1692   * Focuses the appropriate element within the footer. If the summary
   1693   * is visible, focus it. If not, focus the "Show all downloads"
   1694   * button.
   1695   */
   1696  focus(focusOptions) {
   1697    if (this._showingSummary) {
   1698      DownloadsSummary.focus(focusOptions);
   1699    } else {
   1700      DownloadsView.downloadsHistory.focus(focusOptions);
   1701    }
   1702  },
   1703 
   1704  _showingSummary: false,
   1705 
   1706  /**
   1707   * Sets whether or not the Downloads Summary should be displayed in the
   1708   * footer. If not, the "Show all downloads" button is shown instead.
   1709   */
   1710  set showingSummary(aValue) {
   1711    if (this._footerNode) {
   1712      if (aValue) {
   1713        this._footerNode.setAttribute("showingsummary", "true");
   1714      } else {
   1715        this._footerNode.removeAttribute("showingsummary");
   1716      }
   1717      this._showingSummary = aValue;
   1718    }
   1719  },
   1720 
   1721  /**
   1722   * Element corresponding to the footer of the downloads panel.
   1723   */
   1724  get _footerNode() {
   1725    let node = document.getElementById("downloadsFooter");
   1726    if (!node) {
   1727      return null;
   1728    }
   1729    delete this._footerNode;
   1730    return (this._footerNode = node);
   1731  },
   1732 };
   1733 
   1734 XPCOMUtils.defineConstant(this, "DownloadsFooter", DownloadsFooter);
   1735 
   1736 // DownloadsBlockedSubview
   1737 
   1738 /**
   1739 * Manages the blocked subview that slides in when you click a blocked download.
   1740 */
   1741 var DownloadsBlockedSubview = {
   1742  /**
   1743   * Elements in the subview.
   1744   */
   1745  get elements() {
   1746    let idSuffixes = [
   1747      "title",
   1748      "details1",
   1749      "details2",
   1750      "unblockButton",
   1751      "deleteButton",
   1752    ];
   1753    let elements = idSuffixes.reduce((memo, s) => {
   1754      memo[s] = document.getElementById("downloadsPanel-blockedSubview-" + s);
   1755      return memo;
   1756    }, {});
   1757    delete this.elements;
   1758    return (this.elements = elements);
   1759  },
   1760 
   1761  /**
   1762   * The blocked-download richlistitem element that was clicked to show the
   1763   * subview.  If the subview is not showing, this is undefined.
   1764   */
   1765  element: undefined,
   1766 
   1767  /**
   1768   * Slides in the blocked subview.
   1769   *
   1770   * @param element
   1771   *        The blocked-download richlistitem element that was clicked.
   1772   * @param title
   1773   *        The title to show in the subview.
   1774   * @param details
   1775   *        An array of strings with information about the block.
   1776   */
   1777  toggle(element, title, details) {
   1778    DownloadsView.subViewOpen = true;
   1779    DownloadsViewController.updateCommands();
   1780    const { download } = DownloadsView.itemForElement(element);
   1781 
   1782    let e = this.elements;
   1783    let s = DownloadsCommon.strings;
   1784 
   1785    e.deleteButton.hidden =
   1786      download.error?.becauseBlockedByContentAnalysis &&
   1787      download.error?.reputationCheckVerdict === "Malware";
   1788 
   1789    e.unblockButton.hidden =
   1790      download.error?.becauseBlockedByContentAnalysis &&
   1791      download.error?.reputationCheckVerdict === "Malware";
   1792 
   1793    title.l10n
   1794      ? document.l10n.setAttributes(e.title, title.l10n.id, title.l10n.args)
   1795      : (e.title.textContent = title);
   1796 
   1797    details[0].l10n
   1798      ? document.l10n.setAttributes(
   1799          e.details1,
   1800          details[0].l10n.id,
   1801          details[0].l10n.args
   1802        )
   1803      : (e.details1.textContent = details[0]);
   1804 
   1805    e.details2.textContent = details[1];
   1806 
   1807    if (download.launchWhenSucceeded) {
   1808      e.unblockButton.label = s.unblockButtonOpen;
   1809      e.unblockButton.command = "downloadsCmd_unblockAndOpen";
   1810    } else {
   1811      e.unblockButton.label = s.unblockButtonUnblock;
   1812      e.unblockButton.command = "downloadsCmd_unblockAndSave";
   1813    }
   1814 
   1815    e.deleteButton.label = s.unblockButtonConfirmBlock;
   1816 
   1817    let verdict = element.getAttribute("verdict");
   1818    this.subview.setAttribute("verdict", verdict);
   1819 
   1820    this.mainView.addEventListener("ViewShown", this);
   1821    DownloadsPanel.panel.addEventListener("popuphidden", this);
   1822    this.panelMultiView.showSubView(this.subview);
   1823 
   1824    // Without this, the mainView is more narrow than the panel once all
   1825    // downloads are removed from the panel.
   1826    this.mainView.style.minWidth = window.getComputedStyle(this.subview).width;
   1827  },
   1828 
   1829  handleEvent(event) {
   1830    // This is called when the main view is shown or the panel is hidden.
   1831    DownloadsView.subViewOpen = false;
   1832    this.mainView.removeEventListener("ViewShown", this);
   1833    DownloadsPanel.panel.removeEventListener("popuphidden", this);
   1834    // Focus the proper element if we're going back to the main panel.
   1835    if (event.type == "ViewShown") {
   1836      DownloadsPanel.showPanel();
   1837    }
   1838  },
   1839 
   1840  /**
   1841   * Deletes the download and hides the entire panel.
   1842   */
   1843  confirmBlock() {
   1844    goDoCommand("cmd_delete");
   1845    DownloadsPanel.hidePanel();
   1846  },
   1847 };
   1848 
   1849 ChromeUtils.defineLazyGetter(DownloadsBlockedSubview, "panelMultiView", () =>
   1850  document.getElementById("downloadsPanel-multiView")
   1851 );
   1852 ChromeUtils.defineLazyGetter(DownloadsBlockedSubview, "mainView", () =>
   1853  document.getElementById("downloadsPanel-mainView")
   1854 );
   1855 ChromeUtils.defineLazyGetter(DownloadsBlockedSubview, "subview", () =>
   1856  document.getElementById("downloadsPanel-blockedSubview")
   1857 );
   1858 
   1859 XPCOMUtils.defineConstant(
   1860  this,
   1861  "DownloadsBlockedSubview",
   1862  DownloadsBlockedSubview
   1863 );
   1864 
   1865 /**
   1866 * Manages the private browsing downloads subview that appears when you download a file in private browsing mode
   1867 */
   1868 var PrivateDownloadsSubview = {
   1869  /**
   1870   * Slides in the private downloads subview.
   1871   *
   1872   * @param element
   1873   *        The download richlistitem element that was clicked.
   1874   */
   1875  openWhenReady() {
   1876    DownloadsView.subViewOpen = true;
   1877    DownloadsViewController.updateCommands();
   1878 
   1879    this.mainView.addEventListener("ViewShown", this, { once: true });
   1880    this.mainView.toggleAttribute("showing-private-browsing-choice", true);
   1881  },
   1882 
   1883  handleEvent(event) {
   1884    // This is called when the main view is shown or the panel is hidden.
   1885 
   1886    // Focus the proper element if we're going back to the main panel.
   1887    if (event.type == "ViewShown") {
   1888      this.panelMultiView.showSubView(this.subview);
   1889    }
   1890  },
   1891 
   1892  /**
   1893   * Sets whether to delete files at the end of private download session
   1894   * Based on user response to download notification prompt
   1895   *
   1896   * @param deletePrivate
   1897   *        True if the user chose to delete files at the end of the session
   1898   */
   1899  choose(deletePrivate) {
   1900    if (deletePrivate) {
   1901      Services.prefs.setBoolPref("browser.download.deletePrivate", true);
   1902    }
   1903    Services.prefs.setBoolPref("browser.download.deletePrivate.chosen", true);
   1904    DownloadsView.subViewOpen = false;
   1905    this.mainView.toggleAttribute("showing-private-browsing-choice", false);
   1906    this.panelMultiView.goBack();
   1907  },
   1908 };
   1909 
   1910 ChromeUtils.defineLazyGetter(PrivateDownloadsSubview, "panelMultiView", () =>
   1911  document.getElementById("downloadsPanel-multiView")
   1912 );
   1913 
   1914 ChromeUtils.defineLazyGetter(PrivateDownloadsSubview, "mainView", () =>
   1915  document.getElementById("downloadsPanel-mainView")
   1916 );
   1917 
   1918 ChromeUtils.defineLazyGetter(PrivateDownloadsSubview, "subview", () =>
   1919  document.getElementById("downloadsPanel-privateBrowsing")
   1920 );
   1921 
   1922 XPCOMUtils.defineConstant(
   1923  this,
   1924  "PrivateDownloadsSubview",
   1925  PrivateDownloadsSubview
   1926 );