tor-browser

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

DownloadsViewUI.sys.mjs (47174B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /*
      6 * This module is imported by code that uses the "download.xml" binding, and
      7 * provides prototypes for objects that handle input and display information.
      8 */
      9 
     10 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     11 
     12 const lazy = {};
     13 
     14 ChromeUtils.defineESModuleGetters(lazy, {
     15  BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
     16  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
     17  DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
     18  Downloads: "resource://gre/modules/Downloads.sys.mjs",
     19  DownloadsCommon:
     20    "moz-src:///browser/components/downloads/DownloadsCommon.sys.mjs",
     21  FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
     22  UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs",
     23 });
     24 
     25 XPCOMUtils.defineLazyServiceGetter(
     26  lazy,
     27  "handlerSvc",
     28  "@mozilla.org/uriloader/handler-service;1",
     29  Ci.nsIHandlerService
     30 );
     31 
     32 XPCOMUtils.defineLazyServiceGetter(
     33  lazy,
     34  "gReputationService",
     35  "@mozilla.org/reputationservice/application-reputation-service;1",
     36  Ci.nsIApplicationReputationService
     37 );
     38 
     39 XPCOMUtils.defineLazyPreferenceGetter(
     40  lazy,
     41  "contentAnalysisAgentName",
     42  "browser.contentanalysis.agent_name",
     43  "A DLP agent"
     44 );
     45 
     46 import { Integration } from "resource://gre/modules/Integration.sys.mjs";
     47 
     48 Integration.downloads.defineESModuleGetter(
     49  lazy,
     50  "DownloadIntegration",
     51  "resource://gre/modules/DownloadIntegration.sys.mjs"
     52 );
     53 
     54 const HTML_NS = "http://www.w3.org/1999/xhtml";
     55 
     56 var gDownloadElementButtons = {
     57  cancel: {
     58    commandName: "downloadsCmd_cancel",
     59    l10nId: "downloads-cmd-cancel",
     60    descriptionL10nId: "downloads-cancel-download",
     61    panelL10nId: "downloads-cmd-cancel-panel",
     62    iconClass: "downloadIconCancel",
     63  },
     64  retry: {
     65    commandName: "downloadsCmd_retry",
     66    l10nId: "downloads-cmd-retry",
     67    descriptionL10nId: "downloads-retry-download",
     68    panelL10nId: "downloads-cmd-retry-panel",
     69    iconClass: "downloadIconRetry",
     70  },
     71  show: {
     72    commandName: "downloadsCmd_show",
     73    l10nId: "downloads-cmd-show-button-2",
     74    descriptionL10nId: "downloads-cmd-show-description-2",
     75    panelL10nId: "downloads-cmd-show-panel-2",
     76    iconClass: "downloadIconShow",
     77  },
     78  subviewOpenOrRemoveFile: {
     79    commandName: "downloadsCmd_showBlockedInfo",
     80    l10nId: "downloads-cmd-choose-open",
     81    descriptionL10nId: "downloads-show-more-information",
     82    panelL10nId: "downloads-cmd-choose-open-panel",
     83    iconClass: "downloadIconSubviewArrow",
     84  },
     85  askOpenOrRemoveFile: {
     86    commandName: "downloadsCmd_chooseOpen",
     87    l10nId: "downloads-cmd-choose-open",
     88    panelL10nId: "downloads-cmd-choose-open-panel",
     89    iconClass: "downloadIconShow",
     90  },
     91  askRemoveFileOrAllow: {
     92    commandName: "downloadsCmd_chooseUnblock",
     93    l10nId: "downloads-cmd-choose-unblock",
     94    panelL10nId: "downloads-cmd-choose-unblock-panel",
     95    iconClass: "downloadIconShow",
     96  },
     97  removeFile: {
     98    commandName: "downloadsCmd_confirmBlock",
     99    l10nId: "downloads-cmd-remove-file",
    100    panelL10nId: "downloads-cmd-remove-file-panel",
    101    iconClass: "downloadIconCancel",
    102  },
    103 };
    104 
    105 /**
    106 * Associates each document with a pre-built DOM fragment representing the
    107 * download list item. This is then cloned to create each individual list item.
    108 * This is stored on the document to prevent leaks that would occur if a single
    109 * instance created by one document's DOMParser was stored globally.
    110 */
    111 var gDownloadListItemFragments = new WeakMap();
    112 
    113 export var DownloadsViewUI = {
    114  /**
    115   * Returns true if the given string is the name of a command that can be
    116   * handled by the Downloads user interface, including standard commands.
    117   */
    118  isCommandName(name) {
    119    return name.startsWith("cmd_") || name.startsWith("downloadsCmd_");
    120  },
    121 
    122  /**
    123   * Get source url of the download without'http' or'https' prefix.
    124   */
    125  getStrippedUrl(download) {
    126    return lazy.UrlbarUtils.stripPrefixAndTrim(download?.source?.url, {
    127      stripHttp: true,
    128      stripHttps: true,
    129    })[0];
    130  },
    131 
    132  /**
    133   * Returns the user-facing label for the given Download object. This is
    134   * normally the leaf name of the download target file. In case this is a very
    135   * old history download for which the target file is unknown, the download
    136   * source URI is displayed.
    137   */
    138  getDisplayName(download) {
    139    if (
    140      download.error?.reputationCheckVerdict ==
    141      lazy.Downloads.Error.BLOCK_VERDICT_DOWNLOAD_SPAM
    142    ) {
    143      let l10n = {
    144        id: "downloads-blocked-from-url",
    145        args: { url: DownloadsViewUI.getStrippedUrl(download) },
    146      };
    147      return { l10n };
    148    }
    149    return download.target.path
    150      ? PathUtils.filename(download.target.path)
    151      : download.source.url;
    152  },
    153 
    154  /**
    155   * Given a Download object, returns a string representing its file size with
    156   * an appropriate measurement unit, for example "1.5 MB", or an empty string
    157   * if the size is unknown.
    158   */
    159  getSizeWithUnits(download) {
    160    if (download.target.size === undefined) {
    161      return "";
    162    }
    163 
    164    let [size, unit] = lazy.DownloadUtils.convertByteUnits(
    165      download.target.size
    166    );
    167    return lazy.DownloadsCommon.strings.sizeWithUnits(size, unit);
    168  },
    169 
    170  /**
    171   * Given a context menu and a download element on which it is invoked,
    172   * update items in the context menu to reflect available options for
    173   * that download element.
    174   */
    175  updateContextMenuForElement(contextMenu, element) {
    176    // Get the state and ensure only the appropriate items are displayed.
    177    let state = parseInt(element.getAttribute("state"), 10);
    178 
    179    const document = contextMenu.ownerDocument;
    180 
    181    const {
    182      DOWNLOAD_NOTSTARTED,
    183      DOWNLOAD_DOWNLOADING,
    184      DOWNLOAD_FINISHED,
    185      DOWNLOAD_FAILED,
    186      DOWNLOAD_CANCELED,
    187      DOWNLOAD_PAUSED,
    188      DOWNLOAD_BLOCKED_PARENTAL,
    189      DOWNLOAD_DIRTY,
    190      DOWNLOAD_BLOCKED_POLICY,
    191    } = lazy.DownloadsCommon;
    192 
    193    contextMenu.querySelector(".downloadPauseMenuItem").hidden =
    194      state != DOWNLOAD_DOWNLOADING;
    195 
    196    contextMenu.querySelector(".downloadResumeMenuItem").hidden =
    197      state != DOWNLOAD_PAUSED;
    198 
    199    // Only show "unblock" for blocked (dirty) items that have not been
    200    // confirmed and have temporary data:
    201    contextMenu.querySelector(".downloadUnblockMenuItem").hidden =
    202      state != DOWNLOAD_DIRTY || !element.classList.contains("temporary-block");
    203 
    204    // Can only remove finished/failed/canceled/blocked downloads.
    205    contextMenu.querySelector(".downloadRemoveFromHistoryMenuItem").hidden = ![
    206      DOWNLOAD_FINISHED,
    207      DOWNLOAD_FAILED,
    208      DOWNLOAD_CANCELED,
    209      DOWNLOAD_BLOCKED_PARENTAL,
    210      DOWNLOAD_DIRTY,
    211      DOWNLOAD_BLOCKED_POLICY,
    212    ].includes(state);
    213 
    214    // Can reveal downloads with data on the file system using the relevant OS
    215    // tool (Explorer, Finder, appropriate Linux file system viewer):
    216    contextMenu.querySelector(".downloadShowMenuItem").hidden =
    217      ![
    218        DOWNLOAD_NOTSTARTED,
    219        DOWNLOAD_DOWNLOADING,
    220        DOWNLOAD_FINISHED,
    221        DOWNLOAD_PAUSED,
    222      ].includes(state) ||
    223      (state == DOWNLOAD_FINISHED && !element.hasAttribute("exists"));
    224 
    225    // Show the separator if we're showing either unblock or reveal menu items.
    226    contextMenu.querySelector(".downloadCommandsSeparator").hidden =
    227      contextMenu.querySelector(".downloadUnblockMenuItem").hidden &&
    228      contextMenu.querySelector(".downloadShowMenuItem").hidden;
    229 
    230    let download = element._shell.download;
    231    let mimeInfo = lazy.DownloadsCommon.getMimeInfo(download);
    232    let { preferredAction, useSystemDefault, defaultDescription } = mimeInfo
    233      ? mimeInfo
    234      : {};
    235 
    236    // Hide the "Delete" item if there's no file data to delete.
    237    contextMenu.querySelector(".downloadDeleteFileMenuItem").hidden =
    238      download.deleted ||
    239      !(download.target?.exists || download.target?.partFileExists);
    240 
    241    // Hide the "Go To Download Page" item if there's no referrer. Ideally the
    242    // Downloads API will require a referrer (see bug 1723712) to create a
    243    // download, but this fallback will ensure any failures aren't user facing.
    244    contextMenu.querySelector(".downloadOpenReferrerMenuItem").hidden =
    245      !download.source.referrerInfo?.originalReferrer;
    246 
    247    // Hide the "use system viewer" and "always use system viewer" items
    248    // if the feature is disabled or this download doesn't support it:
    249    let useSystemViewerItem = contextMenu.querySelector(
    250      ".downloadUseSystemDefaultMenuItem"
    251    );
    252    let alwaysUseSystemViewerItem = contextMenu.querySelector(
    253      ".downloadAlwaysUseSystemDefaultMenuItem"
    254    );
    255    let canViewInternally = element.hasAttribute("viewable-internally");
    256    useSystemViewerItem.hidden =
    257      !lazy.DownloadsCommon.openInSystemViewerItemEnabled ||
    258      !canViewInternally ||
    259      !download.target?.exists;
    260 
    261    alwaysUseSystemViewerItem.hidden =
    262      !lazy.DownloadsCommon.alwaysOpenInSystemViewerItemEnabled ||
    263      !canViewInternally;
    264 
    265    // Set menuitem labels to display the system viewer's name. Stop the l10n
    266    // mutation observer temporarily since we're going to synchronously
    267    // translate the elements to avoid translation delay. See bug 1737951 & bug
    268    // 1746748. This can be simplified when they're resolved.
    269    try {
    270      document.l10n.pauseObserving();
    271      // Handler descriptions longer than 40 characters will be skipped to avoid
    272      // unreasonably stretching the context menu.
    273      if (defaultDescription && defaultDescription.length < 40) {
    274        document.l10n.setAttributes(
    275          useSystemViewerItem,
    276          "downloads-cmd-use-system-default-named",
    277          { handler: defaultDescription }
    278        );
    279        document.l10n.setAttributes(
    280          alwaysUseSystemViewerItem,
    281          "downloads-cmd-always-use-system-default-named",
    282          { handler: defaultDescription }
    283        );
    284      } else {
    285        // In the unlikely event that defaultDescription is somehow missing/invalid,
    286        // fall back to the static "Open In System Viewer" label.
    287        document.l10n.setAttributes(
    288          useSystemViewerItem,
    289          "downloads-cmd-use-system-default"
    290        );
    291        document.l10n.setAttributes(
    292          alwaysUseSystemViewerItem,
    293          "downloads-cmd-always-use-system-default"
    294        );
    295      }
    296    } finally {
    297      document.l10n.resumeObserving();
    298    }
    299    document.l10n.translateElements([
    300      useSystemViewerItem,
    301      alwaysUseSystemViewerItem,
    302    ]);
    303 
    304    // If non default mime-type or cannot be opened internally, display
    305    // "always open similar files" item instead so that users can add a new
    306    // mimetype to about:preferences table and set to open with system default.
    307    let alwaysOpenSimilarFilesItem = contextMenu.querySelector(
    308      ".downloadAlwaysOpenSimilarFilesMenuItem"
    309    );
    310 
    311    /**
    312     * In HelperAppDlg.sys.mjs, we determine whether or not an "always open..." checkbox
    313     * should appear in the unknownContentType window. Here, we use similar checks to
    314     * determine if we should show the "always open similar files" context menu item.
    315     *
    316     * Note that we also read the content type using mimeInfo to detect better and available
    317     * mime types, given a file extension. Some sites default to "application/octet-stream",
    318     * further limiting what file types can be added to about:preferences, even for file types
    319     * that are in fact capable of being handled with a default application.
    320     *
    321     * There are also cases where download.contentType is undefined (ex. when opening
    322     * the context menu on a previously downloaded item via download history).
    323     * Using mimeInfo ensures that content type exists and prevents intermittence.
    324     */
    325    //
    326    let filename = PathUtils.filename(download.target.path);
    327 
    328    let isExemptExecutableExtension =
    329      Services.policies.isExemptExecutableExtension(
    330        download.source.originalUrl || download.source.url,
    331        filename?.split(".").at(-1)
    332      );
    333 
    334    let shouldNotRememberChoice =
    335      !mimeInfo?.type ||
    336      mimeInfo.type === "application/octet-stream" ||
    337      mimeInfo.type === "application/x-msdownload" ||
    338      mimeInfo.type === "application/x-msdos-program" ||
    339      (lazy.gReputationService.isExecutable(filename) &&
    340        !isExemptExecutableExtension) ||
    341      (mimeInfo.type === "text/plain" &&
    342        lazy.gReputationService.isBinary(download.target.path));
    343 
    344    alwaysOpenSimilarFilesItem.hidden =
    345      canViewInternally ||
    346      state !== DOWNLOAD_FINISHED ||
    347      shouldNotRememberChoice;
    348 
    349    // Update checkbox for "always open..." options.
    350    if (preferredAction === useSystemDefault) {
    351      alwaysUseSystemViewerItem.setAttribute("checked", "true");
    352      alwaysOpenSimilarFilesItem.setAttribute("checked", "true");
    353    } else {
    354      alwaysUseSystemViewerItem.removeAttribute("checked");
    355      alwaysOpenSimilarFilesItem.removeAttribute("checked");
    356    }
    357  },
    358 };
    359 
    360 XPCOMUtils.defineLazyPreferenceGetter(
    361  DownloadsViewUI,
    362  "clearHistoryOnDelete",
    363  "browser.download.clearHistoryOnDelete",
    364  0
    365 );
    366 
    367 DownloadsViewUI.BaseView = class {
    368  canClearDownloads(nodeContainer) {
    369    // Downloads can be cleared if there's at least one removable download in
    370    // the list (either a history download or a completed session download).
    371    // Because history downloads are always removable and are listed after the
    372    // session downloads, check from bottom to top.
    373    for (let elt = nodeContainer.lastChild; elt; elt = elt.previousSibling) {
    374      // Stopped, paused, and failed downloads with partial data are removed.
    375      let download = elt._shell.download;
    376      if (download.stopped && !(download.canceled && download.hasPartialData)) {
    377        return true;
    378      }
    379    }
    380    return false;
    381  }
    382 };
    383 
    384 /**
    385 * A download element shell is responsible for handling the commands and the
    386 * displayed data for a single element that uses the "download.xml" binding.
    387 *
    388 * The information to display is obtained through the associated Download object
    389 * from the JavaScript API for downloads, and commands are executed using a
    390 * combination of Download methods and DownloadsCommon.sys.mjs helper functions.
    391 *
    392 * Specialized versions of this shell must be defined, and they are required to
    393 * implement the "download" property or getter. Currently these objects are the
    394 * HistoryDownloadElementShell and the DownloadsViewItem for the panel. The
    395 * history view may use a HistoryDownload object in place of a Download object.
    396 */
    397 DownloadsViewUI.DownloadElementShell = function () {};
    398 
    399 DownloadsViewUI.DownloadElementShell.prototype = {
    400  /**
    401   * The richlistitem for the download, initialized by the derived object.
    402   */
    403  element: null,
    404 
    405  /**
    406   * Manages the "active" state of the shell. By default all the shells are
    407   * inactive, thus their UI is not updated. They must be activated when
    408   * entering the visible area.
    409   */
    410  ensureActive() {
    411    if (!this._active) {
    412      this._active = true;
    413      this.connect();
    414      this.onChanged();
    415    }
    416  },
    417  get active() {
    418    return !!this._active;
    419  },
    420 
    421  connect() {
    422    let document = this.element.ownerDocument;
    423    let downloadListItemFragment = gDownloadListItemFragments.get(document);
    424    // When changing the markup within the fragment, please ensure that
    425    // the functions within DownloadsView still operate correctly.
    426    if (!downloadListItemFragment) {
    427      let MozXULElement = document.defaultView.MozXULElement;
    428      downloadListItemFragment = MozXULElement.parseXULToFragment(`
    429        <hbox class="downloadMainArea" flex="1" align="center">
    430          <image class="downloadTypeIcon"/>
    431          <vbox class="downloadContainer" flex="1" pack="center">
    432            <description class="downloadTarget" crop="center"/>
    433            <description class="downloadDetails downloadDetailsNormal"
    434                         crop="end"/>
    435            <description class="downloadDetails downloadDetailsHover"
    436                         crop="end"/>
    437            <description class="downloadDetails downloadDetailsButtonHover"
    438                         crop="end"/>
    439          </vbox>
    440          <image class="downloadBlockedBadge" />
    441        </hbox>
    442        <button class="downloadButton"/>
    443      `);
    444      gDownloadListItemFragments.set(document, downloadListItemFragment);
    445    }
    446    this.element.setAttribute("active", true);
    447    this.element.setAttribute("orient", "horizontal");
    448    this.element.addEventListener("click", ev => {
    449      ev.target.ownerGlobal.DownloadsView.onDownloadClick(ev);
    450    });
    451    this.element.appendChild(
    452      document.importNode(downloadListItemFragment, true)
    453    );
    454    let downloadButton = this.element.querySelector(".downloadButton");
    455    downloadButton.addEventListener("command", function (event) {
    456      event.target.ownerGlobal.DownloadsView.onDownloadButton(event);
    457    });
    458    for (let [propertyName, selector] of [
    459      ["_downloadTypeIcon", ".downloadTypeIcon"],
    460      ["_downloadTarget", ".downloadTarget"],
    461      ["_downloadDetailsNormal", ".downloadDetailsNormal"],
    462      ["_downloadDetailsHover", ".downloadDetailsHover"],
    463      ["_downloadDetailsButtonHover", ".downloadDetailsButtonHover"],
    464      ["_downloadButton", ".downloadButton"],
    465    ]) {
    466      this[propertyName] = this.element.querySelector(selector);
    467    }
    468 
    469    // HTML elements can be created directly without using parseXULToFragment.
    470    let progress = (this._downloadProgress = document.createElementNS(
    471      HTML_NS,
    472      "progress"
    473    ));
    474    progress.className = "downloadProgress";
    475    progress.setAttribute("max", "100");
    476    this._downloadTarget.insertAdjacentElement("afterend", progress);
    477  },
    478 
    479  /**
    480   * URI string for the file type icon displayed in the download element.
    481   */
    482  get image() {
    483    if (!this.download.target.path) {
    484      // Old history downloads may not have a target path.
    485      return "moz-icon://.unknown?size=32";
    486    }
    487 
    488    // When a download that was previously in progress finishes successfully, it
    489    // means that the target file now exists and we can extract its specific
    490    // icon, for example from a Windows executable. To ensure that the icon is
    491    // reloaded, however, we must change the URI used by the XUL image element,
    492    // for example by adding a query parameter. This only works if we add one of
    493    // the parameters explicitly supported by the nsIMozIconURI interface.
    494    return (
    495      "moz-icon://" +
    496      this.download.target.path +
    497      "?size=32" +
    498      (this.download.succeeded ? "&state=normal" : "")
    499    );
    500  },
    501 
    502  get browserWindow() {
    503    return lazy.BrowserWindowTracker.getTopWindow({
    504      allowFromInactiveWorkspace: true,
    505    });
    506  },
    507 
    508  /**
    509   * Updates the display name and icon.
    510   *
    511   * @param displayName
    512   *        This is usually the full file name of the download without the path.
    513   * @param icon
    514   *        URL of the icon to load, generally from the "image" property.
    515   */
    516  showDisplayNameAndIcon(displayName, icon) {
    517    if (displayName.l10n) {
    518      let document = this.element.ownerDocument;
    519      document.l10n.setAttributes(
    520        this._downloadTarget,
    521        displayName.l10n.id,
    522        displayName.l10n.args
    523      );
    524    } else {
    525      this._downloadTarget.setAttribute("value", displayName);
    526      this._downloadTarget.setAttribute("tooltiptext", displayName);
    527    }
    528    this._downloadTypeIcon.setAttribute("src", icon);
    529  },
    530 
    531  /**
    532   * Updates the displayed progress bar.
    533   *
    534   * @param mode
    535   *        Either "normal" or "undetermined".
    536   * @param value
    537   *        Percentage of the progress bar to display, from 0 to 100.
    538   * @param paused
    539   *        True to display the progress bar style for paused downloads.
    540   */
    541  showProgress(mode, value, paused) {
    542    if (mode == "undetermined") {
    543      this._downloadProgress.removeAttribute("value");
    544    } else {
    545      this._downloadProgress.setAttribute("value", value);
    546    }
    547    this._downloadProgress.toggleAttribute("paused", !!paused);
    548  },
    549 
    550  /**
    551   * Updates the full status line.
    552   *
    553   * @param status
    554   *        Status line of the Downloads Panel or the Downloads View.
    555   * @param hoverStatus
    556   *        Label to show in the Downloads Panel when the mouse pointer is over
    557   *        the main area of the item. If not specified, this will be the same
    558   *        as the status line. This is ignored in the Downloads View. Type is
    559   *        either l10n object or string literal.
    560   */
    561  showStatus(status, hoverStatus = status) {
    562    let document = this.element.ownerDocument;
    563    if (status?.l10n) {
    564      document.l10n.setAttributes(
    565        this._downloadDetailsNormal,
    566        status.l10n.id,
    567        status.l10n.args
    568      );
    569    } else {
    570      this._downloadDetailsNormal.removeAttribute("data-l10n-id");
    571      this._downloadDetailsNormal.setAttribute("value", status);
    572      this._downloadDetailsNormal.setAttribute("tooltiptext", status);
    573    }
    574    if (hoverStatus?.l10n) {
    575      document.l10n.setAttributes(
    576        this._downloadDetailsHover,
    577        hoverStatus.l10n.id,
    578        hoverStatus.l10n.args
    579      );
    580    } else {
    581      this._downloadDetailsHover.removeAttribute("data-l10n-id");
    582      this._downloadDetailsHover.setAttribute("value", hoverStatus);
    583      this._downloadDetailsHover.setAttribute("tooltiptext", hoverStatus);
    584    }
    585  },
    586 
    587  /**
    588   * Updates the status line combining the given state label with other labels.
    589   *
    590   * @param stateLabel
    591   *        Label representing the state of the download, for example "Failed".
    592   *        In the Downloads Panel, this is the only text displayed when the
    593   *        the mouse pointer is not over the main area of the item. In the
    594   *        Downloads View, this label is combined with the host and date, for
    595   *        example "Failed - example.com - 1:45 PM".
    596   * @param hoverStatus
    597   *        Label to show in the Downloads Panel when the mouse pointer is over
    598   *        the main area of the item. If not specified, this will be the
    599   *        state label combined with the host and date. This is ignored in the
    600   *        Downloads View. Type is either l10n object or string literal.
    601   */
    602  showStatusWithDetails(stateLabel, hoverStatus) {
    603    if (stateLabel.l10n) {
    604      this.showStatus(stateLabel, hoverStatus);
    605      return;
    606    }
    607    let uri = URL.parse(this.download.source.url)?.URI;
    608    let displayHost = uri
    609      ? lazy.BrowserUtils.formatURIForDisplay(uri, {
    610          onlyBaseDomain: true,
    611        })
    612      : "";
    613 
    614    let [displayDate] = lazy.DownloadUtils.getReadableDates(
    615      new Date(this.download.endTime)
    616    );
    617 
    618    let firstPart = lazy.DownloadsCommon.strings.statusSeparator(
    619      stateLabel,
    620      displayHost
    621    );
    622    let fullStatus = lazy.DownloadsCommon.strings.statusSeparator(
    623      firstPart,
    624      displayDate
    625    );
    626 
    627    if (!this.isPanel) {
    628      this.showStatus(fullStatus);
    629    } else {
    630      this.showStatus(stateLabel, hoverStatus || fullStatus);
    631    }
    632  },
    633 
    634  /**
    635   * Updates the main action button and makes it visible.
    636   *
    637   * @param type
    638   *        One of the presets defined in gDownloadElementButtons.
    639   */
    640  showButton(type) {
    641    let { commandName, l10nId, descriptionL10nId, panelL10nId, iconClass } =
    642      gDownloadElementButtons[type];
    643 
    644    this.buttonCommandName = commandName;
    645    let stringId = this.isPanel ? panelL10nId : l10nId;
    646    let document = this.element.ownerDocument;
    647    document.l10n.setAttributes(this._downloadButton, stringId);
    648    if (this.isPanel && descriptionL10nId) {
    649      document.l10n.setAttributes(
    650        this._downloadDetailsButtonHover,
    651        descriptionL10nId
    652      );
    653    }
    654    this._downloadButton.setAttribute("class", "downloadButton " + iconClass);
    655    this._downloadButton.removeAttribute("hidden");
    656  },
    657 
    658  hideButton() {
    659    this._downloadButton.hidden = true;
    660  },
    661 
    662  lastEstimatedSecondsLeft: Infinity,
    663 
    664  /**
    665   * This is called when a major state change occurs in the download, but is not
    666   * called for every progress update in order to improve performance.
    667   */
    668  _updateState() {
    669    this.showDisplayNameAndIcon(
    670      DownloadsViewUI.getDisplayName(this.download),
    671      this.image
    672    );
    673    this.element.setAttribute(
    674      "state",
    675      lazy.DownloadsCommon.stateOfDownload(this.download)
    676    );
    677 
    678    if (!this.download.stopped) {
    679      // When the download becomes in progress, we make all the major changes to
    680      // the user interface here. The _updateStateInner function takes care of
    681      // displaying the right button type for all other state changes.
    682      this.showButton("cancel");
    683 
    684      // If there was a verdict set but the download is running we can assume
    685      // that the verdict has been overruled and can be removed.
    686      this.element.removeAttribute("verdict");
    687    }
    688 
    689    // Since state changed, reset the time left estimation.
    690    this.lastEstimatedSecondsLeft = Infinity;
    691 
    692    this._updateStateInner();
    693  },
    694 
    695  /**
    696   * This is called for all changes in the download, including progress updates.
    697   * For major state changes, _updateState is called first, but several elements
    698   * are still updated here. When the download is in progress, this function
    699   * takes a faster path with less element updates to improve performance.
    700   */
    701  _updateStateInner() {
    702    let progressPaused = false;
    703 
    704    this.element.classList.toggle("openWhenFinished", !this.download.stopped);
    705 
    706    if (!this.download.stopped) {
    707      // The download is in progress, so we don't change the button state
    708      // because the _updateState function already did it. We still need to
    709      // update all elements that may change during the download.
    710      let totalBytes = this.download.hasProgress
    711        ? this.download.totalBytes
    712        : -1;
    713      let [status, newEstimatedSecondsLeft] =
    714        lazy.DownloadUtils.getDownloadStatus(
    715          this.download.currentBytes,
    716          totalBytes,
    717          this.download.speed,
    718          this.lastEstimatedSecondsLeft
    719        );
    720      this.lastEstimatedSecondsLeft = newEstimatedSecondsLeft;
    721 
    722      if (this.download.launchWhenSucceeded) {
    723        status = lazy.DownloadUtils.getFormattedTimeStatus(
    724          newEstimatedSecondsLeft
    725        );
    726      }
    727      let hoverStatus = {
    728        l10n: { id: "downloading-file-click-to-open" },
    729      };
    730      this.showStatus(status, hoverStatus);
    731    } else {
    732      let verdict = "";
    733 
    734      // The download is not in progress, so we update the user interface based
    735      // on other properties. The order in which we check the properties of the
    736      // Download object is the same used by stateOfDownload.
    737      if (this.download.deleted) {
    738        this.showDeletedOrMissing();
    739      } else if (this.download.succeeded) {
    740        lazy.DownloadsCommon.log(
    741          "_updateStateInner, target exists? ",
    742          this.download.target.path,
    743          this.download.target.exists
    744        );
    745        if (this.download.target.exists) {
    746          // This is a completed download, and the target file still exists.
    747          this.element.setAttribute("exists", "true");
    748 
    749          this.element.toggleAttribute(
    750            "viewable-internally",
    751            lazy.DownloadIntegration.shouldViewDownloadInternally(
    752              lazy.DownloadsCommon.getMimeInfo(this.download)?.type
    753            )
    754          );
    755 
    756          let sizeWithUnits = DownloadsViewUI.getSizeWithUnits(this.download);
    757          if (this.isPanel) {
    758            // In the Downloads Panel, we show the file size after the state
    759            // label, for example "Completed - 1.5 MB". When the pointer is over
    760            // the main area of the item, this label is replaced with a
    761            // description of the default action, which opens the file.
    762            let status = lazy.DownloadsCommon.strings.stateCompleted;
    763            if (sizeWithUnits) {
    764              status = lazy.DownloadsCommon.strings.statusSeparator(
    765                status,
    766                sizeWithUnits
    767              );
    768            }
    769            this.showStatus(status, { l10n: { id: "downloads-open-file" } });
    770          } else {
    771            // In the Downloads View, we show the file size in place of the
    772            // state label, for example "1.5 MB - example.com - 1:45 PM".
    773            this.showStatusWithDetails(
    774              sizeWithUnits || lazy.DownloadsCommon.strings.sizeUnknown
    775            );
    776          }
    777          this.showButton("show");
    778        } else {
    779          // This is a completed download, but the target file does not exist
    780          // anymore, so the main action of opening the file is unavailable.
    781          this.showDeletedOrMissing();
    782        }
    783      } else if (this.download.error) {
    784        if (this.download.error.becauseBlockedByParentalControls) {
    785          // This download was blocked permanently by parental controls.
    786          this.showStatusWithDetails(
    787            lazy.DownloadsCommon.strings.stateBlockedParentalControls
    788          );
    789          this.hideButton();
    790        } else if (
    791          this.download.error.becauseBlockedByReputationCheck ||
    792          this.download.error.becauseBlockedByContentAnalysis
    793        ) {
    794          verdict = this.download.error.reputationCheckVerdict;
    795          let hover = "";
    796          if (!this.download.hasBlockedData) {
    797            // This download was blocked permanently by reputation check.
    798            this.hideButton();
    799          } else if (this.isPanel) {
    800            // This download was blocked temporarily by reputation check. In the
    801            // Downloads Panel, a subview can be used to remove the file or open
    802            // the download anyways.
    803            this.showButton("subviewOpenOrRemoveFile");
    804            hover = { l10n: { id: "downloads-show-more-information" } };
    805          } else {
    806            // This download was blocked temporarily by reputation check. In the
    807            // Downloads View, the interface depends on the threat severity.
    808            switch (verdict) {
    809              case lazy.Downloads.Error.BLOCK_VERDICT_UNCOMMON:
    810              case lazy.Downloads.Error.BLOCK_VERDICT_INSECURE:
    811              case lazy.Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
    812                // Keep the option the user chose on the save dialogue
    813                if (this.download.launchWhenSucceeded) {
    814                  this.showButton("askOpenOrRemoveFile");
    815                } else {
    816                  this.showButton("askRemoveFileOrAllow");
    817                }
    818                break;
    819              case lazy.Downloads.Error.BLOCK_VERDICT_DOWNLOAD_SPAM:
    820                this.showButton("askRemoveFileOrAllow");
    821                break;
    822              default:
    823                // Assume Downloads.Error.BLOCK_VERDICT_MALWARE
    824                this.showButton("removeFile");
    825                break;
    826            }
    827          }
    828          this.showStatusWithDetails(this.rawBlockedTitleAndDetails[0], hover);
    829        } else {
    830          // This download failed without being blocked, and can be restarted.
    831          this.showStatusWithDetails(
    832            lazy.DownloadsCommon.strings.stateFailed,
    833            this.download.error.localizedReason
    834          );
    835          this.showButton("retry");
    836        }
    837      } else if (this.download.canceled) {
    838        if (this.download.hasPartialData) {
    839          // This download was paused. The main action button will cancel the
    840          // download, and in both the Downloads Panel and the Downlods View the
    841          // status includes the size, for example "Paused - 1.1 MB".
    842          let totalBytes = this.download.hasProgress
    843            ? this.download.totalBytes
    844            : -1;
    845          let transfer = lazy.DownloadUtils.getTransferTotal(
    846            this.download.currentBytes,
    847            totalBytes
    848          );
    849          this.showStatus(
    850            lazy.DownloadsCommon.strings.statusSeparatorBeforeNumber(
    851              lazy.DownloadsCommon.strings.statePaused,
    852              transfer
    853            )
    854          );
    855          this.showButton("cancel");
    856          progressPaused = true;
    857        } else {
    858          // This download was canceled.
    859          this.showStatusWithDetails(
    860            lazy.DownloadsCommon.strings.stateCanceled
    861          );
    862          this.showButton("retry");
    863        }
    864      } else {
    865        // This download was added to the global list before it started. While
    866        // we still support this case, at the moment it can only be triggered by
    867        // internally developed add-ons and regression tests, and should not
    868        // happen unless there is a bug. This means the stateStarting string can
    869        // probably be removed when converting the localization to Fluent.
    870        this.showStatus(lazy.DownloadsCommon.strings.stateStarting);
    871        this.showButton("cancel");
    872      }
    873 
    874      // These attributes are only set in this slower code path, because they
    875      // are irrelevant for downloads that are in progress.
    876      if (verdict) {
    877        this.element.setAttribute("verdict", verdict);
    878      } else {
    879        this.element.removeAttribute("verdict");
    880      }
    881 
    882      this.element.classList.toggle(
    883        "temporary-block",
    884        !!this.download.hasBlockedData
    885      );
    886    }
    887 
    888    // These attributes are set in all code paths, because they are relevant for
    889    // downloads that are in progress and for other states.
    890    if (this.download.hasProgress) {
    891      this.showProgress("normal", this.download.progress, progressPaused);
    892    } else {
    893      this.showProgress("undetermined", 100, progressPaused);
    894    }
    895  },
    896 
    897  getContentAnalysisErrorTitle(strings, cancelError) {
    898    switch (cancelError) {
    899      case Ci.nsIContentAnalysisResponse.eNoAgent:
    900        return strings.contentAnalysisNoAgentError(
    901          lazy.contentAnalysisAgentName
    902        );
    903      case Ci.nsIContentAnalysisResponse.eInvalidAgentSignature:
    904        return strings.contentAnalysisInvalidAgentSignatureError(
    905          lazy.contentAnalysisAgentName
    906        );
    907      case Ci.nsIContentAnalysisResponse.eTimeout:
    908        return strings.contentAnalysisTimeoutError(
    909          lazy.contentAnalysisAgentName
    910        );
    911      case Ci.nsIContentAnalysisResponse.eErrorOther:
    912        return strings.contentAnalysisUnspecifiedError(
    913          lazy.contentAnalysisAgentName
    914        );
    915      default:
    916        // This also handles the case when cancelError is undefined
    917        // because the request wasn't cancelled at all.
    918        return strings.blockedByContentAnalysis;
    919    }
    920  },
    921 
    922  /**
    923   * Returns [title, [details1, details2]] for blocked downloads.
    924   * The title or details could be raw strings or l10n objects.
    925   */
    926  get rawBlockedTitleAndDetails() {
    927    let s = lazy.DownloadsCommon.strings;
    928    if (
    929      !this.download.error ||
    930      (!this.download.error.becauseBlockedByReputationCheck &&
    931        !this.download.error.becauseBlockedByContentAnalysis)
    932    ) {
    933      return [null, null];
    934    }
    935    switch (this.download.error.reputationCheckVerdict) {
    936      case lazy.Downloads.Error.BLOCK_VERDICT_UNCOMMON:
    937        return [s.blockedUncommon2, [s.unblockTypeUncommon2, s.unblockTip2]];
    938      case lazy.Downloads.Error.BLOCK_VERDICT_INSECURE:
    939        return [
    940          s.blockedPotentiallyInsecure,
    941          [s.unblockInsecure2, s.unblockTip2],
    942        ];
    943      case lazy.Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
    944        if (this.download.error.becauseBlockedByReputationCheck) {
    945          return [
    946            s.blockedPotentiallyUnwanted,
    947            [s.unblockTypePotentiallyUnwanted2, s.unblockTip2],
    948          ];
    949        }
    950        if (!this.download.error.becauseBlockedByContentAnalysis) {
    951          // We expect one of becauseBlockedByReputationCheck or
    952          // becauseBlockedByContentAnalysis to be true; if not,
    953          // fall through to the error case.
    954          break;
    955        }
    956        return [
    957          s.warnedByContentAnalysis,
    958          [s.unblockTypeContentAnalysisWarn, s.unblockContentAnalysisWarnTip],
    959        ];
    960      case lazy.Downloads.Error.BLOCK_VERDICT_MALWARE:
    961        if (this.download.error.becauseBlockedByReputationCheck) {
    962          return [s.blockedMalware, [s.unblockTypeMalware, s.unblockTip2]];
    963        }
    964        if (!this.download.error.becauseBlockedByContentAnalysis) {
    965          // We expect one of becauseBlockedByReputationCheck or
    966          // becauseBlockedByContentAnalysis to be true; if not,
    967          // fall through to the error case.
    968          break;
    969        }
    970        return [
    971          this.getContentAnalysisErrorTitle(
    972            s,
    973            this.download.error.contentAnalysisCancelError
    974          ),
    975          [s.unblockContentAnalysis1, s.unblockContentAnalysis2],
    976        ];
    977      case lazy.Downloads.Error.BLOCK_VERDICT_DOWNLOAD_SPAM: {
    978        let title = {
    979          id: "downloads-files-not-downloaded",
    980          args: {
    981            num: this.download.blockedDownloadsCount,
    982          },
    983        };
    984        let details = {
    985          id: "downloads-blocked-download-detailed-info",
    986          args: { url: DownloadsViewUI.getStrippedUrl(this.download) },
    987        };
    988        return [{ l10n: title }, [{ l10n: details }, null]];
    989      }
    990    }
    991    throw new Error(
    992      "Unexpected reputationCheckVerdict: " +
    993        this.download.error.reputationCheckVerdict
    994    );
    995  },
    996 
    997  showDeletedOrMissing() {
    998    this.element.removeAttribute("exists");
    999    let label =
   1000      lazy.DownloadsCommon.strings[
   1001        this.download.deleted ? "fileDeleted" : "fileMovedOrMissing"
   1002      ];
   1003    this.showStatusWithDetails(label, label);
   1004    this.hideButton();
   1005  },
   1006 
   1007  /**
   1008   * Shows the appropriate unblock dialog based on the verdict, and executes the
   1009   * action selected by the user in the dialog, which may involve unblocking,
   1010   * opening or removing the file.
   1011   *
   1012   * @param window
   1013   *        The window to which the dialog should be anchored.
   1014   * @param dialogType
   1015   *        Can be "unblock", "chooseUnblock", or "chooseOpen".
   1016   */
   1017  confirmUnblock(window, dialogType) {
   1018    lazy.DownloadsCommon.confirmUnblockDownload({
   1019      verdict: this.download.error.reputationCheckVerdict,
   1020      becauseBlockedByReputationCheck:
   1021        this.download.error.becauseBlockedByReputationCheck,
   1022      window,
   1023      dialogType,
   1024    })
   1025      .then(action => {
   1026        if (action == "open") {
   1027          return this.unblockAndOpenDownload();
   1028        } else if (action == "unblock") {
   1029          return this.download.unblock();
   1030        } else if (action == "confirmBlock") {
   1031          return this.download.confirmBlock();
   1032        }
   1033        return Promise.resolve();
   1034      })
   1035      .catch(console.error);
   1036  },
   1037 
   1038  /**
   1039   * Unblocks the downloaded file and opens it.
   1040   *
   1041   * @return A promise that's resolved after the file has been opened.
   1042   */
   1043  unblockAndOpenDownload() {
   1044    return this.download.unblock().then(() => this.downloadsCmd_open());
   1045  },
   1046 
   1047  unblockAndSave() {
   1048    return this.download.unblock();
   1049  },
   1050  /**
   1051   * Returns the name of the default command to use for the current state of the
   1052   * download, when there is a double click or another default interaction. If
   1053   * there is no default command for the current state, returns an empty string.
   1054   * The commands are implemented as functions on this object or derived ones.
   1055   */
   1056  get currentDefaultCommandName() {
   1057    switch (lazy.DownloadsCommon.stateOfDownload(this.download)) {
   1058      case lazy.DownloadsCommon.DOWNLOAD_NOTSTARTED:
   1059        return "downloadsCmd_cancel";
   1060      case lazy.DownloadsCommon.DOWNLOAD_FAILED:
   1061      case lazy.DownloadsCommon.DOWNLOAD_CANCELED:
   1062        return "downloadsCmd_retry";
   1063      case lazy.DownloadsCommon.DOWNLOAD_PAUSED:
   1064        return "downloadsCmd_pauseResume";
   1065      case lazy.DownloadsCommon.DOWNLOAD_FINISHED:
   1066        return "downloadsCmd_open";
   1067      case lazy.DownloadsCommon.DOWNLOAD_BLOCKED_PARENTAL:
   1068      case lazy.DownloadsCommon.DOWNLOAD_BLOCKED_CONTENT_ANALYSIS:
   1069        return "downloadsCmd_openReferrer";
   1070      case lazy.DownloadsCommon.DOWNLOAD_DIRTY:
   1071        return "downloadsCmd_showBlockedInfo";
   1072    }
   1073    return "";
   1074  },
   1075 
   1076  /**
   1077   * Returns true if the specified command can be invoked on the current item.
   1078   * The commands are implemented as functions on this object or derived ones.
   1079   *
   1080   * @param aCommand
   1081   *        Name of the command to check, for example "downloadsCmd_retry".
   1082   */
   1083  isCommandEnabled(aCommand) {
   1084    switch (aCommand) {
   1085      case "downloadsCmd_retry":
   1086        return this.download.canceled || !!this.download.error;
   1087      case "downloadsCmd_pauseResume":
   1088        return this.download.hasPartialData && !this.download.error;
   1089      case "downloadsCmd_openReferrer": {
   1090        let referrer = this.download.source.referrerInfo?.originalReferrer;
   1091        return !!referrer && referrer.asciiSpec != "about:blank";
   1092      }
   1093      case "downloadsCmd_confirmBlock":
   1094      case "downloadsCmd_chooseUnblock":
   1095      case "downloadsCmd_chooseOpen":
   1096      case "downloadsCmd_unblock":
   1097      case "downloadsCmd_unblockAndSave":
   1098      case "downloadsCmd_unblockAndOpen":
   1099        return this.download.hasBlockedData;
   1100      case "downloadsCmd_cancel":
   1101        return this.download.hasPartialData || !this.download.stopped;
   1102      case "downloadsCmd_open":
   1103      case "downloadsCmd_open:current":
   1104      case "downloadsCmd_open:tab":
   1105      case "downloadsCmd_open:tabshifted":
   1106      case "downloadsCmd_open:window":
   1107      case "downloadsCmd_alwaysOpenSimilarFiles":
   1108        // This property is false if the download did not succeed.
   1109        return this.download.target.exists;
   1110 
   1111      case "downloadsCmd_show":
   1112      case "downloadsCmd_deleteFile": {
   1113        let { target } = this.download;
   1114        return (
   1115          !this.download.deleted && (target.exists || target.partFileExists)
   1116        );
   1117      }
   1118      case "downloadsCmd_delete":
   1119      case "cmd_delete":
   1120        // We don't want in-progress downloads to be removed accidentally.
   1121        return this.download.stopped;
   1122      case "downloadsCmd_openInSystemViewer":
   1123      case "downloadsCmd_alwaysOpenInSystemViewer":
   1124        return lazy.DownloadIntegration.shouldViewDownloadInternally(
   1125          lazy.DownloadsCommon.getMimeInfo(this.download)?.type
   1126        );
   1127    }
   1128    return DownloadsViewUI.isCommandName(aCommand) && !!this[aCommand];
   1129  },
   1130 
   1131  doCommand(aCommand) {
   1132    // split off an optional command "modifier" into an argument,
   1133    // e.g. "downloadsCmd_open:window"
   1134    let [command, modifier] = aCommand.split(":");
   1135    if (DownloadsViewUI.isCommandName(command)) {
   1136      this[command](modifier);
   1137    }
   1138  },
   1139 
   1140  onButton() {
   1141    this.doCommand(this.buttonCommandName);
   1142  },
   1143 
   1144  downloadsCmd_cancel() {
   1145    // This is the correct way to avoid race conditions when cancelling.
   1146    this.download.cancel().catch(() => {});
   1147    this.download
   1148      .removePartialData()
   1149      .catch(console.error)
   1150      .finally(() => this.download.target.refresh());
   1151  },
   1152 
   1153  downloadsCmd_confirmBlock() {
   1154    this.download.confirmBlock().catch(console.error);
   1155  },
   1156 
   1157  downloadsCmd_open(openWhere = "tab") {
   1158    lazy.DownloadsCommon.openDownload(this.download, {
   1159      openWhere,
   1160    });
   1161  },
   1162 
   1163  downloadsCmd_openReferrer() {
   1164    this.element.ownerGlobal.openURL(
   1165      this.download.source.referrerInfo.originalReferrer
   1166    );
   1167  },
   1168 
   1169  downloadsCmd_pauseResume() {
   1170    if (this.download.stopped) {
   1171      this.download.start();
   1172    } else {
   1173      this.download.cancel();
   1174    }
   1175  },
   1176 
   1177  downloadsCmd_show() {
   1178    let file = new lazy.FileUtils.File(this.download.target.path);
   1179    lazy.DownloadsCommon.showDownloadedFile(file);
   1180  },
   1181 
   1182  downloadsCmd_retry() {
   1183    if (this.download.start) {
   1184      // Errors when retrying are already reported as download failures.
   1185      this.download.start().catch(() => {});
   1186      return;
   1187    }
   1188 
   1189    let window = this.browserWindow || this.element.ownerGlobal;
   1190    let document = window.document;
   1191 
   1192    // Do not suggest a file name if we don't know the original target.
   1193    let targetPath = this.download.target.path
   1194      ? PathUtils.filename(this.download.target.path)
   1195      : null;
   1196    window.DownloadURL(this.download.source.url, targetPath, document);
   1197  },
   1198 
   1199  downloadsCmd_delete() {
   1200    // Alias for the 'cmd_delete' command, because it may clash with another
   1201    // controller which causes unexpected behavior as different codepaths claim
   1202    // ownership.
   1203    this.cmd_delete();
   1204  },
   1205 
   1206  cmd_delete() {
   1207    lazy.DownloadsCommon.deleteDownload(this.download).catch(console.error);
   1208  },
   1209 
   1210  async downloadsCmd_deleteFile() {
   1211    // Remove the download from the session and history downloads, delete part files.
   1212    await lazy.DownloadsCommon.deleteDownloadFiles(
   1213      this.download,
   1214      DownloadsViewUI.clearHistoryOnDelete
   1215    );
   1216  },
   1217 
   1218  downloadsCmd_openInSystemViewer() {
   1219    // For this interaction only, pass a flag to override the preferredAction for this
   1220    // mime-type and open using the system viewer
   1221    lazy.DownloadsCommon.openDownload(this.download, {
   1222      useSystemDefault: true,
   1223    }).catch(console.error);
   1224  },
   1225 
   1226  downloadsCmd_alwaysOpenInSystemViewer() {
   1227    // this command toggles between setting preferredAction for this mime-type to open
   1228    // using the system viewer, or to open the file in browser.
   1229    const mimeInfo = lazy.DownloadsCommon.getMimeInfo(this.download);
   1230    if (!mimeInfo) {
   1231      throw new Error(
   1232        "Can't open download with unknown mime-type in system viewer"
   1233      );
   1234    }
   1235    if (mimeInfo.preferredAction !== mimeInfo.useSystemDefault) {
   1236      // User has selected to open this mime-type with the system viewer from now on
   1237      lazy.DownloadsCommon.log(
   1238        "downloadsCmd_alwaysOpenInSystemViewer command for download: ",
   1239        this.download,
   1240        "switching to use system default for " + mimeInfo.type
   1241      );
   1242      mimeInfo.preferredAction = mimeInfo.useSystemDefault;
   1243      mimeInfo.alwaysAskBeforeHandling = false;
   1244    } else {
   1245      lazy.DownloadsCommon.log(
   1246        "downloadsCmd_alwaysOpenInSystemViewer command for download: ",
   1247        this.download,
   1248        "currently uses system default, switching to handleInternally"
   1249      );
   1250      // User has selected to not open this mime-type with the system viewer
   1251      mimeInfo.preferredAction = mimeInfo.handleInternally;
   1252    }
   1253    lazy.handlerSvc.store(mimeInfo);
   1254    lazy.DownloadsCommon.openDownload(this.download).catch(console.error);
   1255  },
   1256 
   1257  downloadsCmd_alwaysOpenSimilarFiles() {
   1258    const mimeInfo = lazy.DownloadsCommon.getMimeInfo(this.download);
   1259    if (!mimeInfo) {
   1260      throw new Error("Can't open download with unknown mime-type");
   1261    }
   1262 
   1263    // User has selected to always open this mime-type from now on and will add this
   1264    // mime-type to our preferences table with the system default option. Open the
   1265    // file immediately after selecting the menu item like alwaysOpenInSystemViewer.
   1266    if (mimeInfo.preferredAction !== mimeInfo.useSystemDefault) {
   1267      mimeInfo.preferredAction = mimeInfo.useSystemDefault;
   1268      lazy.handlerSvc.store(mimeInfo);
   1269      lazy.DownloadsCommon.openDownload(this.download).catch(console.error);
   1270    } else {
   1271      // Otherwise, if user unchecks this option after already enabling it from the
   1272      // context menu, resort to saveToDisk.
   1273      mimeInfo.preferredAction = mimeInfo.saveToDisk;
   1274      lazy.handlerSvc.store(mimeInfo);
   1275    }
   1276  },
   1277 };