forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			1643 lines
		
	
	
	
		
			52 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1643 lines
		
	
	
	
		
			52 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 | |
| /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
 | |
| /* This Source Code Form is subject to the terms of the Mozilla Public
 | |
|  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 | |
|  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 | |
| 
 | |
| /**
 | |
|  * Handles the Downloads panel shared methods and data access.
 | |
|  *
 | |
|  * This file includes the following constructors and global objects:
 | |
|  *
 | |
|  * DownloadsCommon
 | |
|  * This object is exposed directly to the consumers of this JavaScript module,
 | |
|  * and provides shared methods for all the instances of the user interface.
 | |
|  *
 | |
|  * DownloadsData
 | |
|  * Retrieves the list of past and completed downloads from the underlying
 | |
|  * Downloads API data, and provides asynchronous notifications allowing
 | |
|  * to build a consistent view of the available data.
 | |
|  *
 | |
|  * DownloadsIndicatorData
 | |
|  * This object registers itself with DownloadsData as a view, and transforms the
 | |
|  * notifications it receives into overall status data, that is then broadcast to
 | |
|  * the registered download status indicators.
 | |
|  */
 | |
| 
 | |
| // Globals
 | |
| 
 | |
| import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | |
| 
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(lazy, {
 | |
|   BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
 | |
|   DownloadHistory: "resource://gre/modules/DownloadHistory.sys.mjs",
 | |
|   DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
 | |
|   Downloads: "resource://gre/modules/Downloads.sys.mjs",
 | |
|   NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
 | |
|   PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
 | |
|   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
 | |
| });
 | |
| 
 | |
| XPCOMUtils.defineLazyServiceGetters(lazy, {
 | |
|   gClipboardHelper: [
 | |
|     "@mozilla.org/widget/clipboardhelper;1",
 | |
|     "nsIClipboardHelper",
 | |
|   ],
 | |
|   gMIMEService: ["@mozilla.org/mime;1", "nsIMIMEService"],
 | |
| });
 | |
| 
 | |
| ChromeUtils.defineLazyGetter(lazy, "DownloadsLogger", () => {
 | |
|   let { ConsoleAPI } = ChromeUtils.importESModule(
 | |
|     "resource://gre/modules/Console.sys.mjs"
 | |
|   );
 | |
|   let consoleOptions = {
 | |
|     maxLogLevelPref: "browser.download.loglevel",
 | |
|     prefix: "Downloads",
 | |
|   };
 | |
|   return new ConsoleAPI(consoleOptions);
 | |
| });
 | |
| 
 | |
| XPCOMUtils.defineLazyPreferenceGetter(
 | |
|   lazy,
 | |
|   "gAlwaysOpenPanel",
 | |
|   "browser.download.alwaysOpenPanel",
 | |
|   true
 | |
| );
 | |
| 
 | |
| const kDownloadsStringBundleUrl =
 | |
|   "chrome://browser/locale/downloads/downloads.properties";
 | |
| 
 | |
| const kDownloadsFluentStrings = new Localization(
 | |
|   ["browser/downloads.ftl"],
 | |
|   true
 | |
| );
 | |
| 
 | |
| const kDownloadsStringsRequiringFormatting = {
 | |
|   sizeWithUnits: true,
 | |
|   statusSeparator: true,
 | |
|   statusSeparatorBeforeNumber: true,
 | |
| };
 | |
| 
 | |
| const kMaxHistoryResultsForLimitedView = 42;
 | |
| 
 | |
| const kPrefBranch = Services.prefs.getBranch("browser.download.");
 | |
| 
 | |
| const kGenericContentTypes = [
 | |
|   "application/octet-stream",
 | |
|   "binary/octet-stream",
 | |
|   "application/unknown",
 | |
| ];
 | |
| 
 | |
| var PrefObserver = {
 | |
|   QueryInterface: ChromeUtils.generateQI([
 | |
|     "nsIObserver",
 | |
|     "nsISupportsWeakReference",
 | |
|   ]),
 | |
|   getPref(name) {
 | |
|     try {
 | |
|       switch (typeof this.prefs[name]) {
 | |
|         case "boolean":
 | |
|           return kPrefBranch.getBoolPref(name);
 | |
|       }
 | |
|     } catch (ex) {}
 | |
|     return this.prefs[name];
 | |
|   },
 | |
|   observe(aSubject, aTopic, aData) {
 | |
|     if (this.prefs.hasOwnProperty(aData)) {
 | |
|       delete this[aData];
 | |
|       this[aData] = this.getPref(aData);
 | |
|     }
 | |
|   },
 | |
|   register(prefs) {
 | |
|     this.prefs = prefs;
 | |
|     kPrefBranch.addObserver("", this, true);
 | |
|     for (let key in prefs) {
 | |
|       let name = key;
 | |
|       ChromeUtils.defineLazyGetter(this, name, function () {
 | |
|         return PrefObserver.getPref(name);
 | |
|       });
 | |
|     }
 | |
|   },
 | |
| };
 | |
| 
 | |
| PrefObserver.register({
 | |
|   // prefName: defaultValue
 | |
|   openInSystemViewerContextMenuItem: true,
 | |
|   alwaysOpenInSystemViewerContextMenuItem: true,
 | |
| });
 | |
| 
 | |
| // DownloadsCommon
 | |
| 
 | |
| /**
 | |
|  * This object is exposed directly to the consumers of this JavaScript module,
 | |
|  * and provides shared methods for all the instances of the user interface.
 | |
|  */
 | |
