1640 lines
52 KiB
JavaScript
1640 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"]),
|
|
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);
|
|
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
|
|
) {
|
|
Glean.downloads.userActionOnBlockedDownload[
|
|
download.error.reputationCheckVerdict
|
|
].accumulateSingleSample(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() {
|
|
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);
|