From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../components/downloads/DownloadsViewUI.sys.mjs | 1201 ++++++++++++++++++++ 1 file changed, 1201 insertions(+) create mode 100644 browser/components/downloads/DownloadsViewUI.sys.mjs (limited to 'browser/components/downloads/DownloadsViewUI.sys.mjs') diff --git a/browser/components/downloads/DownloadsViewUI.sys.mjs b/browser/components/downloads/DownloadsViewUI.sys.mjs new file mode 100644 index 0000000000..0e0f5a7af3 --- /dev/null +++ b/browser/components/downloads/DownloadsViewUI.sys.mjs @@ -0,0 +1,1201 @@ +/* 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, { + DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs", + 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", +}); + +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. + 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)); + + alwaysOpenSimilarFilesItem.hidden = + canViewInternally || + state !== DOWNLOAD_FINISHED || + shouldNotRememberChoice; + + // 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, + "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(` + + + + + + + + + + +