tor-browser

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

DownloadsCommon.sys.mjs (54827B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
      2 /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
      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 shared methods and data access.
      9 *
     10 * This file includes the following constructors and global objects:
     11 *
     12 * DownloadsCommon
     13 * This object is exposed directly to the consumers of this JavaScript module,
     14 * and provides shared methods for all the instances of the user interface.
     15 *
     16 * DownloadsData
     17 * Retrieves the list of past and completed downloads from the underlying
     18 * Downloads API data, and provides asynchronous notifications allowing
     19 * to build a consistent view of the available data.
     20 *
     21 * DownloadsIndicatorData
     22 * This object registers itself with DownloadsData as a view, and transforms the
     23 * notifications it receives into overall status data, that is then broadcast to
     24 * the registered download status indicators.
     25 */
     26 
     27 // Globals
     28 
     29 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     30 
     31 const lazy = {};
     32 
     33 ChromeUtils.defineESModuleGetters(lazy, {
     34  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
     35  DownloadHistory: "resource://gre/modules/DownloadHistory.sys.mjs",
     36  DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
     37  Downloads: "resource://gre/modules/Downloads.sys.mjs",
     38  NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
     39  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     40  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     41 });
     42 
     43 XPCOMUtils.defineLazyServiceGetters(lazy, {
     44  gClipboardHelper: [
     45    "@mozilla.org/widget/clipboardhelper;1",
     46    Ci.nsIClipboardHelper,
     47  ],
     48  gMIMEService: ["@mozilla.org/mime;1", Ci.nsIMIMEService],
     49 });
     50 
     51 ChromeUtils.defineLazyGetter(lazy, "DownloadsLogger", () => {
     52  let { ConsoleAPI } = ChromeUtils.importESModule(
     53    "resource://gre/modules/Console.sys.mjs"
     54  );
     55  let consoleOptions = {
     56    maxLogLevelPref: "toolkit.download.loglevel",
     57    prefix: "Downloads",
     58  };
     59  return new ConsoleAPI(consoleOptions);
     60 });
     61 
     62 XPCOMUtils.defineLazyPreferenceGetter(
     63  lazy,
     64  "gAlwaysOpenPanel",
     65  "browser.download.alwaysOpenPanel",
     66  true
     67 );
     68 
     69 const kDownloadsStringBundleUrl =
     70  "chrome://browser/locale/downloads/downloads.properties";
     71 
     72 const kDownloadsFluentStrings = new Localization(
     73  ["browser/downloads.ftl"],
     74  true
     75 );
     76 
     77 const kDownloadsStringsRequiringFormatting = {
     78  contentAnalysisNoAgentError: true,
     79  contentAnalysisInvalidAgentSignatureError: true,
     80  contentAnalysisUnspecifiedError: true,
     81  contentAnalysisTimeoutError: true,
     82  sizeWithUnits: true,
     83  statusSeparator: true,
     84  statusSeparatorBeforeNumber: true,
     85 };
     86 
     87 const kMaxHistoryResultsForLimitedView = 42;
     88 
     89 const kPrefBranch = Services.prefs.getBranch("browser.download.");
     90 
     91 const kGenericContentTypes = [
     92  "application/octet-stream",
     93  "binary/octet-stream",
     94  "application/unknown",
     95 ];
     96 
     97 var PrefObserver = {
     98  QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
     99  getPref(name) {
    100    try {
    101      switch (typeof this.prefs[name]) {
    102        case "boolean":
    103          return kPrefBranch.getBoolPref(name);
    104      }
    105    } catch (ex) {}
    106    return this.prefs[name];
    107  },
    108  observe(aSubject, aTopic, aData) {
    109    if (this.prefs.hasOwnProperty(aData)) {
    110      delete this[aData];
    111      this[aData] = this.getPref(aData);
    112    }
    113  },
    114  register(prefs) {
    115    this.prefs = prefs;
    116    kPrefBranch.addObserver("", this);
    117    for (let key in prefs) {
    118      let name = key;
    119      ChromeUtils.defineLazyGetter(this, name, function () {
    120        return PrefObserver.getPref(name);
    121      });
    122    }
    123  },
    124 };
    125 
    126 PrefObserver.register({
    127  // prefName: defaultValue
    128  openInSystemViewerContextMenuItem: true,
    129  alwaysOpenInSystemViewerContextMenuItem: true,
    130 });
    131 
    132 // DownloadsCommon
    133 
    134 /**
    135 * This object is exposed directly to the consumers of this JavaScript module,
    136 * and provides shared methods for all the instances of the user interface.
    137 */
    138 export var DownloadsCommon = {
    139  // The following legacy constants are still returned by stateOfDownload, but
    140  // individual properties of the Download object should normally be used.
    141  DOWNLOAD_NOTSTARTED: -1,
    142  DOWNLOAD_DOWNLOADING: 0,
    143  DOWNLOAD_FINISHED: 1,
    144  DOWNLOAD_FAILED: 2,
    145  DOWNLOAD_CANCELED: 3,
    146  DOWNLOAD_PAUSED: 4,
    147  DOWNLOAD_BLOCKED_PARENTAL: 6,
    148  DOWNLOAD_DIRTY: 8,
    149  DOWNLOAD_BLOCKED_POLICY: 9,
    150  DOWNLOAD_BLOCKED_CONTENT_ANALYSIS: 10,
    151 
    152  // The following are the possible values of the "attention" property.
    153  ATTENTION_NONE: "",
    154  ATTENTION_SUCCESS: "success",
    155  ATTENTION_INFO: "info",
    156  ATTENTION_WARNING: "warning",
    157  ATTENTION_SEVERE: "severe",
    158 
    159  // Bit flags for the attentionSuppressed property.
    160  SUPPRESS_NONE: 0,
    161  SUPPRESS_PANEL_OPEN: 1,
    162  SUPPRESS_ALL_DOWNLOADS_OPEN: 2,
    163  SUPPRESS_CONTENT_AREA_DOWNLOADS_OPEN: 4,
    164 
    165  /**
    166   * Returns an object whose keys are the string names from the downloads string
    167   * bundle, and whose values are either the translated strings or functions
    168   * returning formatted strings.
    169   */
    170  get strings() {
    171    let strings = {};
    172    let sb = Services.strings.createBundle(kDownloadsStringBundleUrl);
    173    for (let string of sb.getSimpleEnumeration()) {
    174      let stringName = string.key;
    175      if (stringName in kDownloadsStringsRequiringFormatting) {
    176        strings[stringName] = function () {
    177          // Convert "arguments" to a real array before calling into XPCOM.
    178          return sb.formatStringFromName(stringName, Array.from(arguments));
    179        };
    180      } else {
    181        strings[stringName] = string.value;
    182      }
    183    }
    184    delete this.strings;
    185    return (this.strings = strings);
    186  },
    187 
    188  /**
    189   * Indicates whether or not to show the 'Open in system viewer' context menu item when appropriate
    190   */
    191  get openInSystemViewerItemEnabled() {
    192    return PrefObserver.openInSystemViewerContextMenuItem;
    193  },
    194 
    195  /**
    196   * Indicates whether or not to show the 'Always open...' context menu item when appropriate
    197   */
    198  get alwaysOpenInSystemViewerItemEnabled() {
    199    return PrefObserver.alwaysOpenInSystemViewerContextMenuItem;
    200  },
    201 
    202  /**
    203   * Get access to one of the DownloadsData, PrivateDownloadsData, or
    204   * HistoryDownloadsData objects, depending on the privacy status of the
    205   * specified window and on whether history downloads should be included.
    206   *
    207   * @param [optional] window
    208   *        The browser window which owns the download button.
    209   *        If not given, the privacy status will be assumed as non-private.
    210   * @param [optional] history
    211   *        True to include history downloads when the window is public.
    212   * @param [optional] privateAll
    213   *        Whether to force the public downloads data to be returned together
    214   *        with the private downloads data for a private window.
    215   * @param [optional] limited
    216   *        True to limit the amount of downloads returned to
    217   *        `kMaxHistoryResultsForLimitedView`.
    218   */
    219  getData(window, history = false, privateAll = false, limited = false) {
    220    let isPrivate =
    221      window && lazy.PrivateBrowsingUtils.isContentWindowPrivate(window);
    222    if (isPrivate && !privateAll) {
    223      return lazy.PrivateDownloadsData;
    224    }
    225    if (history) {
    226      if (isPrivate && privateAll) {
    227        return lazy.LimitedPrivateHistoryDownloadData;
    228      }
    229      return limited
    230        ? lazy.LimitedHistoryDownloadsData
    231        : lazy.HistoryDownloadsData;
    232    }
    233    return lazy.DownloadsData;
    234  },
    235 
    236  /**
    237   * Initializes the Downloads back-end and starts receiving events for both the
    238   * private and non-private downloads data objects.
    239   */
    240  initializeAllDataLinks() {
    241    lazy.DownloadsData.initializeDataLink();
    242    lazy.PrivateDownloadsData.initializeDataLink();
    243  },
    244 
    245  /**
    246   * Get access to one of the DownloadsIndicatorData or
    247   * PrivateDownloadsIndicatorData objects, depending on the privacy status of
    248   * the window in question.
    249   */
    250  getIndicatorData(aWindow) {
    251    if (lazy.PrivateBrowsingUtils.isContentWindowPrivate(aWindow)) {
    252      return lazy.PrivateDownloadsIndicatorData;
    253    }
    254    return lazy.DownloadsIndicatorData;
    255  },
    256 
    257  /**
    258   * Returns a reference to the DownloadsSummaryData singleton - creating one
    259   * in the process if one hasn't been instantiated yet.
    260   *
    261   * @param aWindow
    262   *        The browser window which owns the download button.
    263   * @param aNumToExclude
    264   *        The number of items on the top of the downloads list to exclude
    265   *        from the summary.
    266   */
    267  getSummary(aWindow, aNumToExclude) {
    268    if (lazy.PrivateBrowsingUtils.isContentWindowPrivate(aWindow)) {
    269      if (this._privateSummary) {
    270        return this._privateSummary;
    271      }
    272      return (this._privateSummary = new DownloadsSummaryData(
    273        true,
    274        aNumToExclude
    275      ));
    276    }
    277    if (this._summary) {
    278      return this._summary;
    279    }
    280    return (this._summary = new DownloadsSummaryData(false, aNumToExclude));
    281  },
    282  _summary: null,
    283  _privateSummary: null,
    284 
    285  /**
    286   * Returns the legacy state integer value for the provided Download object.
    287   */
    288  stateOfDownload(download) {
    289    // Collapse state using the correct priority.
    290    if (!download.stopped) {
    291      return DownloadsCommon.DOWNLOAD_DOWNLOADING;
    292    }
    293    if (download.succeeded) {
    294      return DownloadsCommon.DOWNLOAD_FINISHED;
    295    }
    296    if (download.error) {
    297      if (download.error.becauseBlockedByParentalControls) {
    298        return DownloadsCommon.DOWNLOAD_BLOCKED_PARENTAL;
    299      }
    300      if (download.error.becauseBlockedByReputationCheck) {
    301        return DownloadsCommon.DOWNLOAD_DIRTY;
    302      }
    303      if (download.error.becauseBlockedByContentAnalysis) {
    304        // BLOCK_VERDICT_MALWARE indicates that the download was
    305        // blocked by the content analysis service, so return
    306        // DOWNLOAD_BLOCKED_CONTENT_ANALYSIS to indicate this.
    307        // Otherwise, the content analysis service returned
    308        // WARN, so the user has a chance to unblock the download,
    309        // which corresponds with DOWNLOAD_DIRTY.
    310        return download.error.reputationCheckVerdict ===
    311          lazy.Downloads.Error.BLOCK_VERDICT_MALWARE
    312          ? DownloadsCommon.DOWNLOAD_BLOCKED_CONTENT_ANALYSIS
    313          : DownloadsCommon.DOWNLOAD_DIRTY;
    314      }
    315      return DownloadsCommon.DOWNLOAD_FAILED;
    316    }
    317    if (download.canceled) {
    318      if (download.hasPartialData) {
    319        return DownloadsCommon.DOWNLOAD_PAUSED;
    320      }
    321      return DownloadsCommon.DOWNLOAD_CANCELED;
    322    }
    323    return DownloadsCommon.DOWNLOAD_NOTSTARTED;
    324  },
    325 
    326  /**
    327   * Removes a Download object from both session and history downloads.
    328   */
    329  async deleteDownload(download) {
    330    // Check hasBlockedData to avoid double counting if you click the X button
    331    // in the Library view and then delete the download from the history.
    332    if (
    333      download.error?.becauseBlockedByReputationCheck &&
    334      download.hasBlockedData
    335    ) {
    336      Glean.downloads.userActionOnBlockedDownload[
    337        download.error.reputationCheckVerdict
    338      ].accumulateSingleSample(1); // confirm block
    339    }
    340 
    341    // Remove the associated history element first, if any, so that the views
    342    // that combine history and session downloads won't resurrect the history
    343    // download into the view just before it is deleted permanently.
    344    try {
    345      await lazy.PlacesUtils.history.remove(download.source.url);
    346    } catch (ex) {
    347      console.error(ex);
    348    }
    349    let list = await lazy.Downloads.getList(lazy.Downloads.ALL);
    350    await list.remove(download);
    351    if (download.error?.becauseBlockedByContentAnalysis) {
    352      await download.respondToContentAnalysisWarnWithBlock();
    353    }
    354    await download.finalize(true);
    355  },
    356 
    357  /**
    358   * Deletes all files associated with a download, with or without removing it
    359   * from the session downloads list and/or download history.
    360   *
    361   * @param download
    362   *        The download to delete and/or forget.
    363   * @param clearHistory
    364   *        Optional. Removes history from session downloads list or history.
    365   *        0 - Don't remove the download from session list or history.
    366   *        1 - Remove the download from session list, but not history.
    367   *        2 - Remove the download from both session list and history.
    368   */
    369  async deleteDownloadFiles(download, clearHistory = 0) {
    370    if (clearHistory > 1) {
    371      try {
    372        await lazy.PlacesUtils.history.remove(download.source.url);
    373      } catch (ex) {
    374        console.error(ex);
    375      }
    376    }
    377    if (clearHistory > 0) {
    378      let list = await lazy.Downloads.getList(lazy.Downloads.ALL);
    379      await list.remove(download);
    380    }
    381    await download.manuallyRemoveData();
    382    if (download.error?.becauseBlockedByContentAnalysis) {
    383      await download.respondToContentAnalysisWarnWithBlock();
    384    }
    385    if (clearHistory < 2) {
    386      lazy.DownloadHistory.updateMetaData(download).catch(console.error);
    387    }
    388  },
    389 
    390  /**
    391   * Get a nsIMIMEInfo object for a download
    392   */
    393  getMimeInfo(download) {
    394    if (!download.succeeded) {
    395      return null;
    396    }
    397    let contentType = download.contentType;
    398    let url = Cc["@mozilla.org/network/standard-url-mutator;1"]
    399      .createInstance(Ci.nsIURIMutator)
    400      .setSpec("http://example.com") // construct the URL
    401      .setFilePath(download.target.path)
    402      .finalize()
    403      .QueryInterface(Ci.nsIURL);
    404    let fileExtension = url.fileExtension;
    405 
    406    // look at file extension if there's no contentType or it is generic
    407    if (!contentType || kGenericContentTypes.includes(contentType)) {
    408      try {
    409        contentType = lazy.gMIMEService.getTypeFromExtension(fileExtension);
    410      } catch (ex) {
    411        DownloadsCommon.log(
    412          "Cant get mimeType from file extension: ",
    413          fileExtension
    414        );
    415      }
    416    }
    417    if (!(contentType || fileExtension)) {
    418      return null;
    419    }
    420    let mimeInfo = null;
    421    try {
    422      mimeInfo = lazy.gMIMEService.getFromTypeAndExtension(
    423        contentType || "",
    424        fileExtension || ""
    425      );
    426    } catch (ex) {
    427      DownloadsCommon.log(
    428        "Can't get nsIMIMEInfo for contentType: ",
    429        contentType,
    430        "and fileExtension:",
    431        fileExtension
    432      );
    433    }
    434    return mimeInfo;
    435  },
    436 
    437  /**
    438   * Confirm if the download exists on the filesystem and is a given mime-type
    439   */
    440  isFileOfType(download, mimeType) {
    441    if (!(download.succeeded && download.target?.exists)) {
    442      DownloadsCommon.log(
    443        `isFileOfType returning false for mimeType: ${mimeType}, succeeded: ${download.succeeded}, exists: ${download.target?.exists}`
    444      );
    445      return false;
    446    }
    447    let mimeInfo = DownloadsCommon.getMimeInfo(download);
    448    return mimeInfo?.type === mimeType.toLowerCase();
    449  },
    450 
    451  /**
    452   * Copies the source URI of the given Download object to the clipboard.
    453   */
    454  copyDownloadLink(download) {
    455    lazy.gClipboardHelper.copyString(
    456      download.source.originalUrl || download.source.url
    457    );
    458  },
    459 
    460  /**
    461   * Given an iterable collection of Download objects, generates and returns
    462   * statistics about that collection.
    463   *
    464   * @param downloads An iterable collection of Download objects.
    465   *
    466   * @return Object whose properties are the generated statistics. Currently,
    467   *         we return the following properties:
    468   *
    469   *         numActive       : The total number of downloads.
    470   *         numPaused       : The total number of paused downloads.
    471   *         numDownloading  : The total number of downloads being downloaded.
    472   *         totalSize       : The total size of all downloads once completed.
    473   *         totalTransferred: The total amount of transferred data for these
    474   *                           downloads.
    475   *         slowestSpeed    : The slowest download rate.
    476   *         rawTimeLeft     : The estimated time left for the downloads to
    477   *                           complete.
    478   *         percentComplete : The percentage of bytes successfully downloaded.
    479   */
    480  summarizeDownloads(downloads) {
    481    let summary = {
    482      numActive: 0,
    483      numPaused: 0,
    484      numDownloading: 0,
    485      totalSize: 0,
    486      totalTransferred: 0,
    487      // slowestSpeed is Infinity so that we can use Math.min to
    488      // find the slowest speed. We'll set this to 0 afterwards if
    489      // it's still at Infinity by the time we're done iterating all
    490      // download.
    491      slowestSpeed: Infinity,
    492      rawTimeLeft: -1,
    493      percentComplete: -1,
    494    };
    495 
    496    for (let download of downloads) {
    497      summary.numActive++;
    498 
    499      if (!download.stopped) {
    500        summary.numDownloading++;
    501        if (download.hasProgress && download.speed > 0) {
    502          let sizeLeft = download.totalBytes - download.currentBytes;
    503          summary.rawTimeLeft = Math.max(
    504            summary.rawTimeLeft,
    505            sizeLeft / download.speed
    506          );
    507          summary.slowestSpeed = Math.min(summary.slowestSpeed, download.speed);
    508        }
    509      } else if (download.canceled && download.hasPartialData) {
    510        summary.numPaused++;
    511      }
    512 
    513      // Only add to total values if we actually know the download size.
    514      if (download.succeeded) {
    515        summary.totalSize += download.target.size;
    516        summary.totalTransferred += download.target.size;
    517      } else if (download.hasProgress) {
    518        summary.totalSize += download.totalBytes;
    519        summary.totalTransferred += download.currentBytes;
    520      }
    521    }
    522 
    523    if (summary.totalSize != 0) {
    524      summary.percentComplete = Math.floor(
    525        (summary.totalTransferred / summary.totalSize) * 100
    526      );
    527    }
    528 
    529    if (summary.slowestSpeed == Infinity) {
    530      summary.slowestSpeed = 0;
    531    }
    532 
    533    return summary;
    534  },
    535 
    536  /**
    537   * If necessary, smooths the estimated number of seconds remaining for one
    538   * or more downloads to complete.
    539   *
    540   * @param aSeconds
    541   *        Current raw estimate on number of seconds left for one or more
    542   *        downloads. This is a floating point value to help get sub-second
    543   *        accuracy for current and future estimates.
    544   */
    545  smoothSeconds(aSeconds, aLastSeconds) {
    546    // We apply an algorithm similar to the DownloadUtils.getTimeLeft function,
    547    // though tailored to a single time estimation for all downloads.  We never
    548    // apply something if the new value is less than half the previous value.
    549    let shouldApplySmoothing = aLastSeconds >= 0 && aSeconds > aLastSeconds / 2;
    550    if (shouldApplySmoothing) {
    551      // Apply hysteresis to favor downward over upward swings.  Trust only 30%
    552      // of the new value if lower, and 10% if higher (exponential smoothing).
    553      let diff = aSeconds - aLastSeconds;
    554      aSeconds = aLastSeconds + (diff < 0 ? 0.3 : 0.1) * diff;
    555 
    556      // If the new time is similar, reuse something close to the last time
    557      // left, but subtract a little to provide forward progress.
    558      diff = aSeconds - aLastSeconds;
    559      let diffPercent = (diff / aLastSeconds) * 100;
    560      if (Math.abs(diff) < 5 || Math.abs(diffPercent) < 5) {
    561        aSeconds = aLastSeconds - (diff < 0 ? 0.4 : 0.2);
    562      }
    563    }
    564 
    565    // In the last few seconds of downloading, we are always subtracting and
    566    // never adding to the time left.  Ensure that we never fall below one
    567    // second left until all downloads are actually finished.
    568    return (aLastSeconds = Math.max(aSeconds, 1));
    569  },
    570 
    571  /**
    572   * Opens a downloaded file.
    573   *
    574   * @param downloadProperties
    575   *        A Download object or the initial properties of a serialized download
    576   * @param options.openWhere
    577   *        Optional string indicating how to handle opening a download target file URI.
    578   *        One of "window", "tab", "tabshifted".
    579   * @param options.useSystemDefault
    580   *        Optional value indicating how to handle launching this download,
    581   *        this call only. Will override the associated mimeInfo.preferredAction
    582   * @returns {Promise<void>}
    583   *   Resolves when the instruction to launch the file has been successfully
    584   *   given to the operating system or handled internally.
    585   * @rejects
    586   *   With a JavaScript exception if there was an error trying to launch the file.
    587   */
    588  async openDownload(download, options) {
    589    // some download objects got serialized and need reconstituting
    590    if (typeof download.launch !== "function") {
    591      download = await lazy.Downloads.createDownload(download);
    592    }
    593    return download.launch(options).catch(ex => console.error(ex));
    594  },
    595 
    596  /**
    597   * Show a downloaded file in the system file manager.
    598   *
    599   * @param aFile
    600   *        a downloaded file.
    601   */
    602  showDownloadedFile(aFile) {
    603    if (!(aFile instanceof Ci.nsIFile)) {
    604      throw new Error("aFile must be a nsIFile object");
    605    }
    606    try {
    607      // Show the directory containing the file and select the file.
    608      aFile.reveal();
    609    } catch (ex) {
    610      // If reveal fails for some reason (e.g., it's not implemented on unix
    611      // or the file doesn't exist), try using the parent if we have it.
    612      let parent = aFile.parent;
    613      if (parent) {
    614        this.showDirectory(parent);
    615      }
    616    }
    617  },
    618 
    619  /**
    620   * Show the specified folder in the system file manager.
    621   *
    622   * @param aDirectory
    623   *        a directory to be opened with system file manager.
    624   */
    625  showDirectory(aDirectory) {
    626    if (!(aDirectory instanceof Ci.nsIFile)) {
    627      throw new Error("aDirectory must be a nsIFile object");
    628    }
    629    try {
    630      aDirectory.launch();
    631    } catch (ex) {
    632      // If launch fails (probably because it's not implemented), let
    633      // the OS handler try to open the directory.
    634      Cc["@mozilla.org/uriloader/external-protocol-service;1"]
    635        .getService(Ci.nsIExternalProtocolService)
    636        .loadURI(
    637          lazy.NetUtil.newURI(aDirectory),
    638          Services.scriptSecurityManager.getSystemPrincipal()
    639        );
    640    }
    641  },
    642 
    643  /**
    644   * Displays an alert message box which asks the user if they want to
    645   * unblock the downloaded file or not.
    646   *
    647   * @param options
    648   *        An object with the following properties:
    649   *        {
    650   *          verdict:
    651   *            The detailed reason why the download was blocked, according to
    652   *            the "Downloads.Error.BLOCK_VERDICT_" constants. If an unknown
    653   *            reason is specified, "Downloads.Error.BLOCK_VERDICT_MALWARE" is
    654   *            assumed.
    655   *          becauseBlockedByReputationCheck:
    656   *            Whether the the download was blocked by a reputation check.
    657   *          window:
    658   *            The window with which this action is associated.
    659   *          dialogType:
    660   *            String that determines which actions are available:
    661   *             - "unblock" to offer just "unblock".
    662   *             - "chooseUnblock" to offer "unblock" and "confirmBlock".
    663   *             - "chooseOpen" to offer "open" and "confirmBlock".
    664   *        }
    665   *
    666   * @returns {Promise<string>}
    667   *   Resolves to a string representing the action that should be executed:
    668   *   - "open" to allow the download and open the file.
    669   *   - "unblock" to allow the download without opening the file.
    670   *   - "confirmBlock" to delete the blocked data permanently.
    671   *   - "cancel" to do nothing and cancel the operation.
    672   */
    673  async confirmUnblockDownload({
    674    verdict,
    675    becauseBlockedByReputationCheck,
    676    window,
    677    dialogType,
    678  }) {
    679    let s = DownloadsCommon.strings;
    680 
    681    // All the dialogs have an action button and a cancel button, while only
    682    // some of them have an additonal button to remove the file. The cancel
    683    // button must always be the one at BUTTON_POS_1 because this is the value
    684    // returned by confirmEx when using ESC or closing the dialog (bug 345067).
    685    let title = s.unblockHeaderUnblock;
    686    let firstButtonText = s.unblockButtonUnblock;
    687    let firstButtonAction = "unblock";
    688    let buttonFlags =
    689      Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 +
    690      Ci.nsIPrompt.BUTTON_TITLE_CANCEL * Ci.nsIPrompt.BUTTON_POS_1;
    691 
    692    switch (dialogType) {
    693      case "unblock":
    694        // Use only the unblock action. The default is to cancel.
    695        buttonFlags += Ci.nsIPrompt.BUTTON_POS_1_DEFAULT;
    696        break;
    697      case "chooseUnblock":
    698        // Use the unblock and remove file actions. The default is remove file.
    699        buttonFlags +=
    700          Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2 +
    701          Ci.nsIPrompt.BUTTON_POS_2_DEFAULT;
    702        break;
    703      case "chooseOpen":
    704        // Use the unblock and open file actions. The default is open file.
    705        title = s.unblockHeaderOpen;
    706        firstButtonText = s.unblockButtonOpen;
    707        firstButtonAction = "open";
    708        buttonFlags +=
    709          Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2 +
    710          Ci.nsIPrompt.BUTTON_POS_0_DEFAULT;
    711        break;
    712      default:
    713        console.error("Unexpected dialog type: " + dialogType);
    714        return "cancel";
    715    }
    716 
    717    let message;
    718    let tip = s.unblockTip2;
    719    switch (verdict) {
    720      case lazy.Downloads.Error.BLOCK_VERDICT_UNCOMMON:
    721        message = s.unblockTypeUncommon2;
    722        break;
    723      case lazy.Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
    724        if (becauseBlockedByReputationCheck) {
    725          message = s.unblockTypePotentiallyUnwanted2;
    726        } else {
    727          message = s.unblockTypeContentAnalysisWarn;
    728          tip = s.unblockContentAnalysisTip;
    729        }
    730        break;
    731      case lazy.Downloads.Error.BLOCK_VERDICT_INSECURE:
    732        message = s.unblockInsecure2;
    733        break;
    734      default:
    735        // Assume Downloads.Error.BLOCK_VERDICT_MALWARE
    736        message = s.unblockTypeMalware;
    737        break;
    738    }
    739    message += "\n\n" + tip;
    740 
    741    Services.ww.registerNotification(function onOpen(subj, topic) {
    742      if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) {
    743        // Make sure to listen for "DOMContentLoaded" because it is fired
    744        // before the "load" event.
    745        subj.addEventListener(
    746          "DOMContentLoaded",
    747          function () {
    748            if (
    749              subj.document.documentURI ==
    750              "chrome://global/content/commonDialog.xhtml"
    751            ) {
    752              Services.ww.unregisterNotification(onOpen);
    753              let dialog = subj.document.getElementById("commonDialog");
    754              if (dialog) {
    755                // Change the dialog to use a warning icon.
    756                dialog.classList.add("alert-dialog");
    757              }
    758            }
    759          },
    760          { once: true }
    761        );
    762      }
    763    });
    764 
    765    let rv = Services.prompt.confirmEx(
    766      window,
    767      title,
    768      message,
    769      buttonFlags,
    770      firstButtonText,
    771      null,
    772      s.unblockButtonConfirmBlock,
    773      null,
    774      {}
    775    );
    776    return [firstButtonAction, "cancel", "confirmBlock"][rv];
    777  },
    778 };
    779 
    780 ChromeUtils.defineLazyGetter(DownloadsCommon, "log", () => {
    781  return lazy.DownloadsLogger.log.bind(lazy.DownloadsLogger);
    782 });
    783 ChromeUtils.defineLazyGetter(DownloadsCommon, "error", () => {
    784  return lazy.DownloadsLogger.error.bind(lazy.DownloadsLogger);
    785 });
    786 
    787 // DownloadsData
    788 
    789 /**
    790 * Retrieves the list of past and completed downloads from the underlying
    791 * Downloads API data, and provides asynchronous notifications allowing to
    792 * build a consistent view of the available data.
    793 *
    794 * Note that using this object does not automatically initialize the list of
    795 * downloads. This is useful to display a neutral progress indicator in
    796 * the main browser window until the autostart timeout elapses.
    797 *
    798 * This powers the DownloadsData, PrivateDownloadsData, and HistoryDownloadsData
    799 * singleton objects.
    800 */
    801 function DownloadsDataCtor({ isPrivate, isHistory, maxHistoryResults } = {}) {
    802  this._isPrivate = !!isPrivate;
    803 
    804  // Contains all the available Download objects and their integer state.
    805  this._oldDownloadStates = new WeakMap();
    806 
    807  // For the history downloads list we don't need to register this as a view,
    808  // but we have to ensure that the DownloadsData object is initialized before
    809  // we register more views. This ensures that the view methods of DownloadsData
    810  // are invoked before those of views registered on HistoryDownloadsData,
    811  // allowing the endTime property to be set correctly.
    812  if (isHistory) {
    813    if (isPrivate) {
    814      lazy.PrivateDownloadsData.initializeDataLink();
    815    }
    816    lazy.DownloadsData.initializeDataLink();
    817    this._promiseList = lazy.DownloadsData._promiseList.then(() => {
    818      // For history downloads in Private Browsing mode, we'll fetch the combined
    819      // list of public and private downloads.
    820      return lazy.DownloadHistory.getList({
    821        type: isPrivate ? lazy.Downloads.ALL : lazy.Downloads.PUBLIC,
    822        maxHistoryResults,
    823      });
    824    });
    825    return;
    826  }
    827 
    828  // This defines "initializeDataLink" and "_promiseList" synchronously, then
    829  // continues execution only when "initializeDataLink" is called, allowing the
    830  // underlying data to be loaded only when actually needed.
    831  this._promiseList = (async () => {
    832    await new Promise(resolve => (this.initializeDataLink = resolve));
    833    let list = await lazy.Downloads.getList(
    834      isPrivate ? lazy.Downloads.PRIVATE : lazy.Downloads.PUBLIC
    835    );
    836    list.addView(this);
    837    return list;
    838  })();
    839 }
    840 
    841 DownloadsDataCtor.prototype = {
    842  /**
    843   * Starts receiving events for current downloads.
    844   */
    845  initializeDataLink() {},
    846 
    847  /**
    848   * Promise resolved with the underlying DownloadList object once we started
    849   * receiving events for current downloads.
    850   */
    851  _promiseList: null,
    852 
    853  /**
    854   * Iterator for all the available Download objects. This is empty until the
    855   * data has been loaded using the JavaScript API for downloads.
    856   */
    857  get _downloads() {
    858    return ChromeUtils.nondeterministicGetWeakMapKeys(this._oldDownloadStates);
    859  },
    860 
    861  /**
    862   * True if there are finished downloads that can be removed from the list.
    863   */
    864  get canRemoveFinished() {
    865    for (let download of this._downloads) {
    866      // Stopped, paused, and failed downloads with partial data are removed.
    867      if (download.stopped && !(download.canceled && download.hasPartialData)) {
    868        return true;
    869      }
    870    }
    871    return false;
    872  },
    873 
    874  /**
    875   * Asks the back-end to remove finished downloads from the list. This method
    876   * is only called after the data link has been initialized.
    877   */
    878  removeFinished() {
    879    lazy.Downloads.getList(
    880      this._isPrivate ? lazy.Downloads.PRIVATE : lazy.Downloads.PUBLIC
    881    )
    882      .then(list => list.removeFinished())
    883      .catch(console.error);
    884  },
    885 
    886  // Integration with the asynchronous Downloads back-end
    887 
    888  onDownloadAdded(download) {
    889    // Download objects do not store the end time of downloads, as the Downloads
    890    // API does not need to persist this information for all platforms. Once a
    891    // download terminates on a Desktop browser, it becomes a history download,
    892    // for which the end time is stored differently, as a Places annotation.
    893    download.endTime = Date.now();
    894 
    895    this._oldDownloadStates.set(
    896      download,
    897      DownloadsCommon.stateOfDownload(download)
    898    );
    899    if (
    900      download.error?.becauseBlockedByReputationCheck ||
    901      download.error?.becauseBlockedByContentAnalysis
    902    ) {
    903      this._notifyDownloadEvent("error");
    904    }
    905  },
    906 
    907  onDownloadChanged(download) {
    908    let oldState = this._oldDownloadStates.get(download);
    909    let newState = DownloadsCommon.stateOfDownload(download);
    910    this._oldDownloadStates.set(download, newState);
    911 
    912    if (oldState != newState) {
    913      if (
    914        download.succeeded ||
    915        (download.canceled && !download.hasPartialData) ||
    916        download.error
    917      ) {
    918        // Store the end time that may be displayed by the views.
    919        download.endTime = Date.now();
    920 
    921        // This state transition code should actually be located in a Downloads
    922        // API module (bug 941009).
    923        lazy.DownloadHistory.updateMetaData(download).catch(console.error);
    924      }
    925 
    926      if (
    927        download.succeeded ||
    928        (download.error && download.error.becauseBlocked)
    929      ) {
    930        this._notifyDownloadEvent("finish");
    931      }
    932    }
    933 
    934    if (!download.newDownloadNotified) {
    935      download.newDownloadNotified = true;
    936      this._notifyDownloadEvent("start", {
    937        openDownloadsListOnStart: download.openDownloadsListOnStart,
    938      });
    939    }
    940  },
    941 
    942  onDownloadRemoved(download) {
    943    this._oldDownloadStates.delete(download);
    944  },
    945 
    946  // Registration of views
    947 
    948  /**
    949   * Adds an object to be notified when the available download data changes.
    950   * The specified object is initialized with the currently available downloads.
    951   *
    952   * @param aView
    953   *        DownloadsView object to be added.  This reference must be passed to
    954   *        removeView before termination.
    955   */
    956  addView(aView) {
    957    this._promiseList.then(list => list.addView(aView)).catch(console.error);
    958  },
    959 
    960  /**
    961   * Removes an object previously added using addView.
    962   *
    963   * @param aView
    964   *        DownloadsView object to be removed.
    965   */
    966  removeView(aView) {
    967    this._promiseList.then(list => list.removeView(aView)).catch(console.error);
    968  },
    969 
    970  // Notifications sent to the most recent browser window only
    971 
    972  /**
    973   * Set to true after the first download causes the downloads panel to be
    974   * displayed.
    975   */
    976  get panelHasShownBefore() {
    977    try {
    978      return Services.prefs.getBoolPref("browser.download.panel.shown");
    979    } catch (ex) {}
    980    return false;
    981  },
    982 
    983  set panelHasShownBefore(aValue) {
    984    Services.prefs.setBoolPref("browser.download.panel.shown", aValue);
    985  },
    986 
    987  /**
    988   * Displays a new or finished download notification in the most recent browser
    989   * window, if one is currently available with the required privacy type.
    990   *
    991   * @param {string} aType
    992   *        Set to "start" for new downloads, "finish" for completed downloads,
    993   *        "error" for downloads that failed and need attention
    994   * @param {boolean} [openDownloadsListOnStart]
    995   *        (Only relevant when aType = "start")
    996   *        true (default) - open the downloads panel.
    997   *        false - only show an indicator notification.
    998   */
    999  _notifyDownloadEvent(aType, { openDownloadsListOnStart = true } = {}) {
   1000    DownloadsCommon.log(
   1001      "Attempting to notify that a new download has started or finished."
   1002    );
   1003 
   1004    // Show the panel in the most recent browser window, if present.
   1005    let browserWin = lazy.BrowserWindowTracker.getTopWindow({
   1006      private: this._isPrivate,
   1007      allowFromInactiveWorkspace: true,
   1008    });
   1009    if (!browserWin) {
   1010      return;
   1011    }
   1012 
   1013    let shouldOpenDownloadsPanel =
   1014      aType == "start" &&
   1015      DownloadsCommon.summarizeDownloads(this._downloads).numDownloading <= 1 &&
   1016      lazy.gAlwaysOpenPanel;
   1017 
   1018    // For new downloads after the first one, don't show the panel
   1019    // automatically, but provide a visible notification in the topmost browser
   1020    // window, if the status indicator is already visible. Also ensure that if
   1021    // openDownloadsListOnStart = false is passed, we always skip opening the
   1022    // panel. That's because this will only be passed if the download is started
   1023    // without user interaction or if a dialog was previously opened in the
   1024    // process of the download (e.g. unknown content type dialog).
   1025    if (
   1026      aType != "error" &&
   1027      ((this.panelHasShownBefore && !shouldOpenDownloadsPanel) ||
   1028        !openDownloadsListOnStart ||
   1029        browserWin != Services.focus.activeWindow)
   1030    ) {
   1031      DownloadsCommon.log("Showing new download notification.");
   1032      browserWin.DownloadsIndicatorView.showEventNotification(aType);
   1033      return;
   1034    }
   1035    this.panelHasShownBefore = true;
   1036    browserWin.DownloadsPanel.showPanel();
   1037  },
   1038 };
   1039 
   1040 ChromeUtils.defineLazyGetter(lazy, "HistoryDownloadsData", function () {
   1041  return new DownloadsDataCtor({ isHistory: true });
   1042 });
   1043 
   1044 ChromeUtils.defineLazyGetter(lazy, "LimitedHistoryDownloadsData", function () {
   1045  return new DownloadsDataCtor({
   1046    isHistory: true,
   1047    maxHistoryResults: kMaxHistoryResultsForLimitedView,
   1048  });
   1049 });
   1050 
   1051 ChromeUtils.defineLazyGetter(
   1052  lazy,
   1053  "LimitedPrivateHistoryDownloadData",
   1054  function () {
   1055    return new DownloadsDataCtor({
   1056      isPrivate: true,
   1057      isHistory: true,
   1058      maxHistoryResults: kMaxHistoryResultsForLimitedView,
   1059    });
   1060  }
   1061 );
   1062 
   1063 ChromeUtils.defineLazyGetter(lazy, "PrivateDownloadsData", function () {
   1064  return new DownloadsDataCtor({ isPrivate: true });
   1065 });
   1066 
   1067 ChromeUtils.defineLazyGetter(lazy, "DownloadsData", function () {
   1068  return new DownloadsDataCtor();
   1069 });
   1070 
   1071 // DownloadsViewPrototype
   1072 
   1073 /**
   1074 * A prototype for an object that registers itself with DownloadsData as soon
   1075 * as a view is registered with it.
   1076 */
   1077 const DownloadsViewPrototype = {
   1078  /**
   1079   * Contains all the available Download objects and their current state value.
   1080   *
   1081   * SUBCLASSES MUST OVERRIDE THIS PROPERTY.
   1082   */
   1083  _oldDownloadStates: null,
   1084 
   1085  // Registration of views
   1086 
   1087  /**
   1088   * Array of view objects that should be notified when the available status
   1089   * data changes.
   1090   *
   1091   * SUBCLASSES MUST OVERRIDE THIS PROPERTY.
   1092   */
   1093  _views: null,
   1094 
   1095  /**
   1096   * Determines whether this view object is over the private or non-private
   1097   * downloads.
   1098   *
   1099   * SUBCLASSES MUST OVERRIDE THIS PROPERTY.
   1100   */
   1101  _isPrivate: false,
   1102 
   1103  /**
   1104   * Adds an object to be notified when the available status data changes.
   1105   * The specified object is initialized with the currently available status.
   1106   *
   1107   * @param aView
   1108   *        View object to be added.  This reference must be
   1109   *        passed to removeView before termination.
   1110   */
   1111  addView(aView) {
   1112    // Start receiving events when the first of our views is registered.
   1113    if (!this._views.length) {
   1114      if (this._isPrivate) {
   1115        lazy.PrivateDownloadsData.addView(this);
   1116      } else {
   1117        lazy.DownloadsData.addView(this);
   1118      }
   1119    }
   1120 
   1121    this._views.push(aView);
   1122    this.refreshView(aView);
   1123  },
   1124 
   1125  /**
   1126   * Updates the properties of an object previously added using addView.
   1127   *
   1128   * @param aView
   1129   *        View object to be updated.
   1130   */
   1131  refreshView(aView) {
   1132    // Update immediately even if we are still loading data asynchronously.
   1133    // Subclasses must provide these two functions!
   1134    this._refreshProperties();
   1135    this._updateView(aView);
   1136  },
   1137 
   1138  /**
   1139   * Removes an object previously added using addView.
   1140   *
   1141   * @param aView
   1142   *        View object to be removed.
   1143   */
   1144  removeView(aView) {
   1145    let index = this._views.indexOf(aView);
   1146    if (index != -1) {
   1147      this._views.splice(index, 1);
   1148    }
   1149 
   1150    // Stop receiving events when the last of our views is unregistered.
   1151    if (!this._views.length) {
   1152      if (this._isPrivate) {
   1153        lazy.PrivateDownloadsData.removeView(this);
   1154      } else {
   1155        lazy.DownloadsData.removeView(this);
   1156      }
   1157    }
   1158  },
   1159 
   1160  // Callback functions from DownloadList
   1161 
   1162  /**
   1163   * Indicates whether we are still loading downloads data asynchronously.
   1164   */
   1165  _loading: false,
   1166 
   1167  /**
   1168   * Called before multiple downloads are about to be loaded.
   1169   */
   1170  onDownloadBatchStarting() {
   1171    this._loading = true;
   1172  },
   1173 
   1174  /**
   1175   * Called after data loading finished.
   1176   */
   1177  onDownloadBatchEnded() {
   1178    this._loading = false;
   1179    this._updateViews();
   1180  },
   1181 
   1182  /**
   1183   * Called when a new download data item is available, either during the
   1184   * asynchronous data load or when a new download is started.
   1185   *
   1186   * @param download
   1187   *        Download object that was just added.
   1188   *
   1189   * Note: Subclasses should override this and still call the base method.
   1190   */
   1191  onDownloadAdded(download) {
   1192    this._oldDownloadStates.set(
   1193      download,
   1194      DownloadsCommon.stateOfDownload(download)
   1195    );
   1196  },
   1197 
   1198  /**
   1199   * Called when the overall state of a Download has changed. In particular,
   1200   * this is called only once when the download succeeds or is blocked
   1201   * permanently, and is never called if only the current progress changed.
   1202   *
   1203   * The onDownloadChanged notification will always be sent afterwards.
   1204   *
   1205   * Note: Subclasses should override this.
   1206   */
   1207  onDownloadStateChanged() {
   1208    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
   1209  },
   1210 
   1211  /**
   1212   * Called every time any state property of a Download may have changed,
   1213   * including progress properties.
   1214   *
   1215   * Note that progress notification changes are throttled at the Downloads.sys.mjs
   1216   * API level, and there is no throttling mechanism in the front-end.
   1217   *
   1218   * Note: Subclasses should override this and still call the base method.
   1219   */
   1220  onDownloadChanged(download) {
   1221    let oldState = this._oldDownloadStates.get(download);
   1222    let newState = DownloadsCommon.stateOfDownload(download);
   1223    this._oldDownloadStates.set(download, newState);
   1224 
   1225    if (oldState != newState) {
   1226      this.onDownloadStateChanged(download);
   1227    }
   1228  },
   1229 
   1230  /**
   1231   * Called when a data item is removed, ensures that the widget associated with
   1232   * the view item is removed from the user interface.
   1233   *
   1234   * @param download
   1235   *        Download object that is being removed.
   1236   *
   1237   * Note: Subclasses should override this.
   1238   */
   1239  onDownloadRemoved(download) {
   1240    this._oldDownloadStates.delete(download);
   1241  },
   1242 
   1243  /**
   1244   * Private function used to refresh the internal properties being sent to
   1245   * each registered view.
   1246   *
   1247   * Note: Subclasses should override this.
   1248   */
   1249  _refreshProperties() {
   1250    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
   1251  },
   1252 
   1253  /**
   1254   * Private function used to refresh an individual view.
   1255   *
   1256   * Note: Subclasses should override this.
   1257   */
   1258  _updateView() {
   1259    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
   1260  },
   1261 
   1262  /**
   1263   * Computes aggregate values and propagates the changes to our views.
   1264   */
   1265  _updateViews() {
   1266    // Do not update the status indicators during batch loads of download items.
   1267    if (this._loading) {
   1268      return;
   1269    }
   1270 
   1271    this._refreshProperties();
   1272    this._views.forEach(this._updateView, this);
   1273  },
   1274 };
   1275 
   1276 // DownloadsIndicatorData
   1277 
   1278 /**
   1279 * This object registers itself with DownloadsData as a view, and transforms the
   1280 * notifications it receives into overall status data, that is then broadcast to
   1281 * the registered download status indicators.
   1282 *
   1283 * Note that using this object does not automatically start the Download Manager
   1284 * service.  Consumers will see an empty list of downloads until the service is
   1285 * actually started.  This is useful to display a neutral progress indicator in
   1286 * the main browser window until the autostart timeout elapses.
   1287 */
   1288 function DownloadsIndicatorDataCtor(aPrivate) {
   1289  this._oldDownloadStates = new WeakMap();
   1290  this._isPrivate = aPrivate;
   1291  this._views = [];
   1292 }
   1293 DownloadsIndicatorDataCtor.prototype = {
   1294  /**
   1295   * Map of the relative severities of different attention states.
   1296   * Used in sorting the map of active downloads' attention states
   1297   * to determine the attention state to be displayed.
   1298   */
   1299  _attentionPriority: new Map([
   1300    [DownloadsCommon.ATTENTION_NONE, 0],
   1301    [DownloadsCommon.ATTENTION_SUCCESS, 1],
   1302    [DownloadsCommon.ATTENTION_INFO, 2],
   1303    [DownloadsCommon.ATTENTION_WARNING, 3],
   1304    [DownloadsCommon.ATTENTION_SEVERE, 4],
   1305  ]),
   1306 
   1307  /**
   1308   * Iterator for all the available Download objects. This is empty until the
   1309   * data has been loaded using the JavaScript API for downloads.
   1310   */
   1311  get _downloads() {
   1312    return ChromeUtils.nondeterministicGetWeakMapKeys(this._oldDownloadStates);
   1313  },
   1314 
   1315  /**
   1316   * Removes an object previously added using addView.
   1317   *
   1318   * @param aView
   1319   *        DownloadsIndicatorView object to be removed.
   1320   */
   1321  removeView(aView) {
   1322    DownloadsViewPrototype.removeView.call(this, aView);
   1323 
   1324    if (!this._views.length) {
   1325      this._itemCount = 0;
   1326    }
   1327  },
   1328 
   1329  onDownloadAdded(download) {
   1330    DownloadsViewPrototype.onDownloadAdded.call(this, download);
   1331    this._itemCount++;
   1332    this._updateViews();
   1333  },
   1334 
   1335  onDownloadStateChanged(download) {
   1336    if (this._attentionSuppressed !== DownloadsCommon.SUPPRESS_NONE) {
   1337      return;
   1338    }
   1339    let attention;
   1340    if (
   1341      !download.succeeded &&
   1342      download.error &&
   1343      download.error.reputationCheckVerdict
   1344    ) {
   1345      switch (download.error.reputationCheckVerdict) {
   1346        case lazy.Downloads.Error.BLOCK_VERDICT_UNCOMMON:
   1347          attention = DownloadsCommon.ATTENTION_INFO;
   1348          break;
   1349        case lazy.Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED: // fall-through
   1350        case lazy.Downloads.Error.BLOCK_VERDICT_INSECURE:
   1351        case lazy.Downloads.Error.BLOCK_VERDICT_DOWNLOAD_SPAM:
   1352          attention = DownloadsCommon.ATTENTION_WARNING;
   1353          break;
   1354        case lazy.Downloads.Error.BLOCK_VERDICT_MALWARE:
   1355          attention = DownloadsCommon.ATTENTION_SEVERE;
   1356          break;
   1357        default:
   1358          attention = DownloadsCommon.ATTENTION_SEVERE;
   1359          console.error(
   1360            "Unknown reputation verdict: " +
   1361              download.error.reputationCheckVerdict
   1362          );
   1363      }
   1364    } else if (download.succeeded) {
   1365      attention = DownloadsCommon.ATTENTION_SUCCESS;
   1366    } else if (download.error) {
   1367      attention = DownloadsCommon.ATTENTION_WARNING;
   1368    }
   1369    download.attention = attention;
   1370    this.updateAttention();
   1371  },
   1372 
   1373  onDownloadChanged(download) {
   1374    DownloadsViewPrototype.onDownloadChanged.call(this, download);
   1375    this._updateViews();
   1376  },
   1377 
   1378  onDownloadRemoved(download) {
   1379    DownloadsViewPrototype.onDownloadRemoved.call(this, download);
   1380    this._itemCount--;
   1381    this.updateAttention();
   1382    this._updateViews();
   1383  },
   1384 
   1385  // Propagation of properties to our views
   1386 
   1387  // The following properties are updated by _refreshProperties and are then
   1388  // propagated to the views.  See _refreshProperties for details.
   1389  _hasDownloads: false,
   1390  _percentComplete: -1,
   1391 
   1392  /**
   1393   * Indicates whether the download indicators should be highlighted.
   1394   */
   1395  set attention(aValue) {
   1396    this._attention = aValue;
   1397    this._updateViews();
   1398  },
   1399  _attention: DownloadsCommon.ATTENTION_NONE,
   1400 
   1401  /**
   1402   * Indicates whether the user is interacting with downloads, thus the
   1403   * attention indication should not be shown even if requested.
   1404   */
   1405  set attentionSuppressed(aFlags) {
   1406    this._attentionSuppressed = aFlags;
   1407    if (aFlags !== DownloadsCommon.SUPPRESS_NONE) {
   1408      for (let download of this._downloads) {
   1409        download.attention = DownloadsCommon.ATTENTION_NONE;
   1410      }
   1411      this.attention = DownloadsCommon.ATTENTION_NONE;
   1412    }
   1413  },
   1414  get attentionSuppressed() {
   1415    return this._attentionSuppressed;
   1416  },
   1417  _attentionSuppressed: DownloadsCommon.SUPPRESS_NONE,
   1418 
   1419  /**
   1420   * Set the indicator's attention to the most severe attention state among the
   1421   * unseen displayed downloads, or DownloadsCommon.ATTENTION_NONE if empty.
   1422   */
   1423  updateAttention() {
   1424    let currentAttention = DownloadsCommon.ATTENTION_NONE;
   1425    let currentPriority = 0;
   1426    for (let download of this._downloads) {
   1427      let { attention } = download;
   1428      let priority = this._attentionPriority.get(attention);
   1429      if (priority > currentPriority) {
   1430        currentPriority = priority;
   1431        currentAttention = attention;
   1432      }
   1433    }
   1434    this.attention = currentAttention;
   1435  },
   1436 
   1437  /**
   1438   * Updates the specified view with the current aggregate values.
   1439   *
   1440   * @param aView
   1441   *        DownloadsIndicatorView object to be updated.
   1442   */
   1443  _updateView(aView) {
   1444    aView.hasDownloads = this._hasDownloads;
   1445    aView.percentComplete = this._percentComplete;
   1446    aView.attention =
   1447      this.attentionSuppressed !== DownloadsCommon.SUPPRESS_NONE
   1448        ? DownloadsCommon.ATTENTION_NONE
   1449        : this._attention;
   1450  },
   1451 
   1452  // Property updating based on current download status
   1453 
   1454  /**
   1455   * Number of download items that are available to be displayed.
   1456   */
   1457  _itemCount: 0,
   1458 
   1459  /**
   1460   * A generator function for the Download objects this summary is currently
   1461   * interested in. This generator is passed off to summarizeDownloads in order
   1462   * to generate statistics about the downloads we care about - in this case,
   1463   * it's all active downloads.
   1464   */
   1465  *_activeDownloads() {
   1466    let downloads = this._isPrivate
   1467      ? lazy.PrivateDownloadsData._downloads
   1468      : lazy.DownloadsData._downloads;
   1469    for (let download of downloads) {
   1470      if (
   1471        download.isInCurrentBatch ||
   1472        (download.canceled && download.hasPartialData)
   1473      ) {
   1474        yield download;
   1475      }
   1476    }
   1477  },
   1478 
   1479  /**
   1480   * Computes aggregate values based on the current state of downloads.
   1481   */
   1482  _refreshProperties() {
   1483    let summary = DownloadsCommon.summarizeDownloads(this._activeDownloads());
   1484 
   1485    // Determine if the indicator should be shown or get attention.
   1486    this._hasDownloads = this._itemCount > 0;
   1487 
   1488    // Always show a progress bar if there are downloads in progress.
   1489    if (summary.percentComplete >= 0) {
   1490      this._percentComplete = summary.percentComplete;
   1491    } else if (summary.numDownloading > 0) {
   1492      this._percentComplete = 0;
   1493    } else {
   1494      this._percentComplete = -1;
   1495    }
   1496  },
   1497 };
   1498 Object.setPrototypeOf(
   1499  DownloadsIndicatorDataCtor.prototype,
   1500  DownloadsViewPrototype
   1501 );
   1502 
   1503 ChromeUtils.defineLazyGetter(
   1504  lazy,
   1505  "PrivateDownloadsIndicatorData",
   1506  function () {
   1507    return new DownloadsIndicatorDataCtor(true);
   1508  }
   1509 );
   1510 
   1511 ChromeUtils.defineLazyGetter(lazy, "DownloadsIndicatorData", function () {
   1512  return new DownloadsIndicatorDataCtor(false);
   1513 });
   1514 
   1515 // DownloadsSummaryData
   1516 
   1517 /**
   1518 * DownloadsSummaryData is a view for DownloadsData that produces a summary
   1519 * of all downloads after a certain exclusion point aNumToExclude. For example,
   1520 * if there were 5 downloads in progress, and a DownloadsSummaryData was
   1521 * constructed with aNumToExclude equal to 3, then that DownloadsSummaryData
   1522 * would produce a summary of the last 2 downloads.
   1523 *
   1524 * @param aIsPrivate
   1525 *        True if the browser window which owns the download button is a private
   1526 *        window.
   1527 * @param aNumToExclude
   1528 *        The number of items to exclude from the summary, starting from the
   1529 *        top of the list.
   1530 */
   1531 function DownloadsSummaryData(aIsPrivate, aNumToExclude) {
   1532  this._numToExclude = aNumToExclude;
   1533  // Since we can have multiple instances of DownloadsSummaryData, we
   1534  // override these values from the prototype so that each instance can be
   1535  // completely separated from one another.
   1536  this._loading = false;
   1537 
   1538  this._downloads = [];
   1539 
   1540  // Floating point value indicating the last number of seconds estimated until
   1541  // the longest download will finish.  We need to store this value so that we
   1542  // don't continuously apply smoothing if the actual download state has not
   1543  // changed.  This is set to -1 if the previous value is unknown.
   1544  this._lastRawTimeLeft = -1;
   1545 
   1546  // Last number of seconds estimated until all in-progress downloads with a
   1547  // known size and speed will finish.  This value is stored to allow smoothing
   1548  // in case of small variations.  This is set to -1 if the previous value is
   1549  // unknown.
   1550  this._lastTimeLeft = -1;
   1551 
   1552  // The following properties are updated by _refreshProperties and are then
   1553  // propagated to the views.
   1554  this._showingProgress = false;
   1555  this._details = "";
   1556  this._description = "";
   1557  this._numActive = 0;
   1558  this._percentComplete = -1;
   1559 
   1560  this._oldDownloadStates = new WeakMap();
   1561  this._isPrivate = aIsPrivate;
   1562  this._views = [];
   1563 }
   1564 
   1565 DownloadsSummaryData.prototype = {
   1566  /**
   1567   * Removes an object previously added using addView.
   1568   *
   1569   * @param aView
   1570   *        DownloadsSummary view to be removed.
   1571   */
   1572  removeView(aView) {
   1573    DownloadsViewPrototype.removeView.call(this, aView);
   1574 
   1575    if (!this._views.length) {
   1576      // Clear out our collection of Download objects. If we ever have
   1577      // another view registered with us, this will get re-populated.
   1578      this._downloads = [];
   1579    }
   1580  },
   1581 
   1582  onDownloadAdded(download) {
   1583    DownloadsViewPrototype.onDownloadAdded.call(this, download);
   1584    this._downloads.unshift(download);
   1585    this._updateViews();
   1586  },
   1587 
   1588  onDownloadStateChanged() {
   1589    // Since the state of a download changed, reset the estimated time left.
   1590    this._lastRawTimeLeft = -1;
   1591    this._lastTimeLeft = -1;
   1592  },
   1593 
   1594  onDownloadChanged(download) {
   1595    DownloadsViewPrototype.onDownloadChanged.call(this, download);
   1596    this._updateViews();
   1597  },
   1598 
   1599  onDownloadRemoved(download) {
   1600    DownloadsViewPrototype.onDownloadRemoved.call(this, download);
   1601    let itemIndex = this._downloads.indexOf(download);
   1602    this._downloads.splice(itemIndex, 1);
   1603    this._updateViews();
   1604  },
   1605 
   1606  // Propagation of properties to our views
   1607 
   1608  /**
   1609   * Updates the specified view with the current aggregate values.
   1610   *
   1611   * @param aView
   1612   *        DownloadsIndicatorView object to be updated.
   1613   */
   1614  _updateView(aView) {
   1615    aView.showingProgress = this._showingProgress;
   1616    aView.percentComplete = this._percentComplete;
   1617    aView.description = this._description;
   1618    aView.details = this._details;
   1619  },
   1620 
   1621  // Property updating based on current download status
   1622 
   1623  /**
   1624   * A generator function for the Download objects this summary is currently
   1625   * interested in. This generator is passed off to summarizeDownloads in order
   1626   * to generate statistics about the downloads we care about - in this case,
   1627   * it's the downloads in this._downloads after the first few to exclude,
   1628   * which was set when constructing this DownloadsSummaryData instance.
   1629   */
   1630  *_downloadsForSummary() {
   1631    if (this._downloads.length) {
   1632      for (let i = this._numToExclude; i < this._downloads.length; ++i) {
   1633        yield this._downloads[i];
   1634      }
   1635    }
   1636  },
   1637 
   1638  /**
   1639   * Computes aggregate values based on the current state of downloads.
   1640   */
   1641  _refreshProperties() {
   1642    // Pre-load summary with default values.
   1643    let summary = DownloadsCommon.summarizeDownloads(
   1644      this._downloadsForSummary()
   1645    );
   1646 
   1647    // Run sync to update view right away and get correct description.
   1648    // See refreshView for more details.
   1649    this._description = kDownloadsFluentStrings.formatValueSync(
   1650      "downloads-more-downloading",
   1651      {
   1652        count: summary.numDownloading,
   1653      }
   1654    );
   1655    this._percentComplete = summary.percentComplete;
   1656 
   1657    // Only show the downloading items.
   1658    this._showingProgress = summary.numDownloading > 0;
   1659 
   1660    // Display the estimated time left, if present.
   1661    if (summary.rawTimeLeft == -1) {
   1662      // There are no downloads with a known time left.
   1663      this._lastRawTimeLeft = -1;
   1664      this._lastTimeLeft = -1;
   1665      this._details = "";
   1666    } else {
   1667      // Compute the new time left only if state actually changed.
   1668      if (this._lastRawTimeLeft != summary.rawTimeLeft) {
   1669        this._lastRawTimeLeft = summary.rawTimeLeft;
   1670        this._lastTimeLeft = DownloadsCommon.smoothSeconds(
   1671          summary.rawTimeLeft,
   1672          this._lastTimeLeft
   1673        );
   1674      }
   1675      [this._details] = lazy.DownloadUtils.getDownloadStatusNoRate(
   1676        summary.totalTransferred,
   1677        summary.totalSize,
   1678        summary.slowestSpeed,
   1679        this._lastTimeLeft
   1680      );
   1681    }
   1682  },
   1683 };
   1684 Object.setPrototypeOf(DownloadsSummaryData.prototype, DownloadsViewPrototype);