1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
6 * This module is imported by code that uses the "download.xml" binding, and
7 * provides prototypes for objects that handle input and display information.
10 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
14 ChromeUtils.defineESModuleGetters(lazy, {
15 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
16 DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
17 Downloads: "resource://gre/modules/Downloads.sys.mjs",
18 DownloadsCommon: "resource:///modules/DownloadsCommon.sys.mjs",
19 FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
20 UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
23 XPCOMUtils.defineLazyServiceGetter(
26 "@mozilla.org/uriloader/handler-service;1",
30 XPCOMUtils.defineLazyServiceGetter(
33 "@mozilla.org/reputationservice/application-reputation-service;1",
34 Ci.nsIApplicationReputationService
37 import { Integration } from "resource://gre/modules/Integration.sys.mjs";
39 Integration.downloads.defineESModuleGetter(
41 "DownloadIntegration",
42 "resource://gre/modules/DownloadIntegration.sys.mjs"
45 const HTML_NS = "http://www.w3.org/1999/xhtml";
47 var gDownloadElementButtons = {
49 commandName: "downloadsCmd_cancel",
50 l10nId: "downloads-cmd-cancel",
51 descriptionL10nId: "downloads-cancel-download",
52 panelL10nId: "downloads-cmd-cancel-panel",
53 iconClass: "downloadIconCancel",
56 commandName: "downloadsCmd_retry",
57 l10nId: "downloads-cmd-retry",
58 descriptionL10nId: "downloads-retry-download",
59 panelL10nId: "downloads-cmd-retry-panel",
60 iconClass: "downloadIconRetry",
63 commandName: "downloadsCmd_show",
64 l10nId: "downloads-cmd-show-button-2",
65 descriptionL10nId: "downloads-cmd-show-description-2",
66 panelL10nId: "downloads-cmd-show-panel-2",
67 iconClass: "downloadIconShow",
69 subviewOpenOrRemoveFile: {
70 commandName: "downloadsCmd_showBlockedInfo",
71 l10nId: "downloads-cmd-choose-open",
72 descriptionL10nId: "downloads-show-more-information",
73 panelL10nId: "downloads-cmd-choose-open-panel",
74 iconClass: "downloadIconSubviewArrow",
76 askOpenOrRemoveFile: {
77 commandName: "downloadsCmd_chooseOpen",
78 l10nId: "downloads-cmd-choose-open",
79 panelL10nId: "downloads-cmd-choose-open-panel",
80 iconClass: "downloadIconShow",
82 askRemoveFileOrAllow: {
83 commandName: "downloadsCmd_chooseUnblock",
84 l10nId: "downloads-cmd-choose-unblock",
85 panelL10nId: "downloads-cmd-choose-unblock-panel",
86 iconClass: "downloadIconShow",
89 commandName: "downloadsCmd_confirmBlock",
90 l10nId: "downloads-cmd-remove-file",
91 panelL10nId: "downloads-cmd-remove-file-panel",
92 iconClass: "downloadIconCancel",
97 * Associates each document with a pre-built DOM fragment representing the
98 * download list item. This is then cloned to create each individual list item.
99 * This is stored on the document to prevent leaks that would occur if a single
100 * instance created by one document's DOMParser was stored globally.
102 var gDownloadListItemFragments = new WeakMap();
104 export var DownloadsViewUI = {
106 * Returns true if the given string is the name of a command that can be
107 * handled by the Downloads user interface, including standard commands.
109 isCommandName(name) {
110 return name.startsWith("cmd_") || name.startsWith("downloadsCmd_");
114 * Get source url of the download without'http' or'https' prefix.
116 getStrippedUrl(download) {
117 return lazy.UrlbarUtils.stripPrefixAndTrim(download?.source?.url, {
124 * Returns the user-facing label for the given Download object. This is
125 * normally the leaf name of the download target file. In case this is a very
126 * old history download for which the target file is unknown, the download
127 * source URI is displayed.
129 getDisplayName(download) {
131 download.error?.reputationCheckVerdict ==
132 lazy.Downloads.Error.BLOCK_VERDICT_DOWNLOAD_SPAM
135 id: "downloads-blocked-from-url",
136 args: { url: DownloadsViewUI.getStrippedUrl(download) },
140 return download.target.path
141 ? PathUtils.filename(download.target.path)
142 : download.source.url;
146 * Given a Download object, returns a string representing its file size with
147 * an appropriate measurement unit, for example "1.5 MB", or an empty string
148 * if the size is unknown.
150 getSizeWithUnits(download) {
151 if (download.target.size === undefined) {
155 let [size, unit] = lazy.DownloadUtils.convertByteUnits(
158 return lazy.DownloadsCommon.strings.sizeWithUnits(size, unit);
162 * Given a context menu and a download element on which it is invoked,
163 * update items in the context menu to reflect available options for
164 * that download element.
166 updateContextMenuForElement(contextMenu, element) {
167 // Get the state and ensure only the appropriate items are displayed.
168 let state = parseInt(element.getAttribute("state"), 10);
170 const document = contextMenu.ownerDocument;
174 DOWNLOAD_DOWNLOADING,
179 DOWNLOAD_BLOCKED_PARENTAL,
181 DOWNLOAD_BLOCKED_POLICY,
182 } = lazy.DownloadsCommon;
184 contextMenu.querySelector(".downloadPauseMenuItem").hidden =
185 state != DOWNLOAD_DOWNLOADING;
187 contextMenu.querySelector(".downloadResumeMenuItem").hidden =
188 state != DOWNLOAD_PAUSED;
190 // Only show "unblock" for blocked (dirty) items that have not been
191 // confirmed and have temporary data:
192 contextMenu.querySelector(".downloadUnblockMenuItem").hidden =
193 state != DOWNLOAD_DIRTY || !element.classList.contains("temporary-block");
195 // Can only remove finished/failed/canceled/blocked downloads.
196 contextMenu.querySelector(".downloadRemoveFromHistoryMenuItem").hidden = ![
200 DOWNLOAD_BLOCKED_PARENTAL,
202 DOWNLOAD_BLOCKED_POLICY,
205 // Can reveal downloads with data on the file system using the relevant OS
206 // tool (Explorer, Finder, appropriate Linux file system viewer):
207 contextMenu.querySelector(".downloadShowMenuItem").hidden =
210 DOWNLOAD_DOWNLOADING,
214 (state == DOWNLOAD_FINISHED && !element.hasAttribute("exists"));
216 // Show the separator if we're showing either unblock or reveal menu items.
217 contextMenu.querySelector(".downloadCommandsSeparator").hidden =
218 contextMenu.querySelector(".downloadUnblockMenuItem").hidden &&
219 contextMenu.querySelector(".downloadShowMenuItem").hidden;
221 let download = element._shell.download;
222 let mimeInfo = lazy.DownloadsCommon.getMimeInfo(download);
223 let { preferredAction, useSystemDefault, defaultDescription } = mimeInfo
227 // Hide the "Delete" item if there's no file data to delete.
228 contextMenu.querySelector(".downloadDeleteFileMenuItem").hidden =
230 !(download.target?.exists || download.target?.partFileExists);
232 // Hide the "Go To Download Page" item if there's no referrer. Ideally the
233 // Downloads API will require a referrer (see bug 1723712) to create a
234 // download, but this fallback will ensure any failures aren't user facing.
235 contextMenu.querySelector(".downloadOpenReferrerMenuItem").hidden =
236 !download.source.referrerInfo?.originalReferrer;
238 // Hide the "use system viewer" and "always use system viewer" items
239 // if the feature is disabled or this download doesn't support it:
240 let useSystemViewerItem = contextMenu.querySelector(
241 ".downloadUseSystemDefaultMenuItem"
243 let alwaysUseSystemViewerItem = contextMenu.querySelector(
244 ".downloadAlwaysUseSystemDefaultMenuItem"
246 let canViewInternally = element.hasAttribute("viewable-internally");
247 useSystemViewerItem.hidden =
248 !lazy.DownloadsCommon.openInSystemViewerItemEnabled ||
249 !canViewInternally ||
250 !download.target?.exists;
252 alwaysUseSystemViewerItem.hidden =
253 !lazy.DownloadsCommon.alwaysOpenInSystemViewerItemEnabled ||
256 // Set menuitem labels to display the system viewer's name. Stop the l10n
257 // mutation observer temporarily since we're going to synchronously
258 // translate the elements to avoid translation delay. See bug 1737951 & bug
259 // 1746748. This can be simplified when they're resolved.
261 document.l10n.pauseObserving();
262 // Handler descriptions longer than 40 characters will be skipped to avoid
263 // unreasonably stretching the context menu.
264 if (defaultDescription && defaultDescription.length < 40) {
265 document.l10n.setAttributes(
267 "downloads-cmd-use-system-default-named",
268 { handler: defaultDescription }
270 document.l10n.setAttributes(
271 alwaysUseSystemViewerItem,
272 "downloads-cmd-always-use-system-default-named",
273 { handler: defaultDescription }
276 // In the unlikely event that defaultDescription is somehow missing/invalid,
277 // fall back to the static "Open In System Viewer" label.
278 document.l10n.setAttributes(
280 "downloads-cmd-use-system-default"
282 document.l10n.setAttributes(
283 alwaysUseSystemViewerItem,
284 "downloads-cmd-always-use-system-default"
288 document.l10n.resumeObserving();
290 document.l10n.translateElements([
292 alwaysUseSystemViewerItem,
295 // If non default mime-type or cannot be opened internally, display
296 // "always open similar files" item instead so that users can add a new
297 // mimetype to about:preferences table and set to open with system default.
298 let alwaysOpenSimilarFilesItem = contextMenu.querySelector(
299 ".downloadAlwaysOpenSimilarFilesMenuItem"
303 * In HelperAppDlg.sys.mjs, we determine whether or not an "always open..." checkbox
304 * should appear in the unknownContentType window. Here, we use similar checks to
305 * determine if we should show the "always open similar files" context menu item.
307 * Note that we also read the content type using mimeInfo to detect better and available
308 * mime types, given a file extension. Some sites default to "application/octet-stream",
309 * further limiting what file types can be added to about:preferences, even for file types
310 * that are in fact capable of being handled with a default application.
312 * There are also cases where download.contentType is undefined (ex. when opening
313 * the context menu on a previously downloaded item via download history).
314 * Using mimeInfo ensures that content type exists and prevents intermittence.
317 let filename = PathUtils.filename(download.target.path);
319 let isExemptExecutableExtension =
320 Services.policies.isExemptExecutableExtension(
321 download.source.originalUrl || download.source.url,
322 filename?.split(".").at(-1)
325 let shouldNotRememberChoice =
327 mimeInfo.type === "application/octet-stream" ||
328 mimeInfo.type === "application/x-msdownload" ||
329 mimeInfo.type === "application/x-msdos-program" ||
330 (lazy.gReputationService.isExecutable(filename) &&
331 !isExemptExecutableExtension) ||
332 (mimeInfo.type === "text/plain" &&
333 lazy.gReputationService.isBinary(download.target.path));
335 alwaysOpenSimilarFilesItem.hidden =
337 state !== DOWNLOAD_FINISHED ||
338 shouldNotRememberChoice;
340 // Update checkbox for "always open..." options.
341 if (preferredAction === useSystemDefault) {
342 alwaysUseSystemViewerItem.setAttribute("checked", "true");
343 alwaysOpenSimilarFilesItem.setAttribute("checked", "true");
345 alwaysUseSystemViewerItem.removeAttribute("checked");
346 alwaysOpenSimilarFilesItem.removeAttribute("checked");
351 XPCOMUtils.defineLazyPreferenceGetter(
353 "clearHistoryOnDelete",
354 "browser.download.clearHistoryOnDelete",
358 DownloadsViewUI.BaseView = class {
359 canClearDownloads(nodeContainer) {
360 // Downloads can be cleared if there's at least one removable download in
361 // the list (either a history download or a completed session download).
362 // Because history downloads are always removable and are listed after the
363 // session downloads, check from bottom to top.
364 for (let elt = nodeContainer.lastChild; elt; elt = elt.previousSibling) {
365 // Stopped, paused, and failed downloads with partial data are removed.
366 let download = elt._shell.download;
367 if (download.stopped && !(download.canceled && download.hasPartialData)) {
376 * A download element shell is responsible for handling the commands and the
377 * displayed data for a single element that uses the "download.xml" binding.
379 * The information to display is obtained through the associated Download object
380 * from the JavaScript API for downloads, and commands are executed using a
381 * combination of Download methods and DownloadsCommon.sys.mjs helper functions.
383 * Specialized versions of this shell must be defined, and they are required to
384 * implement the "download" property or getter. Currently these objects are the
385 * HistoryDownloadElementShell and the DownloadsViewItem for the panel. The
386 * history view may use a HistoryDownload object in place of a Download object.
388 DownloadsViewUI.DownloadElementShell = function () {};
390 DownloadsViewUI.DownloadElementShell.prototype = {
392 * The richlistitem for the download, initialized by the derived object.
397 * Manages the "active" state of the shell. By default all the shells are
398 * inactive, thus their UI is not updated. They must be activated when
399 * entering the visible area.
409 return !!this._active;
413 let document = this.element.ownerDocument;
414 let downloadListItemFragment = gDownloadListItemFragments.get(document);
415 // When changing the markup within the fragment, please ensure that
416 // the functions within DownloadsView still operate correctly.
417 if (!downloadListItemFragment) {
418 let MozXULElement = document.defaultView.MozXULElement;
419 downloadListItemFragment = MozXULElement.parseXULToFragment(`
420 <hbox class="downloadMainArea" flex="1" align="center">
421 <image class="downloadTypeIcon"/>
422 <vbox class="downloadContainer" flex="1" pack="center">
423 <description class="downloadTarget" crop="center"/>
424 <description class="downloadDetails downloadDetailsNormal"
426 <description class="downloadDetails downloadDetailsHover"
428 <description class="downloadDetails downloadDetailsButtonHover"
431 <image class="downloadBlockedBadge" />
433 <button class="downloadButton"/>
435 gDownloadListItemFragments.set(document, downloadListItemFragment);
437 this.element.setAttribute("active", true);
438 this.element.setAttribute("orient", "horizontal");
439 this.element.addEventListener("click", ev => {
440 ev.target.ownerGlobal.DownloadsView.onDownloadClick(ev);
442 this.element.appendChild(
443 document.importNode(downloadListItemFragment, true)
445 let downloadButton = this.element.querySelector(".downloadButton");
446 downloadButton.addEventListener("command", function (event) {
447 event.target.ownerGlobal.DownloadsView.onDownloadButton(event);
449 for (let [propertyName, selector] of [
450 ["_downloadTypeIcon", ".downloadTypeIcon"],
451 ["_downloadTarget", ".downloadTarget"],
452 ["_downloadDetailsNormal", ".downloadDetailsNormal"],
453 ["_downloadDetailsHover", ".downloadDetailsHover"],
454 ["_downloadDetailsButtonHover", ".downloadDetailsButtonHover"],
455 ["_downloadButton", ".downloadButton"],
457 this[propertyName] = this.element.querySelector(selector);
460 // HTML elements can be created directly without using parseXULToFragment.
461 let progress = (this._downloadProgress = document.createElementNS(
465 progress.className = "downloadProgress";
466 progress.setAttribute("max", "100");
467 this._downloadTarget.insertAdjacentElement("afterend", progress);
471 * URI string for the file type icon displayed in the download element.
474 if (!this.download.target.path) {
475 // Old history downloads may not have a target path.
476 return "moz-icon://.unknown?size=32";
479 // When a download that was previously in progress finishes successfully, it
480 // means that the target file now exists and we can extract its specific
481 // icon, for example from a Windows executable. To ensure that the icon is
482 // reloaded, however, we must change the URI used by the XUL image element,
483 // for example by adding a query parameter. This only works if we add one of
484 // the parameters explicitly supported by the nsIMozIconURI interface.
487 this.download.target.path +
489 (this.download.succeeded ? "&state=normal" : "")
493 get browserWindow() {
494 return lazy.BrowserWindowTracker.getTopWindow();
498 * Updates the display name and icon.
501 * This is usually the full file name of the download without the path.
503 * URL of the icon to load, generally from the "image" property.
505 showDisplayNameAndIcon(displayName, icon) {
506 if (displayName.l10n) {
507 let document = this.element.ownerDocument;
508 document.l10n.setAttributes(
509 this._downloadTarget,
511 displayName.l10n.args
514 this._downloadTarget.setAttribute("value", displayName);
515 this._downloadTarget.setAttribute("tooltiptext", displayName);
517 this._downloadTypeIcon.setAttribute("src", icon);
521 * Updates the displayed progress bar.
524 * Either "normal" or "undetermined".
526 * Percentage of the progress bar to display, from 0 to 100.
528 * True to display the progress bar style for paused downloads.
530 showProgress(mode, value, paused) {
531 if (mode == "undetermined") {
532 this._downloadProgress.removeAttribute("value");
534 this._downloadProgress.setAttribute("value", value);
536 this._downloadProgress.toggleAttribute("paused", !!paused);
540 * Updates the full status line.
543 * Status line of the Downloads Panel or the Downloads View.
545 * Label to show in the Downloads Panel when the mouse pointer is over
546 * the main area of the item. If not specified, this will be the same
547 * as the status line. This is ignored in the Downloads View. Type is
548 * either l10n object or string literal.
550 showStatus(status, hoverStatus = status) {
551 let document = this.element.ownerDocument;
553 document.l10n.setAttributes(
554 this._downloadDetailsNormal,
559 this._downloadDetailsNormal.removeAttribute("data-l10n-id");
560 this._downloadDetailsNormal.setAttribute("value", status);
561 this._downloadDetailsNormal.setAttribute("tooltiptext", status);
563 if (hoverStatus?.l10n) {
564 document.l10n.setAttributes(
565 this._downloadDetailsHover,
567 hoverStatus.l10n.args
570 this._downloadDetailsHover.removeAttribute("data-l10n-id");
571 this._downloadDetailsHover.setAttribute("value", hoverStatus);
576 * Updates the status line combining the given state label with other labels.
579 * Label representing the state of the download, for example "Failed".
580 * In the Downloads Panel, this is the only text displayed when the
581 * the mouse pointer is not over the main area of the item. In the
582 * Downloads View, this label is combined with the host and date, for
583 * example "Failed - example.com - 1:45 PM".
585 * Label to show in the Downloads Panel when the mouse pointer is over
586 * the main area of the item. If not specified, this will be the
587 * state label combined with the host and date. This is ignored in the
588 * Downloads View. Type is either l10n object or string literal.
590 showStatusWithDetails(stateLabel, hoverStatus) {
591 if (stateLabel.l10n) {
592 this.showStatus(stateLabel, hoverStatus);
595 let [displayHost] = lazy.DownloadUtils.getURIHost(this.download.source.url);
596 let [displayDate] = lazy.DownloadUtils.getReadableDates(
597 new Date(this.download.endTime)
600 let firstPart = lazy.DownloadsCommon.strings.statusSeparator(
604 let fullStatus = lazy.DownloadsCommon.strings.statusSeparator(
610 this.showStatus(fullStatus);
612 this.showStatus(stateLabel, hoverStatus || fullStatus);
617 * Updates the main action button and makes it visible.
620 * One of the presets defined in gDownloadElementButtons.
623 let { commandName, l10nId, descriptionL10nId, panelL10nId, iconClass } =
624 gDownloadElementButtons[type];
626 this.buttonCommandName = commandName;
627 let stringId = this.isPanel ? panelL10nId : l10nId;
628 let document = this.element.ownerDocument;
629 document.l10n.setAttributes(this._downloadButton, stringId);
630 if (this.isPanel && descriptionL10nId) {
631 document.l10n.setAttributes(
632 this._downloadDetailsButtonHover,
636 this._downloadButton.setAttribute("class", "downloadButton " + iconClass);
637 this._downloadButton.removeAttribute("hidden");
641 this._downloadButton.hidden = true;
644 lastEstimatedSecondsLeft: Infinity,
647 * This is called when a major state change occurs in the download, but is not
648 * called for every progress update in order to improve performance.
651 this.showDisplayNameAndIcon(
652 DownloadsViewUI.getDisplayName(this.download),
655 this.element.setAttribute(
657 lazy.DownloadsCommon.stateOfDownload(this.download)
660 if (!this.download.stopped) {
661 // When the download becomes in progress, we make all the major changes to
662 // the user interface here. The _updateStateInner function takes care of
663 // displaying the right button type for all other state changes.
664 this.showButton("cancel");
666 // If there was a verdict set but the download is running we can assume
667 // that the verdict has been overruled and can be removed.
668 this.element.removeAttribute("verdict");
671 // Since state changed, reset the time left estimation.
672 this.lastEstimatedSecondsLeft = Infinity;
674 this._updateStateInner();
678 * This is called for all changes in the download, including progress updates.
679 * For major state changes, _updateState is called first, but several elements
680 * are still updated here. When the download is in progress, this function
681 * takes a faster path with less element updates to improve performance.
683 _updateStateInner() {
684 let progressPaused = false;
686 this.element.classList.toggle("openWhenFinished", !this.download.stopped);
688 if (!this.download.stopped) {
689 // The download is in progress, so we don't change the button state
690 // because the _updateState function already did it. We still need to
691 // update all elements that may change during the download.
692 let totalBytes = this.download.hasProgress
693 ? this.download.totalBytes
695 let [status, newEstimatedSecondsLeft] =
696 lazy.DownloadUtils.getDownloadStatus(
697 this.download.currentBytes,
700 this.lastEstimatedSecondsLeft
702 this.lastEstimatedSecondsLeft = newEstimatedSecondsLeft;
704 if (this.download.launchWhenSucceeded) {
705 status = lazy.DownloadUtils.getFormattedTimeStatus(
706 newEstimatedSecondsLeft
710 l10n: { id: "downloading-file-click-to-open" },
712 this.showStatus(status, hoverStatus);
716 // The download is not in progress, so we update the user interface based
717 // on other properties. The order in which we check the properties of the
718 // Download object is the same used by stateOfDownload.
719 if (this.download.deleted) {
720 this.showDeletedOrMissing();
721 } else if (this.download.succeeded) {
722 lazy.DownloadsCommon.log(
723 "_updateStateInner, target exists? ",
724 this.download.target.path,
725 this.download.target.exists
727 if (this.download.target.exists) {
728 // This is a completed download, and the target file still exists.
729 this.element.setAttribute("exists", "true");
731 this.element.toggleAttribute(
732 "viewable-internally",
733 lazy.DownloadIntegration.shouldViewDownloadInternally(
734 lazy.DownloadsCommon.getMimeInfo(this.download)?.type
738 let sizeWithUnits = DownloadsViewUI.getSizeWithUnits(this.download);
740 // In the Downloads Panel, we show the file size after the state
741 // label, for example "Completed - 1.5 MB". When the pointer is over
742 // the main area of the item, this label is replaced with a
743 // description of the default action, which opens the file.
744 let status = lazy.DownloadsCommon.strings.stateCompleted;
746 status = lazy.DownloadsCommon.strings.statusSeparator(
751 this.showStatus(status, { l10n: { id: "downloads-open-file" } });
753 // In the Downloads View, we show the file size in place of the
754 // state label, for example "1.5 MB - example.com - 1:45 PM".
755 this.showStatusWithDetails(
756 sizeWithUnits || lazy.DownloadsCommon.strings.sizeUnknown
759 this.showButton("show");
761 // This is a completed download, but the target file does not exist
762 // anymore, so the main action of opening the file is unavailable.
763 this.showDeletedOrMissing();
765 } else if (this.download.error) {
766 if (this.download.error.becauseBlockedByParentalControls) {
767 // This download was blocked permanently by parental controls.
768 this.showStatusWithDetails(
769 lazy.DownloadsCommon.strings.stateBlockedParentalControls
772 } else if (this.download.error.becauseBlockedByReputationCheck) {
773 verdict = this.download.error.reputationCheckVerdict;
775 if (!this.download.hasBlockedData) {
776 // This download was blocked permanently by reputation check.
778 } else if (this.isPanel) {
779 // This download was blocked temporarily by reputation check. In the
780 // Downloads Panel, a subview can be used to remove the file or open
781 // the download anyways.
782 this.showButton("subviewOpenOrRemoveFile");
783 hover = { l10n: { id: "downloads-show-more-information" } };
785 // This download was blocked temporarily by reputation check. In the
786 // Downloads View, the interface depends on the threat severity.
788 case lazy.Downloads.Error.BLOCK_VERDICT_UNCOMMON:
789 case lazy.Downloads.Error.BLOCK_VERDICT_INSECURE:
790 case lazy.Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
791 // Keep the option the user chose on the save dialogue
792 if (this.download.launchWhenSucceeded) {
793 this.showButton("askOpenOrRemoveFile");
795 this.showButton("askRemoveFileOrAllow");
798 case lazy.Downloads.Error.BLOCK_VERDICT_DOWNLOAD_SPAM:
799 this.showButton("askRemoveFileOrAllow");
802 // Assume Downloads.Error.BLOCK_VERDICT_MALWARE
803 this.showButton("removeFile");
807 this.showStatusWithDetails(this.rawBlockedTitleAndDetails[0], hover);
809 // This download failed without being blocked, and can be restarted.
810 this.showStatusWithDetails(lazy.DownloadsCommon.strings.stateFailed);
811 this.showButton("retry");
813 } else if (this.download.canceled) {
814 if (this.download.hasPartialData) {
815 // This download was paused. The main action button will cancel the
816 // download, and in both the Downloads Panel and the Downlods View the
817 // status includes the size, for example "Paused - 1.1 MB".
818 let totalBytes = this.download.hasProgress
819 ? this.download.totalBytes
821 let transfer = lazy.DownloadUtils.getTransferTotal(
822 this.download.currentBytes,
826 lazy.DownloadsCommon.strings.statusSeparatorBeforeNumber(
827 lazy.DownloadsCommon.strings.statePaused,
831 this.showButton("cancel");
832 progressPaused = true;
834 // This download was canceled.
835 this.showStatusWithDetails(
836 lazy.DownloadsCommon.strings.stateCanceled
838 this.showButton("retry");
841 // This download was added to the global list before it started. While
842 // we still support this case, at the moment it can only be triggered by
843 // internally developed add-ons and regression tests, and should not
844 // happen unless there is a bug. This means the stateStarting string can
845 // probably be removed when converting the localization to Fluent.
846 this.showStatus(lazy.DownloadsCommon.strings.stateStarting);
847 this.showButton("cancel");
850 // These attributes are only set in this slower code path, because they
851 // are irrelevant for downloads that are in progress.
853 this.element.setAttribute("verdict", verdict);
855 this.element.removeAttribute("verdict");
858 this.element.classList.toggle(
860 !!this.download.hasBlockedData
864 // These attributes are set in all code paths, because they are relevant for
865 // downloads that are in progress and for other states.
866 if (this.download.hasProgress) {
867 this.showProgress("normal", this.download.progress, progressPaused);
869 this.showProgress("undetermined", 100, progressPaused);
874 * Returns [title, [details1, details2]] for blocked downloads.
875 * The title or details could be raw strings or l10n objects.
877 get rawBlockedTitleAndDetails() {
878 let s = lazy.DownloadsCommon.strings;
880 !this.download.error ||
881 !this.download.error.becauseBlockedByReputationCheck
885 switch (this.download.error.reputationCheckVerdict) {
886 case lazy.Downloads.Error.BLOCK_VERDICT_UNCOMMON:
887 return [s.blockedUncommon2, [s.unblockTypeUncommon2, s.unblockTip2]];
888 case lazy.Downloads.Error.BLOCK_VERDICT_INSECURE:
890 s.blockedPotentiallyInsecure,
891 [s.unblockInsecure2, s.unblockTip2],
893 case lazy.Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
895 s.blockedPotentiallyUnwanted,
896 [s.unblockTypePotentiallyUnwanted2, s.unblockTip2],
898 case lazy.Downloads.Error.BLOCK_VERDICT_MALWARE:
899 return [s.blockedMalware, [s.unblockTypeMalware, s.unblockTip2]];
901 case lazy.Downloads.Error.BLOCK_VERDICT_DOWNLOAD_SPAM:
903 id: "downloads-files-not-downloaded",
905 num: this.download.blockedDownloadsCount,
909 id: "downloads-blocked-download-detailed-info",
910 args: { url: DownloadsViewUI.getStrippedUrl(this.download) },
912 return [{ l10n: title }, [{ l10n: details }, null]];
915 "Unexpected reputationCheckVerdict: " +
916 this.download.error.reputationCheckVerdict
920 showDeletedOrMissing() {
921 this.element.removeAttribute("exists");
923 lazy.DownloadsCommon.strings[
924 this.download.deleted ? "fileDeleted" : "fileMovedOrMissing"
926 this.showStatusWithDetails(label, label);
931 * Shows the appropriate unblock dialog based on the verdict, and executes the
932 * action selected by the user in the dialog, which may involve unblocking,
933 * opening or removing the file.
936 * The window to which the dialog should be anchored.
938 * Can be "unblock", "chooseUnblock", or "chooseOpen".
940 confirmUnblock(window, dialogType) {
941 lazy.DownloadsCommon.confirmUnblockDownload({
942 verdict: this.download.error.reputationCheckVerdict,
947 if (action == "open") {
948 return this.unblockAndOpenDownload();
949 } else if (action == "unblock") {
950 return this.download.unblock();
951 } else if (action == "confirmBlock") {
952 return this.download.confirmBlock();
954 return Promise.resolve();
956 .catch(console.error);
960 * Unblocks the downloaded file and opens it.
962 * @return A promise that's resolved after the file has been opened.
964 unblockAndOpenDownload() {
965 return this.download.unblock().then(() => this.downloadsCmd_open());
969 return this.download.unblock();
972 * Returns the name of the default command to use for the current state of the
973 * download, when there is a double click or another default interaction. If
974 * there is no default command for the current state, returns an empty string.
975 * The commands are implemented as functions on this object or derived ones.
977 get currentDefaultCommandName() {
978 switch (lazy.DownloadsCommon.stateOfDownload(this.download)) {
979 case lazy.DownloadsCommon.DOWNLOAD_NOTSTARTED:
980 return "downloadsCmd_cancel";
981 case lazy.DownloadsCommon.DOWNLOAD_FAILED:
982 case lazy.DownloadsCommon.DOWNLOAD_CANCELED:
983 return "downloadsCmd_retry";
984 case lazy.DownloadsCommon.DOWNLOAD_PAUSED:
985 return "downloadsCmd_pauseResume";
986 case lazy.DownloadsCommon.DOWNLOAD_FINISHED:
987 return "downloadsCmd_open";
988 case lazy.DownloadsCommon.DOWNLOAD_BLOCKED_PARENTAL:
989 return "downloadsCmd_openReferrer";
990 case lazy.DownloadsCommon.DOWNLOAD_DIRTY:
991 return "downloadsCmd_showBlockedInfo";
997 * Returns true if the specified command can be invoked on the current item.
998 * The commands are implemented as functions on this object or derived ones.
1001 * Name of the command to check, for example "downloadsCmd_retry".
1003 isCommandEnabled(aCommand) {
1005 case "downloadsCmd_retry":
1006 return this.download.canceled || !!this.download.error;
1007 case "downloadsCmd_pauseResume":
1008 return this.download.hasPartialData && !this.download.error;
1009 case "downloadsCmd_openReferrer":
1011 !!this.download.source.referrerInfo &&
1012 !!this.download.source.referrerInfo.originalReferrer
1014 case "downloadsCmd_confirmBlock":
1015 case "downloadsCmd_chooseUnblock":
1016 case "downloadsCmd_chooseOpen":
1017 case "downloadsCmd_unblock":
1018 case "downloadsCmd_unblockAndSave":
1019 case "downloadsCmd_unblockAndOpen":
1020 return this.download.hasBlockedData;
1021 case "downloadsCmd_cancel":
1022 return this.download.hasPartialData || !this.download.stopped;
1023 case "downloadsCmd_open":
1024 case "downloadsCmd_open:current":
1025 case "downloadsCmd_open:tab":
1026 case "downloadsCmd_open:tabshifted":
1027 case "downloadsCmd_open:window":
1028 case "downloadsCmd_alwaysOpenSimilarFiles":
1029 // This property is false if the download did not succeed.
1030 return this.download.target.exists;
1032 case "downloadsCmd_show":
1033 case "downloadsCmd_deleteFile":
1034 let { target } = this.download;
1036 !this.download.deleted && (target.exists || target.partFileExists)
1039 case "downloadsCmd_delete":
1041 // We don't want in-progress downloads to be removed accidentally.
1042 return this.download.stopped;
1043 case "downloadsCmd_openInSystemViewer":
1044 case "downloadsCmd_alwaysOpenInSystemViewer":
1045 return lazy.DownloadIntegration.shouldViewDownloadInternally(
1046 lazy.DownloadsCommon.getMimeInfo(this.download)?.type
1049 return DownloadsViewUI.isCommandName(aCommand) && !!this[aCommand];
1052 doCommand(aCommand) {
1053 // split off an optional command "modifier" into an argument,
1054 // e.g. "downloadsCmd_open:window"
1055 let [command, modifier] = aCommand.split(":");
1056 if (DownloadsViewUI.isCommandName(command)) {
1057 this[command](modifier);
1062 this.doCommand(this.buttonCommandName);
1065 downloadsCmd_cancel() {
1066 // This is the correct way to avoid race conditions when cancelling.
1067 this.download.cancel().catch(() => {});
1069 .removePartialData()
1070 .catch(console.error)
1071 .finally(() => this.download.target.refresh());
1074 downloadsCmd_confirmBlock() {
1075 this.download.confirmBlock().catch(console.error);
1078 downloadsCmd_open(openWhere = "tab") {
1079 lazy.DownloadsCommon.openDownload(this.download, {
1084 downloadsCmd_openReferrer() {
1085 this.element.ownerGlobal.openURL(
1086 this.download.source.referrerInfo.originalReferrer
1090 downloadsCmd_pauseResume() {
1091 if (this.download.stopped) {
1092 this.download.start();
1094 this.download.cancel();
1098 downloadsCmd_show() {
1099 let file = new lazy.FileUtils.File(this.download.target.path);
1100 lazy.DownloadsCommon.showDownloadedFile(file);
1103 downloadsCmd_retry() {
1104 if (this.download.start) {
1105 // Errors when retrying are already reported as download failures.
1106 this.download.start().catch(() => {});
1110 let window = this.browserWindow || this.element.ownerGlobal;
1111 let document = window.document;
1113 // Do not suggest a file name if we don't know the original target.
1114 let targetPath = this.download.target.path
1115 ? PathUtils.filename(this.download.target.path)
1117 window.DownloadURL(this.download.source.url, targetPath, document);
1120 downloadsCmd_delete() {
1121 // Alias for the 'cmd_delete' command, because it may clash with another
1122 // controller which causes unexpected behavior as different codepaths claim
1128 lazy.DownloadsCommon.deleteDownload(this.download).catch(console.error);
1131 async downloadsCmd_deleteFile() {
1132 // Remove the download from the session and history downloads, delete part files.
1133 await lazy.DownloadsCommon.deleteDownloadFiles(
1135 DownloadsViewUI.clearHistoryOnDelete
1139 downloadsCmd_openInSystemViewer() {
1140 // For this interaction only, pass a flag to override the preferredAction for this
1141 // mime-type and open using the system viewer
1142 lazy.DownloadsCommon.openDownload(this.download, {
1143 useSystemDefault: true,
1144 }).catch(console.error);
1147 downloadsCmd_alwaysOpenInSystemViewer() {
1148 // this command toggles between setting preferredAction for this mime-type to open
1149 // using the system viewer, or to open the file in browser.
1150 const mimeInfo = lazy.DownloadsCommon.getMimeInfo(this.download);
1153 "Can't open download with unknown mime-type in system viewer"
1156 if (mimeInfo.preferredAction !== mimeInfo.useSystemDefault) {
1157 // User has selected to open this mime-type with the system viewer from now on
1158 lazy.DownloadsCommon.log(
1159 "downloadsCmd_alwaysOpenInSystemViewer command for download: ",
1161 "switching to use system default for " + mimeInfo.type
1163 mimeInfo.preferredAction = mimeInfo.useSystemDefault;
1164 mimeInfo.alwaysAskBeforeHandling = false;
1166 lazy.DownloadsCommon.log(
1167 "downloadsCmd_alwaysOpenInSystemViewer command for download: ",
1169 "currently uses system default, switching to handleInternally"
1171 // User has selected to not open this mime-type with the system viewer
1172 mimeInfo.preferredAction = mimeInfo.handleInternally;
1174 lazy.handlerSvc.store(mimeInfo);
1175 lazy.DownloadsCommon.openDownload(this.download).catch(console.error);
1178 downloadsCmd_alwaysOpenSimilarFiles() {
1179 const mimeInfo = lazy.DownloadsCommon.getMimeInfo(this.download);
1181 throw new Error("Can't open download with unknown mime-type");
1184 // User has selected to always open this mime-type from now on and will add this
1185 // mime-type to our preferences table with the system default option. Open the
1186 // file immediately after selecting the menu item like alwaysOpenInSystemViewer.
1187 if (mimeInfo.preferredAction !== mimeInfo.useSystemDefault) {
1188 mimeInfo.preferredAction = mimeInfo.useSystemDefault;
1189 lazy.handlerSvc.store(mimeInfo);
1190 lazy.DownloadsCommon.openDownload(this.download).catch(console.error);
1192 // Otherwise, if user unchecks this option after already enabling it from the
1193 // context menu, resort to saveToDisk.
1194 mimeInfo.preferredAction = mimeInfo.saveToDisk;
1195 lazy.handlerSvc.store(mimeInfo);