/* 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/. */ /* * This module is imported by code that uses the "download.xml" binding, and * provides prototypes for objects that handle input and display information. */ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { Downloads: "resource://gre/modules/Downloads.sys.mjs", DownloadsCommon: "resource:///modules/DownloadsCommon.sys.mjs", FileUtils: "resource://gre/modules/FileUtils.sys.mjs", UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", }); XPCOMUtils.defineLazyModuleGetters(lazy, { BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm", DownloadUtils: "resource://gre/modules/DownloadUtils.jsm", }); XPCOMUtils.defineLazyServiceGetter( lazy, "handlerSvc", "@mozilla.org/uriloader/handler-service;1", "nsIHandlerService" ); XPCOMUtils.defineLazyServiceGetter( lazy, "gReputationService", "@mozilla.org/reputationservice/application-reputation-service;1", Ci.nsIApplicationReputationService ); import { Integration } from "resource://gre/modules/Integration.sys.mjs"; Integration.downloads.defineESModuleGetter( lazy, "DownloadIntegration", "resource://gre/modules/DownloadIntegration.sys.mjs" ); const HTML_NS = "http://www.w3.org/1999/xhtml"; var gDownloadElementButtons = { cancel: { commandName: "downloadsCmd_cancel", l10nId: "downloads-cmd-cancel", descriptionL10nId: "downloads-cancel-download", panelL10nId: "downloads-cmd-cancel-panel", iconClass: "downloadIconCancel", }, retry: { commandName: "downloadsCmd_retry", l10nId: "downloads-cmd-retry", descriptionL10nId: "downloads-retry-download", panelL10nId: "downloads-cmd-retry-panel", iconClass: "downloadIconRetry", }, show: { commandName: "downloadsCmd_show", l10nId: "downloads-cmd-show-button-2", descriptionL10nId: "downloads-cmd-show-description-2", panelL10nId: "downloads-cmd-show-panel-2", iconClass: "downloadIconShow", }, subviewOpenOrRemoveFile: { commandName: "downloadsCmd_showBlockedInfo", l10nId: "downloads-cmd-choose-open", descriptionL10nId: "downloads-show-more-information", panelL10nId: "downloads-cmd-choose-open-panel", iconClass: "downloadIconSubviewArrow", }, askOpenOrRemoveFile: { commandName: "downloadsCmd_chooseOpen", l10nId: "downloads-cmd-choose-open", panelL10nId: "downloads-cmd-choose-open-panel", iconClass: "downloadIconShow", }, askRemoveFileOrAllow: { commandName: "downloadsCmd_chooseUnblock", l10nId: "downloads-cmd-choose-unblock", panelL10nId: "downloads-cmd-choose-unblock-panel", iconClass: "downloadIconShow", }, removeFile: { commandName: "downloadsCmd_confirmBlock", l10nId: "downloads-cmd-remove-file", panelL10nId: "downloads-cmd-remove-file-panel", iconClass: "downloadIconCancel", }, }; /** * Associates each document with a pre-built DOM fragment representing the * download list item. This is then cloned to create each individual list item. * This is stored on the document to prevent leaks that would occur if a single * instance created by one document's DOMParser was stored globally. */ var gDownloadListItemFragments = new WeakMap(); export var DownloadsViewUI = { /** * Returns true if the given string is the name of a command that can be * handled by the Downloads user interface, including standard commands. */ isCommandName(name) { return name.startsWith("cmd_") || name.startsWith("downloadsCmd_"); }, /** * Get source url of the download without'http' or'https' prefix. */ getStrippedUrl(download) { return lazy.UrlbarUtils.stripPrefixAndTrim(download?.source?.url, { stripHttp: true, stripHttps: true, })[0]; }, /** * Returns the user-facing label for the given Download object. This is * normally the leaf name of the download target file. In case this is a very * old history download for which the target file is unknown, the download * source URI is displayed. */ getDisplayName(download) { if ( download.error?.reputationCheckVerdict == lazy.Downloads.Error.BLOCK_VERDICT_DOWNLOAD_SPAM ) { let l10n = { id: "downloads-blocked-from-url", args: { url: DownloadsViewUI.getStrippedUrl(download) }, }; return { l10n }; } return download.target.path ? PathUtils.filename(download.target.path) : download.source.url; }, /** * Given a Download object, returns a string representing its file size with * an appropriate measurement unit, for example "1.5 MB", or an empty string * if the size is unknown. */ getSizeWithUnits(download) { if (download.target.size === undefined) { return ""; } let [size, unit] = lazy.DownloadUtils.convertByteUnits( download.target.size ); return lazy.DownloadsCommon.strings.sizeWithUnits(size, unit); }, /** * Given a context menu and a download element on which it is invoked, * update items in the context menu to reflect available options for * that download element. */ updateContextMenuForElement(contextMenu, element) { // Get the state and ensure only the appropriate items are displayed. let state = parseInt(element.getAttribute("state"), 10); const document = contextMenu.ownerDocument; const { DOWNLOAD_NOTSTARTED, DOWNLOAD_DOWNLOADING, DOWNLOAD_FINISHED, DOWNLOAD_FAILED, DOWNLOAD_CANCELED, DOWNLOAD_PAUSED, DOWNLOAD_BLOCKED_PARENTAL, DOWNLOAD_DIRTY, DOWNLOAD_BLOCKED_POLICY, } = lazy.DownloadsCommon; contextMenu.querySelector(".downloadPauseMenuItem").hidden = state != DOWNLOAD_DOWNLOADING; contextMenu.querySelector(".downloadResumeMenuItem").hidden = state != DOWNLOAD_PAUSED; // Only show "unblock" for blocked (dirty) items that have not been // confirmed and have temporary data: contextMenu.querySelector(".downloadUnblockMenuItem").hidden = state != DOWNLOAD_DIRTY || !element.classList.contains("temporary-block"); // Can only remove finished/failed/canceled/blocked downloads. contextMenu.querySelector(".downloadRemoveFromHistoryMenuItem").hidden = ![ DOWNLOAD_FINISHED, DOWNLOAD_FAILED, DOWNLOAD_CANCELED, DOWNLOAD_BLOCKED_PARENTAL, DOWNLOAD_DIRTY, DOWNLOAD_BLOCKED_POLICY, ].includes(state); // Can reveal downloads with data on the file system using the relevant OS // tool (Explorer, Finder, appropriate Linux file system viewer): contextMenu.querySelector(".downloadShowMenuItem").hidden = ![ DOWNLOAD_NOTSTARTED, DOWNLOAD_DOWNLOADING, DOWNLOAD_FINISHED, DOWNLOAD_PAUSED, ].includes(state) || (state == DOWNLOAD_FINISHED && !element.hasAttribute("exists")); // Show the separator if we're showing either unblock or reveal menu items. contextMenu.querySelector(".downloadCommandsSeparator").hidden = contextMenu.querySelector(".downloadUnblockMenuItem").hidden && contextMenu.querySelector(".downloadShowMenuItem").hidden; let download = element._shell.download; let mimeInfo = lazy.DownloadsCommon.getMimeInfo(download); let { preferredAction, useSystemDefault, defaultDescription } = mimeInfo ? mimeInfo : {}; // Hide the "Delete" item if there's no file data to delete. contextMenu.querySelector(".downloadDeleteFileMenuItem").hidden = download.deleted || !(download.target?.exists || download.target?.partFileExists); // Hide the "Go To Download Page" item if there's no referrer. Ideally the // Downloads API will require a referrer (see bug 1723712) to create a // download, but this fallback will ensure any failures aren't user facing. contextMenu.querySelector( ".downloadOpenReferrerMenuItem" ).hidden = !download.source.referrerInfo?.originalReferrer; // Hide the "use system viewer" and "always use system viewer" items // if the feature is disabled or this download doesn't support it: let useSystemViewerItem = contextMenu.querySelector( ".downloadUseSystemDefaultMenuItem" ); let alwaysUseSystemViewerItem = contextMenu.querySelector( ".downloadAlwaysUseSystemDefaultMenuItem" ); let canViewInternally = element.hasAttribute("viewable-internally"); useSystemViewerItem.hidden = !lazy.DownloadsCommon.openInSystemViewerItemEnabled || !canViewInternally || !download.target?.exists; alwaysUseSystemViewerItem.hidden = !lazy.DownloadsCommon.alwaysOpenInSystemViewerItemEnabled || !canViewInternally; // Set menuitem labels to display the system viewer's name. Stop the l10n // mutation observer temporarily since we're going to synchronously // translate the elements to avoid translation delay. See bug 1737951 & bug // 1746748. This can be simplified when they're resolved. try { document.l10n.pauseObserving(); // Handler descriptions longer than 40 characters will be skipped to avoid // unreasonably stretching the context menu. if (defaultDescription && defaultDescription.length < 40) { document.l10n.setAttributes( useSystemViewerItem, "downloads-cmd-use-system-default-named", { handler: defaultDescription } ); document.l10n.setAttributes( alwaysUseSystemViewerItem, "downloads-cmd-always-use-system-default-named", { handler: defaultDescription } ); } else { // In the unlikely event that defaultDescription is somehow missing/invalid, // fall back to the static "Open In System Viewer" label. document.l10n.setAttributes( useSystemViewerItem, "downloads-cmd-use-system-default" ); document.l10n.setAttributes( alwaysUseSystemViewerItem, "downloads-cmd-always-use-system-default" ); } } finally { document.l10n.resumeObserving(); } document.l10n.translateElements([ useSystemViewerItem, alwaysUseSystemViewerItem, ]); // If non default mime-type or cannot be opened internally, display // "always open similar files" item instead so that users can add a new // mimetype to about:preferences table and set to open with system default. // Only appear if browser.download.improvements_to_download_panel is enabled. let alwaysOpenSimilarFilesItem = contextMenu.querySelector( ".downloadAlwaysOpenSimilarFilesMenuItem" ); /** * In HelperAppDlg.sys.mjs, we determine whether or not an "always open..." checkbox * should appear in the unknownContentType window. Here, we use similar checks to * determine if we should show the "always open similar files" context menu item. * * Note that we also read the content type using mimeInfo to detect better and available * mime types, given a file extension. Some sites default to "application/octet-stream", * further limiting what file types can be added to about:preferences, even for file types * that are in fact capable of being handled with a default application. * * There are also cases where download.contentType is undefined (ex. when opening * the context menu on a previously downloaded item via download history). * Using mimeInfo ensures that content type exists and prevents intermittence. */ // let filename = PathUtils.filename(download.target.path); let isExemptExecutableExtension = Services.policies.isExemptExecutableExtension( download.source.originalUrl || download.source.url, filename?.split(".").at(-1) ); let shouldNotRememberChoice = !mimeInfo?.type || mimeInfo.type === "application/octet-stream" || mimeInfo.type === "application/x-msdownload" || mimeInfo.type === "application/x-msdos-program" || (lazy.gReputationService.isExecutable(filename) && !isExemptExecutableExtension) || (mimeInfo.type === "text/plain" && lazy.gReputationService.isBinary(download.target.path)); if (DownloadsViewUI.improvementsIsOn && !canViewInternally) { alwaysOpenSimilarFilesItem.hidden = state !== DOWNLOAD_FINISHED || shouldNotRememberChoice; } else { alwaysOpenSimilarFilesItem.hidden = true; } // Update checkbox for "always open..." options. if (preferredAction === useSystemDefault) { alwaysUseSystemViewerItem.setAttribute("checked", "true"); alwaysOpenSimilarFilesItem.setAttribute("checked", "true"); } else { alwaysUseSystemViewerItem.removeAttribute("checked"); alwaysOpenSimilarFilesItem.removeAttribute("checked"); } }, }; XPCOMUtils.defineLazyPreferenceGetter( DownloadsViewUI, "improvementsIsOn", "browser.download.improvements_to_download_panel", false ); XPCOMUtils.defineLazyPreferenceGetter( DownloadsViewUI, "clearHistoryOnDelete", "browser.download.clearHistoryOnDelete", 0 ); DownloadsViewUI.BaseView = class { canClearDownloads(nodeContainer) { // Downloads can be cleared if there's at least one removable download in // the list (either a history download or a completed session download). // Because history downloads are always removable and are listed after the // session downloads, check from bottom to top. for (let elt = nodeContainer.lastChild; elt; elt = elt.previousSibling) { // Stopped, paused, and failed downloads with partial data are removed. let download = elt._shell.download; if (download.stopped && !(download.canceled && download.hasPartialData)) { return true; } } return false; } }; /** * A download element shell is responsible for handling the commands and the * displayed data for a single element that uses the "download.xml" binding. * * The information to display is obtained through the associated Download object * from the JavaScript API for downloads, and commands are executed using a * combination of Download methods and DownloadsCommon.sys.mjs helper functions. * * Specialized versions of this shell must be defined, and they are required to * implement the "download" property or getter. Currently these objects are the * HistoryDownloadElementShell and the DownloadsViewItem for the panel. The * history view may use a HistoryDownload object in place of a Download object. */ DownloadsViewUI.DownloadElementShell = function() {}; DownloadsViewUI.DownloadElementShell.prototype = { /** * The richlistitem for the download, initialized by the derived object. */ element: null, /** * Manages the "active" state of the shell. By default all the shells are * inactive, thus their UI is not updated. They must be activated when * entering the visible area. */ ensureActive() { if (!this._active) { this._active = true; this.connect(); this.onChanged(); } }, get active() { return !!this._active; }, connect() { let document = this.element.ownerDocument; let downloadListItemFragment = gDownloadListItemFragments.get(document); // When changing the markup within the fragment, please ensure that // the functions within DownloadsView still operate correctly. if (!downloadListItemFragment) { let MozXULElement = document.defaultView.MozXULElement; downloadListItemFragment = MozXULElement.parseXULToFragment(`