| export var DownloadsCommon = {
 | |
|   // The following legacy constants are still returned by stateOfDownload, but
 | |
|   // individual properties of the Download object should normally be used.
 | |
|   DOWNLOAD_NOTSTARTED: -1,
 | |
|   DOWNLOAD_DOWNLOADING: 0,
 | |
|   DOWNLOAD_FINISHED: 1,
 | |
|   DOWNLOAD_FAILED: 2,
 | |
|   DOWNLOAD_CANCELED: 3,
 | |
|   DOWNLOAD_PAUSED: 4,
 | |
|   DOWNLOAD_BLOCKED_PARENTAL: 6,
 | |
|   DOWNLOAD_DIRTY: 8,
 | |
|   DOWNLOAD_BLOCKED_POLICY: 9,
 | |
| 
 | |
|   // The following are the possible values of the "attention" property.
 | |
|   ATTENTION_NONE: "",
 | |
|   ATTENTION_SUCCESS: "success",
 | |
|   ATTENTION_INFO: "info",
 | |
|   ATTENTION_WARNING: "warning",
 | |
|   ATTENTION_SEVERE: "severe",
 | |
| 
 | |
|   // Bit flags for the attentionSuppressed property.
 | |
|   SUPPRESS_NONE: 0,
 | |
|   SUPPRESS_PANEL_OPEN: 1,
 | |
|   SUPPRESS_ALL_DOWNLOADS_OPEN: 2,
 | |
|   SUPPRESS_CONTENT_AREA_DOWNLOADS_OPEN: 4,
 | |
| 
 | |
|   /**
 | |
|    * Returns an object whose keys are the string names from the downloads string
 | |
|    * bundle, and whose values are either the translated strings or functions
 | |
|    * returning formatted strings.
 | |
|    */
 | |
|   get strings() {
 | |
|     let strings = {};
 | |
|     let sb = Services.strings.createBundle(kDownloadsStringBundleUrl);
 | |
|     for (let string of sb.getSimpleEnumeration()) {
 | |
|       let stringName = string.key;
 | |
|       if (stringName in kDownloadsStringsRequiringFormatting) {
 | |
|         strings[stringName] = function () {
 | |
|           // Convert "arguments" to a real array before calling into XPCOM.
 | |
|           return sb.formatStringFromName(stringName, Array.from(arguments));
 | |
|         };
 | |
|       } else {
 | |
|         strings[stringName] = string.value;
 | |
|       }
 | |
|     }
 | |
|     delete this.strings;
 | |
|     return (this.strings = strings);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Indicates whether or not to show the 'Open in system viewer' context menu item when appropriate
 | |
|    */
 | |
|   get openInSystemViewerItemEnabled() {
 | |
|     return PrefObserver.openInSystemViewerContextMenuItem;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Indicates whether or not to show the 'Always open...' context menu item when appropriate
 | |
|    */
 | |
|   get alwaysOpenInSystemViewerItemEnabled() {
 | |
|     return PrefObserver.alwaysOpenInSystemViewerContextMenuItem;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get access to one of the DownloadsData, PrivateDownloadsData, or
 | |
|    * HistoryDownloadsData objects, depending on the privacy status of the
 | |
|    * specified window and on whether history downloads should be included.
 | |
|    *
 | |
|    * @param [optional] window
 | |
|    *        The browser window which owns the download button.
 | |
|    *        If not given, the privacy status will be assumed as non-private.
 | |
|    * @param [optional] history
 | |
|    *        True to include history downloads when the window is public.
 | |
|    * @param [optional] privateAll
 | |
|    *        Whether to force the public downloads data to be returned together
 | |
|    *        with the private downloads data for a private window.
 | |
|    * @param [optional] limited
 | |
|    *        True to limit the amount of downloads returned to
 | |
|    *        `kMaxHistoryResultsForLimitedView`.
 | |
|    */
 | |
|   getData(window, history = false, privateAll = false, limited = false) {
 | |
|     let isPrivate =
 | |
|       window && lazy.PrivateBrowsingUtils.isContentWindowPrivate(window);
 | |
|     if (isPrivate && !privateAll) {
 | |
|       return lazy.PrivateDownloadsData;
 | |
|     }
 | |
|     if (history) {
 | |
|       if (isPrivate && privateAll) {
 | |
|         return lazy.LimitedPrivateHistoryDownloadData;
 | |
|       }
 | |
|       return limited
 | |
|         ? lazy.LimitedHistoryDownloadsData
 | |
|         : lazy.HistoryDownloadsData;
 | |
|     }
 | |
|     return lazy.DownloadsData;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Initializes the Downloads back-end and starts receiving events for both the
 | |
|    * private and non-private downloads data objects.
 | |
|    */
 | |
|   initializeAllDataLinks() {
 | |
|     lazy.DownloadsData.initializeDataLink();
 | |
|     lazy.PrivateDownloadsData.initializeDataLink();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get access to one of the DownloadsIndicatorData or
 | |
|    * PrivateDownloadsIndicatorData objects, depending on the privacy status of
 | |
|    * the window in question.
 | |
|    */
 | |
|   getIndicatorData(aWindow) {
 | |
|     if (lazy.PrivateBrowsingUtils.isContentWindowPrivate(aWindow)) {
 | |
|       return lazy.PrivateDownloadsIndicatorData;
 | |
|     }
 | |
|     return lazy.DownloadsIndicatorData;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Returns a reference to the DownloadsSummaryData singleton - creating one
 | |
|    * in the process if one hasn't been instantiated yet.
 | |
|    *
 | |
|    * @param aWindow
 | |
|    *        The browser window which owns the download button.
 | |
|    * @param aNumToExclude
 | |
|    *        The number of items on the top of the downloads list to exclude
 | |
|    *        from the summary.
 | |
|    */
 | |
|   getSummary(aWindow, aNumToExclude) {
 | |
|     if (lazy.PrivateBrowsingUtils.isContentWindowPrivate(aWindow)) {
 | |
|       if (this._privateSummary) {
 | |
|         return this._privateSummary;
 | |
|       }
 | |
|       return (this._privateSummary = new DownloadsSummaryData(
 | |
|         true,
 | |
|         aNumToExclude
 | |
|       ));
 | |
|     }
 | |
|     if (this._summary) {
 | |
|       return this._summary;
 | |
|     }
 | |
|     return (this._summary = new DownloadsSummaryData(false, aNumToExclude));
 | |
|   },
 | |
|   _summary: null,
 | |
|   _privateSummary: null,
 | |
| 
 | |
|   /**
 | |
|    * Returns the legacy state integer value for the provided Download object.
 | |
|    */
 | |
|   stateOfDownload(download) {
 | |
|     // Collapse state using the correct priority.
 | |
|     if (!download.stopped) {
 | |
|       return DownloadsCommon.DOWNLOAD_DOWNLOADING;
 | |
|     }
 | |
|     if (download.succeeded) {
 | |
|       return DownloadsCommon.DOWNLOAD_FINISHED;
 | |
|     }
 | |
|     if (download.error) {
 | |
|       if (download.error.becauseBlockedByParentalControls) {
 | |
|         return DownloadsCommon.DOWNLOAD_BLOCKED_PARENTAL;
 | |
|       }
 | |
|       if (download.error.becauseBlockedByReputationCheck) {
 | |
|         return DownloadsCommon.DOWNLOAD_DIRTY;
 | |
|       }
 | |
|       return DownloadsCommon.DOWNLOAD_FAILED;
 | |
|     }
 | |
|     if (download.canceled) {
 | |
|       if (download.hasPartialData) {
 | |
|         return DownloadsCommon.DOWNLOAD_PAUSED;
 | |
|       }
 | |
|       return DownloadsCommon.DOWNLOAD_CANCELED;
 | |
|     }
 | |
|     return DownloadsCommon.DOWNLOAD_NOTSTARTED;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Removes a Download object from both session and history downloads.
 | |
|    */
 | |
|   async deleteDownload(download) {
 | |
|     // Check hasBlockedData to avoid double counting if you click the X button
 | |
|     // in the Libarary view and then delete the download from the history.
 | |
|     if (
 | |
|       download.error?.becauseBlockedByReputationCheck &&
 | |
|       download.hasBlockedData
 | |
|     ) {
 | |
|       Services.telemetry
 | |
|         .getKeyedHistogramById("DOWNLOADS_USER_ACTION_ON_BLOCKED_DOWNLOAD")
 | |
|         .add(download.error.reputationCheckVerdict, 1); // confirm block
 | |
|     }
 | |
| 
 | |
|     // Remove the associated history element first, if any, so that the views
 | |
|     // that combine history and session downloads won't resurrect the history
 | |
|     // download into the view just before it is deleted permanently.
 | |
|     try {
 | |
|       await lazy.PlacesUtils.history.remove(download.source.url);
 | |
|     } catch (ex) {
 | |
|       console.error(ex);
 | |
|     }
 | |
|     let list = await lazy.Downloads.getList(lazy.Downloads.ALL);
 | |
|     await list.remove(download);
 | |
|     await download.finalize(true);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Deletes all files associated with a download, with or without removing it
 | |
|    * from the session downloads list and/or download history.
 | |
|    *
 | |
|    * @param download
 | |
|    *        The download to delete and/or forget.
 | |
|    * @param clearHistory
 | |
|    *        Optional. Removes history from session downloads list or history.
 | |
|    *        0 - Don't remove the download from session list or history.
 | |
|    *        1 - Remove the download from session list, but not history.
 | |
|    *        2 - Remove the download from both session list and history.
 | |
|    */
 | |
|   async deleteDownloadFiles(download, clearHistory = 0) {
 | |
|     if (clearHistory > 1) {
 | |
|       try {
 | |
|         await lazy.PlacesUtils.history.remove(download.source.url);
 | |
|       } catch (ex) {
 | |
|         console.error(ex);
 | |
|       }
 | |
|     }
 | |
|     if (clearHistory > 0) {
 | |
|       let list = await lazy.Downloads.getList(lazy.Downloads.ALL);
 | |
|       await list.remove(download);
 | |
|     }
 | |
|     await download.manuallyRemoveData();
 | |
|     if (clearHistory < 2) {
 | |
|       lazy.DownloadHistory.updateMetaData(download).catch(console.error);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get a nsIMIMEInfo object for a download
 | |
|    */
 | |
|   getMimeInfo(download) {
 | |
|     if (!download.succeeded) {
 | |
|       return null;
 | |
|     }
 | |
|     let contentType = download.contentType;
 | |
|     let url = Cc["@mozilla.org/network/standard-url-mutator;1"]
 | |
|       .createInstance(Ci.nsIURIMutator)
 | |
|       .setSpec("http://example.com") // construct the URL
 | |
|       .setFilePath(download.target.path)
 | |
|       .finalize()
 | |
|       .QueryInterface(Ci.nsIURL);
 | |
|     let fileExtension = url.fileExtension;
 | |
| 
 | |
|     // look at file extension if there's no contentType or it is generic
 | |
|     if (!contentType || kGenericContentTypes.includes(contentType)) {
 | |
|       try {
 | |
|         contentType = lazy.gMIMEService.getTypeFromExtension(fileExtension);
 | |
|       } catch (ex) {
 | |
|         DownloadsCommon.log(
 | |
|           "Cant get mimeType from file extension: ",
 | |
|           fileExtension
 | |
|         );
 | |
|       }
 | |
|     }
 | |
|     if (!(contentType || fileExtension)) {
 | |
|       return null;
 | |
|     }
 | |
|     let mimeInfo = null;
 | |
|     try {
 | |
|       mimeInfo = lazy.gMIMEService.getFromTypeAndExtension(
 | |
|         contentType || "",
 | |
|         fileExtension || ""
 | |
|       );
 | |
|     } catch (ex) {
 | |
|       DownloadsCommon.log(
 | |
|         "Can't get nsIMIMEInfo for contentType: ",
 | |
|         contentType,
 | |
|         "and fileExtension:",
 | |
|         fileExtension
 | |
|       );
 | |
|     }
 | |
|     return mimeInfo;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Confirm if the download exists on the filesystem and is a given mime-type
 | |
|    */
 | |
|   isFileOfType(download, mimeType) {
 | |
|     if (!(download.succeeded && download.target?.exists)) {
 | |
|       DownloadsCommon.log(
 | |
|         `isFileOfType returning false for mimeType: ${mimeType}, succeeded: ${download.succeeded}, exists: ${download.target?.exists}`
 | |
|       );
 | |
|       return false;
 | |
|     }
 | |
|     let mimeInfo = DownloadsCommon.getMimeInfo(download);
 | |
|     return mimeInfo?.type === mimeType.toLowerCase();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Copies the source URI of the given Download object to the clipboard.
 | |
|    */
 | |
|   copyDownloadLink(download) {
 | |
|     lazy.gClipboardHelper.copyString(
 | |
|       download.source.originalUrl || download.source.url
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Given an iterable collection of Download objects, generates and returns
 | |
|    * statistics about that collection.
 | |
|    *
 | |
|    * @param downloads An iterable collection of Download objects.
 | |
|    *
 | |
|    * @return Object whose properties are the generated statistics. Currently,
 | |
|    *         we return the following properties:
 | |
|    *
 | |
|    *         numActive       : The total number of downloads.
 | |
|    *         numPaused       : The total number of paused downloads.
 | |
|    *         numDownloading  : The total number of downloads being downloaded.
 | |
|    *         totalSize       : The total size of all downloads once completed.
 | |
|    *         totalTransferred: The total amount of transferred data for these
 | |
|    *                           downloads.
 | |
|    *         slowestSpeed    : The slowest download rate.
 | |
|    *         rawTimeLeft     : The estimated time left for the downloads to
 | |
|    *                           complete.
 | |
|    *         percentComplete : The percentage of bytes successfully downloaded.
 | |
|    */
 | |
|   summarizeDownloads(downloads) {
 | |
|     let summary = {
 | |
|       numActive: 0,
 | |
|       numPaused: 0,
 | |
|       numDownloading: 0,
 | |
|       totalSize: 0,
 | |
|       totalTransferred: 0,
 | |
|       // slowestSpeed is Infinity so that we can use Math.min to
 | |
|       // find the slowest speed. We'll set this to 0 afterwards if
 | |
|       // it's still at Infinity by the time we're done iterating all
 | |
|       // download.
 | |
|       slowestSpeed: Infinity,
 | |
|       rawTimeLeft: -1,
 | |
|       percentComplete: -1,
 | |
|     };
 | |
| 
 | |
|     for (let download of downloads) {
 | |
|       summary.numActive++;
 | |
| 
 | |
|       if (!download.stopped) {
 | |
|         summary.numDownloading++;
 | |
|         if (download.hasProgress && download.speed > 0) {
 | |
|           let sizeLeft = download.totalBytes - download.currentBytes;
 | |
|           summary.rawTimeLeft = Math.max(
 | |
|             summary.rawTimeLeft,
 | |
|             sizeLeft / download.speed
 | |
|           );
 | |
|           summary.slowestSpeed = Math.min(summary.slowestSpeed, download.speed);
 | |
|         }
 | |
|       } else if (download.canceled && download.hasPartialData) {
 | |
|         summary.numPaused++;
 | |
|       }
 | |
| 
 | |
|       // Only add to total values if we actually know the download size.
 | |
|       if (download.succeeded) {
 | |
|         summary.totalSize += download.target.size;
 | |
|         summary.totalTransferred += download.target.size;
 | |
|       } else if (download.hasProgress) {
 | |
|         summary.totalSize += download.totalBytes;
 | |
|         summary.totalTransferred += download.currentBytes;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (summary.totalSize != 0) {
 | |
|       summary.percentComplete = Math.floor(
 | |
|         (summary.totalTransferred / summary.totalSize) * 100
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     if (summary.slowestSpeed == Infinity) {
 | |
|       summary.slowestSpeed = 0;
 | |
|     }
 | |
| 
 | |
|     return summary;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * If necessary, smooths the estimated number of seconds remaining for one
 | |
|    * or more downloads to complete.
 | |
|    *
 | |
|    * @param aSeconds
 | |
|    *        Current raw estimate on number of seconds left for one or more
 | |
|    *        downloads. This is a floating point value to help get sub-second
 | |
|    *        accuracy for current and future estimates.
 | |
|    */
 | |
|   smoothSeconds(aSeconds, aLastSeconds) {
 | |
|     // We apply an algorithm similar to the DownloadUtils.getTimeLeft function,
 | |
|     // though tailored to a single time estimation for all downloads.  We never
 | |
|     // apply something if the new value is less than half the previous value.
 | |
|     let shouldApplySmoothing = aLastSeconds >= 0 && aSeconds > aLastSeconds / 2;
 | |
|     if (shouldApplySmoothing) {
 | |
|       // Apply hysteresis to favor downward over upward swings.  Trust only 30%
 | |
|       // of the new value if lower, and 10% if higher (exponential smoothing).
 | |
|       let diff = aSeconds - aLastSeconds;
 | |
|       aSeconds = aLastSeconds + (diff < 0 ? 0.3 : 0.1) * diff;
 | |
| 
 | |
|       // If the new time is similar, reuse something close to the last time
 | |
|       // left, but subtract a little to provide forward progress.
 | |
|       diff = aSeconds - aLastSeconds;
 | |
|       let diffPercent = (diff / aLastSeconds) * 100;
 | |
|       if (Math.abs(diff) < 5 || Math.abs(diffPercent) < 5) {
 | |
|         aSeconds = aLastSeconds - (diff < 0 ? 0.4 : 0.2);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // In the last few seconds of downloading, we are always subtracting and
 | |
|     // never adding to the time left.  Ensure that we never fall below one
 | |
|     // second left until all downloads are actually finished.
 | |
|     return (aLastSeconds = Math.max(aSeconds, 1));
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Opens a downloaded file.
 | |
|    *
 | |
|    * @param downloadProperties
 | |
|    *        A Download object or the initial properties of a serialized download
 | |
|    * @param options.openWhere
 | |
|    *        Optional string indicating how to handle opening a download target file URI.
 | |
|    *        One of "window", "tab", "tabshifted".
 | |
|    * @param options.useSystemDefault
 | |
|    *        Optional value indicating how to handle launching this download,
 | |
|    *        this call only. Will override the associated mimeInfo.preferredAction
 | |
|    * @return {Promise}
 | |
|    * @resolves When the instruction to launch the file has been
 | |
|    *           successfully given to the operating system or handled internally
 | |
|    * @rejects  JavaScript exception if there was an error trying to launch
 | |
|    *           the file.
 | |
|    */
 | |
|   async openDownload(download, options) {
 | |
|     // some download objects got serialized and need reconstituting
 | |
|     if (typeof download.launch !== "function") {
 | |
|       download = await lazy.Downloads.createDownload(download);
 | |
|     }
 | |
|     return download.launch(options).catch(ex => console.error(ex));
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Show a downloaded file in the system file manager.
 | |
|    *
 | |
|    * @param aFile
 | |
|    *        a downloaded file.
 | |
|    */
 | |
|   showDownloadedFile(aFile) {
 | |
|     if (!(aFile instanceof Ci.nsIFile)) {
 | |
|       throw new Error("aFile must be a nsIFile object");
 | |
|     }
 | |
|     try {
 | |
|       // Show the directory containing the file and select the file.
 | |
|       aFile.reveal();
 | |
|     } catch (ex) {
 | |
|       // If reveal fails for some reason (e.g., it's not implemented on unix
 | |
|       // or the file doesn't exist), try using the parent if we have it.
 | |
|       let parent = aFile.parent;
 | |
|       if (parent) {
 | |
|         this.showDirectory(parent);
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Show the specified folder in the system file manager.
 | |
|    *
 | |
|    * @param aDirectory
 | |
|    *        a directory to be opened with system file manager.
 | |
|    */
 | |
|   showDirectory(aDirectory) {
 | |
|     if (!(aDirectory instanceof Ci.nsIFile)) {
 | |
|       throw new Error("aDirectory must be a nsIFile object");
 | |
|     }
 | |
|     try {
 | |
|       aDirectory.launch();
 | |
|     } catch (ex) {
 | |
|       // If launch fails (probably because it's not implemented), let
 | |
|       // the OS handler try to open the directory.
 | |
|       Cc["@mozilla.org/uriloader/external-protocol-service;1"]
 | |
|         .getService(Ci.nsIExternalProtocolService)
 | |
|         .loadURI(
 | |
|           lazy.NetUtil.newURI(aDirectory),
 | |
|           Services.scriptSecurityManager.getSystemPrincipal()
 | |
|         );
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Displays an alert message box which asks the user if they want to
 | |
|    * unblock the downloaded file or not.
 | |
|    *
 | |
|    * @param options
 | |
|    *        An object with the following properties:
 | |
|    *        {
 | |
|    *          verdict:
 | |
|    *            The detailed reason why the download was blocked, according to
 | |
|    *            the "Downloads.Error.BLOCK_VERDICT_" constants. If an unknown
 | |
|    *            reason is specified, "Downloads.Error.BLOCK_VERDICT_MALWARE" is
 | |
|    *            assumed.
 | |
|    *          window:
 | |
|    *            The window with which this action is associated.
 | |
|    *          dialogType:
 | |
|    *            String that determines which actions are available:
 | |
|    *             - "unblock" to offer just "unblock".
 | |
|    *             - "chooseUnblock" to offer "unblock" and "confirmBlock".
 | |
|    *             - "chooseOpen" to offer "open" and "confirmBlock".
 | |
|    *        }
 | |
|    *
 | |
|    * @return {Promise}
 | |
|    * @resolves String representing the action that should be executed:
 | |
|    *            - "open" to allow the download and open the file.
 | |
|    *            - "unblock" to allow the download without opening the file.
 | |
|    *            - "confirmBlock" to delete the blocked data permanently.
 | |
|    *            - "cancel" to do nothing and cancel the operation.
 | |
|    */
 | |
|   async confirmUnblockDownload({ verdict, window, dialogType }) {
 | |
|     let s = DownloadsCommon.strings;
 | |
| 
 | |
|     // All the dialogs have an action button and a cancel button, while only
 | |
|     // some of them have an additonal button to remove the file. The cancel
 | |
|     // button must always be the one at BUTTON_POS_1 because this is the value
 | |
|     // returned by confirmEx when using ESC or closing the dialog (bug 345067).
 | |
|     let title = s.unblockHeaderUnblock;
 | |
|     let firstButtonText = s.unblockButtonUnblock;
 | |
|     let firstButtonAction = "unblock";
 | |
|     let buttonFlags =
 | |
|       Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 +
 | |
|       Ci.nsIPrompt.BUTTON_TITLE_CANCEL * Ci.nsIPrompt.BUTTON_POS_1;
 | |
| 
 | |
|     switch (dialogType) {
 | |
|       case "unblock":
 | |
|         // Use only the unblock action. The default is to cancel.
 | |
|         buttonFlags += Ci.nsIPrompt.BUTTON_POS_1_DEFAULT;
 | |
|         break;
 | |
|       case "chooseUnblock":
 | |
|         // Use the unblock and remove file actions. The default is remove file.
 | |
|         buttonFlags +=
 | |
|           Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2 +
 | |
|           Ci.nsIPrompt.BUTTON_POS_2_DEFAULT;
 | |
|         break;
 | |
|       case "chooseOpen":
 | |
|         // Use the unblock and open file actions. The default is open file.
 | |
|         title = s.unblockHeaderOpen;
 | |
|         firstButtonText = s.unblockButtonOpen;
 | |
|         firstButtonAction = "open";
 | |
|         buttonFlags +=
 | |
|           Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2 +
 | |
|           Ci.nsIPrompt.BUTTON_POS_0_DEFAULT;
 | |
|         break;
 | |
|       default:
 | |
|         console.error("Unexpected dialog type: " + dialogType);
 | |
|         return "cancel";
 | |
|     }
 | |
| 
 | |
|     let message;
 | |
|     switch (verdict) {
 | |
|       case lazy.Downloads.Error.BLOCK_VERDICT_UNCOMMON:
 | |
|         message = s.unblockTypeUncommon2;
 | |
|         break;
 | |
|       case lazy.Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
 | |
|         message = s.unblockTypePotentiallyUnwanted2;
 | |
|         break;
 | |
|       case lazy.Downloads.Error.BLOCK_VERDICT_INSECURE:
 | |
|         message = s.unblockInsecure2;
 | |
|         break;
 | |
|       default:
 | |
|         // Assume Downloads.Error.BLOCK_VERDICT_MALWARE
 | |
|         message = s.unblockTypeMalware;
 | |
|         break;
 | |
|     }
 | |
|     message += "\n\n" + s.unblockTip2;
 | |
| 
 | |
|     Services.ww.registerNotification(function onOpen(subj, topic) {
 | |
|       if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) {
 | |
|         // Make sure to listen for "DOMContentLoaded" because it is fired
 | |
|         // before the "load" event.
 | |
|         subj.addEventListener(
 | |
|           "DOMContentLoaded",
 | |
|           function () {
 | |
|             if (
 | |
|               subj.document.documentURI ==
 | |
|               "chrome://global/content/commonDialog.xhtml"
 | |
|             ) {
 | |
|               Services.ww.unregisterNotification(onOpen);
 | |
|               let dialog = subj.document.getElementById("commonDialog");
 | |
|               if (dialog) {
 | |
|                 // Change the dialog to use a warning icon.
 | |
|                 dialog.classList.add("alert-dialog");
 | |
|               }
 | |
|             }
 | |
|           },
 | |
|           { once: true }
 | |
|         );
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     let rv = Services.prompt.confirmEx(
 | |
|       window,
 | |
|       title,
 | |
|       message,
 | |
|       buttonFlags,
 | |
|       firstButtonText,
 | |
|       null,
 | |
|       s.unblockButtonConfirmBlock,
 | |
|       null,
 | |
|       {}
 | |
|     );
 | |
|     return [firstButtonAction, "cancel", "confirmBlock"][rv];
 | |
|   },
 | |
| };
 | |
| 
 | |
| ChromeUtils.defineLazyGetter(DownloadsCommon, "log", () => {
 | |
|   return lazy.DownloadsLogger.log.bind(lazy.DownloadsLogger);
 | |
| });
 | |
| ChromeUtils.defineLazyGetter(DownloadsCommon, "error", () => {
 | |
|   return lazy.DownloadsLogger.error.bind(lazy.DownloadsLogger);
 | |
| });
 | |
| 
 | |
| // DownloadsData
 | |
| 
 | |
| /**
 | |
|  * Retrieves the list of past and completed downloads from the underlying
 | |
|  * Downloads API data, and provides asynchronous notifications allowing to
 | |
|  * build a consistent view of the available data.
 | |
|  *
 | |
|  * Note that using this object does not automatically initialize the list of
 | |
|  * downloads. This is useful to display a neutral progress indicator in
 | |
|  * the main browser window until the autostart timeout elapses.
 | |
|  *
 | |
|  * This powers the DownloadsData, PrivateDownloadsData, and HistoryDownloadsData
 | |
|  * singleton objects.
 | |
|  */
 | |
| function DownloadsDataCtor({ isPrivate, isHistory, maxHistoryResults } = {}) {
 | |
|   this._isPrivate = !!isPrivate;
 | |
| 
 | |
|   // Contains all the available Download objects and their integer state.
 | |
|   this._oldDownloadStates = new WeakMap();
 | |
| 
 | |
|   // For the history downloads list we don't need to register this as a view,
 | |
|   // but we have to ensure that the DownloadsData object is initialized before
 | |
|   // we register more views. This ensures that the view methods of DownloadsData
 | |
|   // are invoked before those of views registered on HistoryDownloadsData,
 | |
|   // allowing the endTime property to be set correctly.
 | |
|   if (isHistory) {
 | |
|     if (isPrivate) {
 | |
|       lazy.PrivateDownloadsData.initializeDataLink();
 | |
|     }
 | |
|     lazy.DownloadsData.initializeDataLink();
 | |
|     this._promiseList = lazy.DownloadsData._promiseList.then(() => {
 | |
|       // For history downloads in Private Browsing mode, we'll fetch the combined
 | |
|       // list of public and private downloads.
 | |
|       return lazy.DownloadHistory.getList({
 | |
|         type: isPrivate ? lazy.Downloads.ALL : lazy.Downloads.PUBLIC,
 | |
|         maxHistoryResults,
 | |
|       });
 | |
|     });
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   // This defines "initializeDataLink" and "_promiseList" synchronously, then
 | |
|   // continues execution only when "initializeDataLink" is called, allowing the
 | |
|   // underlying data to be loaded only when actually needed.
 | |
|   this._promiseList = (async () => {
 | |
|     await new Promise(resolve => (this.initializeDataLink = resolve));
 | |
|     let list = await lazy.Downloads.getList(
 | |
|       isPrivate ? lazy.Downloads.PRIVATE : lazy.Downloads.PUBLIC
 | |
|     );
 | |
|     await list.addView(this);
 | |
|     return list;
 | |
|   })();
 | |
| }
 | |
| 
 | |
| DownloadsDataCtor.prototype = {
 | |
|   /**
 | |
|    * Starts receiving events for current downloads.
 | |
|    */
 | |
|   initializeDataLink() {},
 | |
| 
 | |
|   /**
 | |
|    * Promise resolved with the underlying DownloadList object once we started
 | |
|    * receiving events for current downloads.
 | |
|    */
 | |
|   _promiseList: null,
 | |
| 
 | |
|   /**
 | |
|    * Iterator for all the available Download objects. This is empty until the
 | |
|    * data has been loaded using the JavaScript API for downloads.
 | |
|    */
 | |
|   get _downloads() {
 | |
|     return ChromeUtils.nondeterministicGetWeakMapKeys(this._oldDownloadStates);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * True if there are finished downloads that can be removed from the list.
 | |
|    */
 | |
|   get canRemoveFinished() {
 | |
|     for (let download of this._downloads) {
 | |
|       // Stopped, paused, and failed downloads with partial data are removed.
 | |
|       if (download.stopped && !(download.canceled && download.hasPartialData)) {
 | |
|         return true;
 | |
|       }
 | |
|     }
 | |
|     return false;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Asks the back-end to remove finished downloads from the list. This method
 | |
|    * is only called after the data link has been initialized.
 | |
|    */
 | |
|   removeFinished() {
 | |
|     lazy.Downloads.getList(
 | |
|       this._isPrivate ? lazy.Downloads.PRIVATE : lazy.Downloads.PUBLIC
 | |
|     )
 | |
|       .then(list => list.removeFinished())
 | |
|       .catch(console.error);
 | |
|   },
 | |
| 
 | |
|   // Integration with the asynchronous Downloads back-end
 | |
| 
 | |
|   onDownloadAdded(download) {
 | |
|     // Download objects do not store the end time of downloads, as the Downloads
 | |
|     // API does not need to persist this information for all platforms. Once a
 | |
|     // download terminates on a Desktop browser, it becomes a history download,
 | |
|     // for which the end time is stored differently, as a Places annotation.
 | |
|     download.endTime = Date.now();
 | |
| 
 | |
|     this._oldDownloadStates.set(
 | |
|       download,
 | |
|       DownloadsCommon.stateOfDownload(download)
 | |
|     );
 | |
|     if (download.error?.becauseBlockedByReputationCheck) {
 | |
|       this._notifyDownloadEvent("error");
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   onDownloadChanged(download) {
 | |
|     let oldState = this._oldDownloadStates.get(download);
 | |
|     let newState = DownloadsCommon.stateOfDownload(download);
 | |
|     this._oldDownloadStates.set(download, newState);
 | |
| 
 | |
|     if (oldState != newState) {
 | |
|       if (
 | |
|         download.succeeded ||
 | |
|         (download.canceled && !download.hasPartialData) ||
 | |
|         download.error
 | |
|       ) {
 | |
|         // Store the end time that may be displayed by the views.
 | |
|         download.endTime = Date.now();
 | |
| 
 | |
|         // This state transition code should actually be located in a Downloads
 | |
|         // API module (bug 941009).
 | |
|         lazy.DownloadHistory.updateMetaData(download).catch(console.error);
 | |
|       }
 | |
| 
 | |
|       if (
 | |
|         download.succeeded ||
 | |
|         (download.error && download.error.becauseBlocked)
 | |
|       ) {
 | |
|         this._notifyDownloadEvent("finish");
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (!download.newDownloadNotified) {
 | |
|       download.newDownloadNotified = true;
 | |
|       this._notifyDownloadEvent("start", {
 | |
|         openDownloadsListOnStart: download.openDownloadsListOnStart,
 | |
|       });
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   onDownloadRemoved(download) {
 | |
|     this._oldDownloadStates.delete(download);
 | |
|   },
 | |
| 
 | |
|   // Registration of views
 | |
| 
 | |
|   /**
 | |
|    * Adds an object to be notified when the available download data changes.
 | |
|    * The specified object is initialized with the currently available downloads.
 | |
|    *
 | |
|    * @param aView
 | |
|    *        DownloadsView object to be added.  This reference must be passed to
 | |
|    *        removeView before termination.
 | |
|    */
 | |
|   addView(aView) {
 | |
|     this._promiseList.then(list => list.addView(aView)).catch(console.error);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Removes an object previously added using addView.
 | |
|    *
 | |
|    * @param aView
 | |
|    *        DownloadsView object to be removed.
 | |
|    */
 | |
|   removeView(aView) {
 | |
|     this._promiseList.then(list => list.removeView(aView)).catch(console.error);
 | |
|   },
 | |
| 
 | |
|   // Notifications sent to the most recent browser window only
 | |
| 
 | |
|   /**
 | |
|    * Set to true after the first download causes the downloads panel to be
 | |
|    * displayed.
 | |
|    */
 | |
|   get panelHasShownBefore() {
 | |
|     try {
 | |
|       return Services.prefs.getBoolPref("browser.download.panel.shown");
 | |
|     } catch (ex) {}
 | |
|     return false;
 | |
|   },
 | |
| 
 | |
|   set panelHasShownBefore(aValue) {
 | |
|     Services.prefs.setBoolPref("browser.download.panel.shown", aValue);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Displays a new or finished download notification in the most recent browser
 | |
|    * window, if one is currently available with the required privacy type.
 | |
|    * @param {string} aType
 | |
|    *        Set to "start" for new downloads, "finish" for completed downloads,
 | |
|    *        "error" for downloads that failed and need attention
 | |
|    * @param {boolean} [openDownloadsListOnStart]
 | |
|    *        (Only relevant when aType = "start")
 | |
|    *        true (default) - open the downloads panel.
 | |
|    *        false - only show an indicator notification.
 | |
|    */
 | |
|   _notifyDownloadEvent(aType, { openDownloadsListOnStart = true } = {}) {
 | |
|     DownloadsCommon.log(
 | |
|       "Attempting to notify that a new download has started or finished."
 | |
|     );
 | |
| 
 | |
|     // Show the panel in the most recent browser window, if present.
 | |
|     let browserWin = lazy.BrowserWindowTracker.getTopWindow({
 | |
|       private: this._isPrivate,
 | |
|     });
 | |
|     if (!browserWin) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let shouldOpenDownloadsPanel =
 | |
|       aType == "start" &&
 | |
|       DownloadsCommon.summarizeDownloads(this._downloads).numDownloading <= 1 &&
 | |
|       lazy.gAlwaysOpenPanel;
 | |
| 
 | |
|     // For new downloads after the first one, don't show the panel
 | |
|     // automatically, but provide a visible notification in the topmost browser
 | |
|     // window, if the status indicator is already visible. Also ensure that if
 | |
|     // openDownloadsListOnStart = false is passed, we always skip opening the
 | |
|     // panel. That's because this will only be passed if the download is started
 | |
|     // without user interaction or if a dialog was previously opened in the
 | |
|     // process of the download (e.g. unknown content type dialog).
 | |
|     if (
 | |
|       aType != "error" &&
 | |
|       ((this.panelHasShownBefore && !shouldOpenDownloadsPanel) ||
 | |
|         !openDownloadsListOnStart ||
 | |
|         browserWin != Services.focus.activeWindow)
 | |
|     ) {
 | |
|       DownloadsCommon.log("Showing new download notification.");
 | |
|       browserWin.DownloadsIndicatorView.showEventNotification(aType);
 | |
|       return;
 | |
|     }
 | |
|     this.panelHasShownBefore = true;
 | |
|     browserWin.DownloadsPanel.showPanel();
 | |
|   },
 | |
| };
 | |
| 
 | |
| ChromeUtils.defineLazyGetter(lazy, "HistoryDownloadsData", function () {
 | |
|   return new DownloadsDataCtor({ isHistory: true });
 | |
| });
 | |
| 
 | |
| ChromeUtils.defineLazyGetter(lazy, "LimitedHistoryDownloadsData", function () {
 | |
|   return new DownloadsDataCtor({
 | |
|     isHistory: true,
 | |
|     maxHistoryResults: kMaxHistoryResultsForLimitedView,
 | |
|   });
 | |
| });
 | |
| 
 | |
| ChromeUtils.defineLazyGetter(
 | |
|   lazy,
 | |
|   "LimitedPrivateHistoryDownloadData",
 | |
|   function () {
 | |
|     return new DownloadsDataCtor({
 | |
|       isPrivate: true,
 | |
|       isHistory: true,
 | |
|       maxHistoryResults: kMaxHistoryResultsForLimitedView,
 | |
|     });
 | |
|   }
 | |
| );
 | |
| 
 | |
| ChromeUtils.defineLazyGetter(lazy, "PrivateDownloadsData", function () {
 | |
|   return new DownloadsDataCtor({ isPrivate: true });
 | |
| });
 | |
| 
 | |
| ChromeUtils.defineLazyGetter(lazy, "DownloadsData", function () {
 | |
|   return new DownloadsDataCtor();
 | |
| });
 | |
| 
 | |
| // DownloadsViewPrototype
 | |
| 
 | |
| /**
 | |
|  * A prototype for an object that registers itself with DownloadsData as soon
 | |
|  * as a view is registered with it.
 | |
|  */
 | |
| const DownloadsViewPrototype = {
 | |
|   /**
 | |
|    * Contains all the available Download objects and their current state value.
 | |
|    *
 | |
|    * SUBCLASSES MUST OVERRIDE THIS PROPERTY.
 | |
|    */
 | |
|   _oldDownloadStates: null,
 | |
| 
 | |
|   // Registration of views
 | |
| 
 | |
|   /**
 | |
|    * Array of view objects that should be notified when the available status
 | |
|    * data changes.
 | |
|    *
 | |
|    * SUBCLASSES MUST OVERRIDE THIS PROPERTY.
 | |
|    */
 | |
|   _views: null,
 | |
| 
 | |
|   /**
 | |
|    * Determines whether this view object is over the private or non-private
 | |
|    * downloads.
 | |
|    *
 | |
|    * SUBCLASSES MUST OVERRIDE THIS PROPERTY.
 | |
|    */
 | |
|   _isPrivate: false,
 | |
| 
 | |
|   /**
 | |
|    * Adds an object to be notified when the available status data changes.
 | |
|    * The specified object is initialized with the currently available status.
 | |
|    *
 | |
|    * @param aView
 | |
|    *        View object to be added.  This reference must be
 | |
|    *        passed to removeView before termination.
 | |
|    */
 | |
|   addView(aView) {
 | |
|     // Start receiving events when the first of our views is registered.
 | |
|     if (!this._views.length) {
 | |
|       if (this._isPrivate) {
 | |
|         lazy.PrivateDownloadsData.addView(this);
 | |
|       } else {
 | |
|         lazy.DownloadsData.addView(this);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     this._views.push(aView);
 | |
|     this.refreshView(aView);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Updates the properties of an object previously added using addView.
 | |
|    *
 | |
|    * @param aView
 | |
|    *        View object to be updated.
 | |
|    */
 | |
|   refreshView(aView) {
 | |
|     // Update immediately even if we are still loading data asynchronously.
 | |
|     // Subclasses must provide these two functions!
 | |
|     this._refreshProperties();
 | |
|     this._updateView(aView);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Removes an object previously added using addView.
 | |
|    *
 | |
|    * @param aView
 | |
|    *        View object to be removed.
 | |
|    */
 | |
|   removeView(aView) {
 | |
|     let index = this._views.indexOf(aView);
 | |
|     if (index != -1) {
 | |
|       this._views.splice(index, 1);
 | |
|     }
 | |
| 
 | |
|     // Stop receiving events when the last of our views is unregistered.
 | |
|     if (!this._views.length) {
 | |
|       if (this._isPrivate) {
 | |
|         lazy.PrivateDownloadsData.removeView(this);
 | |
|       } else {
 | |
|         lazy.DownloadsData.removeView(this);
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   // Callback functions from DownloadList
 | |
| 
 | |
|   /**
 | |
|    * Indicates whether we are still loading downloads data asynchronously.
 | |
|    */
 | |
|   _loading: false,
 | |
| 
 | |
|   /**
 | |
|    * Called before multiple downloads are about to be loaded.
 | |
|    */
 | |
|   onDownloadBatchStarting() {
 | |
|     this._loading = true;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Called after data loading finished.
 | |
|    */
 | |
|   onDownloadBatchEnded() {
 | |
|     this._loading = false;
 | |
|     this._updateViews();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Called when a new download data item is available, either during the
 | |
|    * asynchronous data load or when a new download is started.
 | |
|    *
 | |
|    * @param download
 | |
|    *        Download object that was just added.
 | |
|    *
 | |
|    * @note Subclasses should override this and still call the base method.
 | |
|    */
 | |
|   onDownloadAdded(download) {
 | |
|     this._oldDownloadStates.set(
 | |
|       download,
 | |
|       DownloadsCommon.stateOfDownload(download)
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Called when the overall state of a Download has changed. In particular,
 | |
|    * this is called only once when the download succeeds or is blocked
 | |
|    * permanently, and is never called if only the current progress changed.
 | |
|    *
 | |
|    * The onDownloadChanged notification will always be sent afterwards.
 | |
|    *
 | |
|    * @note Subclasses should override this.
 | |
|    */
 | |
|   onDownloadStateChanged(download) {
 | |
|     throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Called every time any state property of a Download may have changed,
 | |
|    * including progress properties.
 | |
|    *
 | |
|    * Note that progress notification changes are throttled at the Downloads.sys.mjs
 | |
|    * API level, and there is no throttling mechanism in the front-end.
 | |
|    *
 | |
|    * @note Subclasses should override this and still call the base method.
 | |
|    */
 | |
|   onDownloadChanged(download) {
 | |
|     let oldState = this._oldDownloadStates.get(download);
 | |
|     let newState = DownloadsCommon.stateOfDownload(download);
 | |
|     this._oldDownloadStates.set(download, newState);
 | |
| 
 | |
|     if (oldState != newState) {
 | |
|       this.onDownloadStateChanged(download);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Called when a data item is removed, ensures that the widget associated with
 | |
|    * the view item is removed from the user interface.
 | |
|    *
 | |
|    * @param download
 | |
|    *        Download object that is being removed.
 | |
|    *
 | |
|    * @note Subclasses should override this.
 | |
|    */
 | |
|   onDownloadRemoved(download) {
 | |
|     this._oldDownloadStates.delete(download);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Private function used to refresh the internal properties being sent to
 | |
|    * each registered view.
 | |
|    *
 | |
|    * @note Subclasses should override this.
 | |
|    */
 | |
|   _refreshProperties() {
 | |
|     throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Private function used to refresh an individual view.
 | |
|    *
 | |
|    * @note Subclasses should override this.
 | |
|    */
 | |
|   _updateView() {
 | |
|     throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Computes aggregate values and propagates the changes to our views.
 | |
|    */
 | |
|   _updateViews() {
 | |
|     // Do not update the status indicators during batch loads of download items.
 | |
|     if (this._loading) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this._refreshProperties();
 | |
|     this._views.forEach(this._updateView, this);
 | |
|   },
 | |
| };
 | |
| 
 | |
| // DownloadsIndicatorData
 | |
| 
 | |
| /**
 | |
|  * This object registers itself with DownloadsData as a view, and transforms the
 | |
|  * notifications it receives into overall status data, that is then broadcast to
 | |
|  * the registered download status indicators.
 | |
|  *
 | |
|  * Note that using this object does not automatically start the Download Manager
 | |
|  * service.  Consumers will see an empty list of downloads until the service is
 | |
|  * actually started.  This is useful to display a neutral progress indicator in
 | |
|  * the main browser window until the autostart timeout elapses.
 | |
|  */
 | |
| function DownloadsIndicatorDataCtor(aPrivate) {
 | |
|   this._oldDownloadStates = new WeakMap();
 | |
|   this._isPrivate = aPrivate;
 | |
|   this._views = [];
 | |
| }
 | |
| DownloadsIndicatorDataCtor.prototype = {
 | |
|   /**
 | |
|    * Map of the relative severities of different attention states.
 | |
|    * Used in sorting the map of active downloads' attention states
 | |
|    * to determine the attention state to be displayed.
 | |
|    */
 | |
|   _attentionPriority: new Map([
 | |
|     [DownloadsCommon.ATTENTION_NONE, 0],
 | |
|     [DownloadsCommon.ATTENTION_SUCCESS, 1],
 | |
|     [DownloadsCommon.ATTENTION_INFO, 2],
 | |
|     [DownloadsCommon.ATTENTION_WARNING, 3],
 | |
|     [DownloadsCommon.ATTENTION_SEVERE, 4],
 | |
|   ]),
 | |
| 
 | |
|   /**
 | |
|    * Iterator for all the available Download objects. This is empty until the
 | |
|    * data has been loaded using the JavaScript API for downloads.
 | |
|    */
 | |
|   get _downloads() {
 | |
|     return ChromeUtils.nondeterministicGetWeakMapKeys(this._oldDownloadStates);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Removes an object previously added using addView.
 | |
|    *
 | |
|    * @param aView
 | |
|    *        DownloadsIndicatorView object to be removed.
 | |
|    */
 | |
|   removeView(aView) {
 | |
|     DownloadsViewPrototype.removeView.call(this, aView);
 | |
| 
 | |
|     if (!this._views.length) {
 | |
|       this._itemCount = 0;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   onDownloadAdded(download) {
 | |
|     DownloadsViewPrototype.onDownloadAdded.call(this, download);
 | |
|     this._itemCount++;
 | |
|     this._updateViews();
 | |
|   },
 | |
| 
 | |
|   onDownloadStateChanged(download) {
 | |
|     if (this._attentionSuppressed !== DownloadsCommon.SUPPRESS_NONE) {
 | |
|       return;
 | |
|     }
 | |
|     let attention;
 | |
|     if (
 | |
|       !download.succeeded &&
 | |
|       download.error &&
 | |
|       download.error.reputationCheckVerdict
 | |
|     ) {
 | |
|       switch (download.error.reputationCheckVerdict) {
 | |
|         case lazy.Downloads.Error.BLOCK_VERDICT_UNCOMMON:
 | |
|           attention = DownloadsCommon.ATTENTION_INFO;
 | |
|           break;
 | |
|         case lazy.Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED: // fall-through
 | |
|         case lazy.Downloads.Error.BLOCK_VERDICT_INSECURE:
 | |
|         case lazy.Downloads.Error.BLOCK_VERDICT_DOWNLOAD_SPAM:
 | |
|           attention = DownloadsCommon.ATTENTION_WARNING;
 | |
|           break;
 | |
|         case lazy.Downloads.Error.BLOCK_VERDICT_MALWARE:
 | |
|           attention = DownloadsCommon.ATTENTION_SEVERE;
 | |
|           break;
 | |
|         default:
 | |
|           attention = DownloadsCommon.ATTENTION_SEVERE;
 | |
|           console.error(
 | |
|             "Unknown reputation verdict: " +
 | |
|               download.error.reputationCheckVerdict
 | |
|           );
 | |
|       }
 | |
|     } else if (download.succeeded) {
 | |
|       attention = DownloadsCommon.ATTENTION_SUCCESS;
 | |
|     } else if (download.error) {
 | |
|       attention = DownloadsCommon.ATTENTION_WARNING;
 | |
|     }
 | |
|     download.attention = attention;
 | |
|     this.updateAttention();
 | |
|   },
 | |
| 
 | |
|   onDownloadChanged(download) {
 | |
|     DownloadsViewPrototype.onDownloadChanged.call(this, download);
 | |
|     this._updateViews();
 | |
|   },
 | |
| 
 | |
|   onDownloadRemoved(download) {
 | |
|     DownloadsViewPrototype.onDownloadRemoved.call(this, download);
 | |
|     this._itemCount--;
 | |
|     this.updateAttention();
 | |
|     this._updateViews();
 | |
|   },
 | |
| 
 | |
|   // Propagation of properties to our views
 | |
| 
 | |
|   // The following properties are updated by _refreshProperties and are then
 | |
|   // propagated to the views.  See _refreshProperties for details.
 | |
|   _hasDownloads: false,
 | |
|   _percentComplete: -1,
 | |
| 
 | |
|   /**
 | |
|    * Indicates whether the download indicators should be highlighted.
 | |
|    */
 | |
|   set attention(aValue) {
 | |
|     this._attention = aValue;
 | |
|     this._updateViews();
 | |
|   },
 | |
|   _attention: DownloadsCommon.ATTENTION_NONE,
 | |
| 
 | |
|   /**
 | |
|    * Indicates whether the user is interacting with downloads, thus the
 | |
|    * attention indication should not be shown even if requested.
 | |
|    */
 | |
|   set attentionSuppressed(aFlags) {
 | |
|     this._attentionSuppressed = aFlags;
 | |
|     if (aFlags !== DownloadsCommon.SUPPRESS_NONE) {
 | |
|       for (let download of this._downloads) {
 | |
|         download.attention = DownloadsCommon.ATTENTION_NONE;
 | |
|       }
 | |
|       this.attention = DownloadsCommon.ATTENTION_NONE;
 | |
|     }
 | |
|   },
 | |
|   get attentionSuppressed() {
 | |
|     return this._attentionSuppressed;
 | |
|   },
 | |
|   _attentionSuppressed: DownloadsCommon.SUPPRESS_NONE,
 | |
| 
 | |
|   /**
 | |
|    * Set the indicator's attention to the most severe attention state among the
 | |
|    * unseen displayed downloads, or DownloadsCommon.ATTENTION_NONE if empty.
 | |
|    */
 | |
|   updateAttention() {
 | |
|     let currentAttention = DownloadsCommon.ATTENTION_NONE;
 | |
|     let currentPriority = 0;
 | |
|     for (let download of this._downloads) {
 | |
|       let { attention } = download;
 | |
|       let priority = this._attentionPriority.get(attention);
 | |
|       if (priority > currentPriority) {
 | |
|         currentPriority = priority;
 | |
|         currentAttention = attention;
 | |
|       }
 | |
|     }
 | |
|     this.attention = currentAttention;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Updates the specified view with the current aggregate values.
 | |
|    *
 | |
|    * @param aView
 | |
|    *        DownloadsIndicatorView object to be updated.
 | |
|    */
 | |
|   _updateView(aView) {
 | |
|     aView.hasDownloads = this._hasDownloads;
 | |
|     aView.percentComplete = this._percentComplete;
 | |
|     aView.attention =
 | |
|       this.attentionSuppressed !== DownloadsCommon.SUPPRESS_NONE
 | |
|         ? DownloadsCommon.ATTENTION_NONE
 | |
|         : this._attention;
 | |
|   },
 | |
| 
 | |
|   // Property updating based on current download status
 | |
| 
 | |
|   /**
 | |
|    * Number of download items that are available to be displayed.
 | |
|    */
 | |
|   _itemCount: 0,
 | |
| 
 | |
|   /**
 | |
|    * A generator function for the Download objects this summary is currently
 | |
|    * interested in. This generator is passed off to summarizeDownloads in order
 | |
|    * to generate statistics about the downloads we care about - in this case,
 | |
|    * it's all active downloads.
 | |
|    */
 | |
|   *_activeDownloads() {
 | |
|     let downloads = this._isPrivate
 | |
|       ? lazy.PrivateDownloadsData._downloads
 | |
|       : lazy.DownloadsData._downloads;
 | |
|     for (let download of downloads) {
 | |
|       if (!download.stopped || (download.canceled && download.hasPartialData)) {
 | |
|         yield download;
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Computes aggregate values based on the current state of downloads.
 | |
|    */
 | |
|   _refreshProperties() {
 | |
|     let summary = DownloadsCommon.summarizeDownloads(this._activeDownloads());
 | |
| 
 | |
|     // Determine if the indicator should be shown or get attention.
 | |
|     this._hasDownloads = this._itemCount > 0;
 | |
| 
 | |
|     // Always show a progress bar if there are downloads in progress.
 | |
|     if (summary.percentComplete >= 0) {
 | |
|       this._percentComplete = summary.percentComplete;
 | |
|     } else if (summary.numDownloading > 0) {
 | |
|       this._percentComplete = 0;
 | |
|     } else {
 | |
|       this._percentComplete = -1;
 | |
|     }
 | |
|   },
 | |
| };
 | |
| Object.setPrototypeOf(
 | |
|   DownloadsIndicatorDataCtor.prototype,
 | |
|   DownloadsViewPrototype
 | |
| );
 | |
| 
 | |
| ChromeUtils.defineLazyGetter(
 | |
|   lazy,
 | |
|   "PrivateDownloadsIndicatorData",
 | |
|   function () {
 | |
|     return new DownloadsIndicatorDataCtor(true);
 | |
|   }
 | |
| );
 | |
| 
 | |
| ChromeUtils.defineLazyGetter(lazy, "DownloadsIndicatorData", function () {
 | |
|   return new DownloadsIndicatorDataCtor(false);
 | |
| });
 | |
| 
 | |
| // DownloadsSummaryData
 | |
| 
 | |
| /**
 | |
|  * DownloadsSummaryData is a view for DownloadsData that produces a summary
 | |
|  * of all downloads after a certain exclusion point aNumToExclude. For example,
 | |
|  * if there were 5 downloads in progress, and a DownloadsSummaryData was
 | |
|  * constructed with aNumToExclude equal to 3, then that DownloadsSummaryData
 | |
|  * would produce a summary of the last 2 downloads.
 | |
|  *
 | |
|  * @param aIsPrivate
 | |
|  *        True if the browser window which owns the download button is a private
 | |
|  *        window.
 | |
|  * @param aNumToExclude
 | |
|  *        The number of items to exclude from the summary, starting from the
 | |
|  *        top of the list.
 | |
|  */
 | |
| function DownloadsSummaryData(aIsPrivate, aNumToExclude) {
 | |
|   this._numToExclude = aNumToExclude;
 | |
|   // Since we can have multiple instances of DownloadsSummaryData, we
 | |
|   // override these values from the prototype so that each instance can be
 | |
|   // completely separated from one another.
 | |
|   this._loading = false;
 | |
| 
 | |
|   this._downloads = [];
 | |
| 
 | |
|   // Floating point value indicating the last number of seconds estimated until
 | |
|   // the longest download will finish.  We need to store this value so that we
 | |
|   // don't continuously apply smoothing if the actual download state has not
 | |
|   // changed.  This is set to -1 if the previous value is unknown.
 | |
|   this._lastRawTimeLeft = -1;
 | |
| 
 | |
|   // Last number of seconds estimated until all in-progress downloads with a
 | |
|   // known size and speed will finish.  This value is stored to allow smoothing
 | |
|   // in case of small variations.  This is set to -1 if the previous value is
 | |
|   // unknown.
 | |
|   this._lastTimeLeft = -1;
 | |
| 
 | |
|   // The following properties are updated by _refreshProperties and are then
 | |
|   // propagated to the views.
 | |
|   this._showingProgress = false;
 | |
|   this._details = "";
 | |
|   this._description = "";
 | |
|   this._numActive = 0;
 | |
|   this._percentComplete = -1;
 | |
| 
 | |
|   this._oldDownloadStates = new WeakMap();
 | |
|   this._isPrivate = aIsPrivate;
 | |
|   this._views = [];
 | |
| }
 | |
| 
 | |
| DownloadsSummaryData.prototype = {
 | |
|   /**
 | |
|    * Removes an object previously added using addView.
 | |
|    *
 | |
|    * @param aView
 | |
|    *        DownloadsSummary view to be removed.
 | |
|    */
 | |
|   removeView(aView) {
 | |
|     DownloadsViewPrototype.removeView.call(this, aView);
 | |
| 
 | |
|     if (!this._views.length) {
 | |
|       // Clear out our collection of Download objects. If we ever have
 | |
|       // another view registered with us, this will get re-populated.
 | |
|       this._downloads = [];
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   onDownloadAdded(download) {
 | |
|     DownloadsViewPrototype.onDownloadAdded.call(this, download);
 | |
|     this._downloads.unshift(download);
 | |
|     this._updateViews();
 | |
|   },
 | |
| 
 | |
|   onDownloadStateChanged() {
 | |
|     // Since the state of a download changed, reset the estimated time left.
 | |
|     this._lastRawTimeLeft = -1;
 | |
|     this._lastTimeLeft = -1;
 | |
|   },
 | |
| 
 | |
|   onDownloadChanged(download) {
 | |
|     DownloadsViewPrototype.onDownloadChanged.call(this, download);
 | |
|     this._updateViews();
 | |
|   },
 | |
| 
 | |
|   onDownloadRemoved(download) {
 | |
|     DownloadsViewPrototype.onDownloadRemoved.call(this, download);
 | |
|     let itemIndex = this._downloads.indexOf(download);
 | |
|     this._downloads.splice(itemIndex, 1);
 | |
|     this._updateViews();
 | |
|   },
 | |
| 
 | |
|   // Propagation of properties to our views
 | |
| 
 | |
|   /**
 | |
|    * Updates the specified view with the current aggregate values.
 | |
|    *
 | |
|    * @param aView
 | |
|    *        DownloadsIndicatorView object to be updated.
 | |
|    */
 | |
|   _updateView(aView) {
 | |
|     aView.showingProgress = this._showingProgress;
 | |
|     aView.percentComplete = this._percentComplete;
 | |
|     aView.description = this._description;
 | |
|     aView.details = this._details;
 | |
|   },
 | |
| 
 | |
|   // Property updating based on current download status
 | |
| 
 | |
|   /**
 | |
|    * A generator function for the Download objects this summary is currently
 | |
|    * interested in. This generator is passed off to summarizeDownloads in order
 | |
|    * to generate statistics about the downloads we care about - in this case,
 | |
|    * it's the downloads in this._downloads after the first few to exclude,
 | |
|    * which was set when constructing this DownloadsSummaryData instance.
 | |
|    */
 | |
|   *_downloadsForSummary() {
 | |
|     if (this._downloads.length) {
 | |
|       for (let i = this._numToExclude; i < this._downloads.length; ++i) {
 | |
|         yield this._downloads[i];
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Computes aggregate values based on the current state of downloads.
 | |
|    */
 | |
|   _refreshProperties() {
 | |
|     // Pre-load summary with default values.
 | |
|     let summary = DownloadsCommon.summarizeDownloads(
 | |
|       this._downloadsForSummary()
 | |
|     );
 | |
| 
 | |
|     // Run sync to update view right away and get correct description.
 | |
|     // See refreshView for more details.
 | |
|     this._description = kDownloadsFluentStrings.formatValueSync(
 | |
|       "downloads-more-downloading",
 | |
|       {
 | |
|         count: summary.numDownloading,
 | |
|       }
 | |
|     );
 | |
|     this._percentComplete = summary.percentComplete;
 | |
| 
 | |
|     // Only show the downloading items.
 | |
|     this._showingProgress = summary.numDownloading > 0;
 | |
| 
 | |
|     // Display the estimated time left, if present.
 | |
|     if (summary.rawTimeLeft == -1) {
 | |
|       // There are no downloads with a known time left.
 | |
|       this._lastRawTimeLeft = -1;
 | |
|       this._lastTimeLeft = -1;
 | |
|       this._details = "";
 | |
|     } else {
 | |
|       // Compute the new time left only if state actually changed.
 | |
|       if (this._lastRawTimeLeft != summary.rawTimeLeft) {
 | |
|         this._lastRawTimeLeft = summary.rawTimeLeft;
 | |
|         this._lastTimeLeft = DownloadsCommon.smoothSeconds(
 | |
|           summary.rawTimeLeft,
 | |
|           this._lastTimeLeft
 | |
|         );
 | |
|       }
 | |
|       [this._details] = lazy.DownloadUtils.getDownloadStatusNoRate(
 | |
|         summary.totalTransferred,
 | |
|         summary.totalSize,
 | |
|         summary.slowestSpeed,
 | |
|         this._lastTimeLeft
 | |
|       );
 | |
|     }
 | |
|   },
 | |
| };
 | |
| Object.setPrototypeOf(DownloadsSummaryData.prototype, DownloadsViewPrototype);
 | 
