/* 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. */ "use strict"; this.EXPORTED_SYMBOLS = [ "DownloadsViewUI", ]; const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", "resource://gre/modules/DownloadUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon", "resource:///modules/DownloadsCommon.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); this.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_"); }, }; /** * 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.jsm 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. */ this.DownloadsViewUI.DownloadElementShell = function() {} this.DownloadsViewUI.DownloadElementShell.prototype = { /** * The richlistitem for the download, initialized by the derived object. */ element: null, /** * URI string for the file type icon displayed in the download element. */ get image() { if (!this.download.target.path) { // Old history downloads may not have a target path. return "moz-icon://.unknown?size=32"; } // When a download that was previously in progress finishes successfully, it // means that the target file now exists and we can extract its specific // icon, for example from a Windows executable. To ensure that the icon is // reloaded, however, we must change the URI used by the XUL image element, // for example by adding a query parameter. This only works if we add one of // the parameters explicitly supported by the nsIMozIconURI interface. return "moz-icon://" + this.download.target.path + "?size=32" + (this.download.succeeded ? "&state=normal" : ""); }, /** * The user-facing label for the download. 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. */ get displayName() { if (!this.download.target.path) { return this.download.source.url; } return OS.Path.basename(this.download.target.path); }, /** * The progress element for the download, or undefined in case the XBL binding * has not been applied yet. */ get _progressElement() { if (!this.__progressElement) { // If the element is not available now, we will try again the next time. this.__progressElement = this.element.ownerDocument.getAnonymousElementByAttribute( this.element, "anonid", "progressmeter"); } return this.__progressElement; }, /** * Processes a major state change in the user interface, then proceeds with * the normal progress update. This function is not called for every progress * update in order to improve performance. */ _updateState() { this.element.setAttribute("displayName", this.displayName); this.element.setAttribute("image", this.image); this.element.setAttribute("state", DownloadsCommon.stateOfDownload(this.download)); if (!this.download.succeeded && this.download.error && this.download.error.becauseBlockedByReputationCheck) { this.element.setAttribute("verdict", this.download.error.reputationCheckVerdict); } else { this.element.removeAttribute("verdict"); } // Since state changed, reset the time left estimation. this.lastEstimatedSecondsLeft = Infinity; this._updateProgress(); }, /** * Updates the elements that change regularly for in-progress downloads, * namely the progress bar and the status line. */ _updateProgress() { if (this.download.succeeded) { // We only need to add or remove this attribute for succeeded downloads. if (this.download.target.exists) { this.element.setAttribute("exists", "true"); } else { this.element.removeAttribute("exists"); } } // When a block is confirmed, the removal of blocked data will not trigger a // state change for the download, so this class must be updated here. this.element.classList.toggle("temporary-block", !!this.download.hasBlockedData); // The progress bar is only displayed for in-progress downloads. if (this.download.hasProgress) { this.element.setAttribute("progressmode", "normal"); this.element.setAttribute("progress", this.download.progress); } else { this.element.setAttribute("progressmode", "undetermined"); } if (this.download.stopped && this.download.canceled && this.download.hasPartialData) { this.element.setAttribute("progresspaused", "true"); } else { this.element.removeAttribute("progresspaused"); } // Dispatch the ValueChange event for accessibility, if possible. if (this._progressElement) { let event = this.element.ownerDocument.createEvent("Events"); event.initEvent("ValueChange", true, true); this._progressElement.dispatchEvent(event); } let labels = this.statusLabels; this.element.setAttribute("status", labels.status); this.element.setAttribute("hoverStatus", labels.hoverStatus); this.element.setAttribute("fullStatus", labels.fullStatus); }, lastEstimatedSecondsLeft: Infinity, /** * Returns the labels for the status of normal, full, and hovering cases. These * are returned by a single property because they are computed together. */ get statusLabels() { let s = DownloadsCommon.strings; let status = ""; let hoverStatus = ""; let fullStatus = ""; if (!this.download.stopped) { let totalBytes = this.download.hasProgress ? this.download.totalBytes : -1; let newEstimatedSecondsLeft; [status, newEstimatedSecondsLeft] = DownloadUtils.getDownloadStatus( this.download.currentBytes, totalBytes, this.download.speed, this.lastEstimatedSecondsLeft); this.lastEstimatedSecondsLeft = newEstimatedSecondsLeft; hoverStatus = status; } else if (this.download.canceled && this.download.hasPartialData) { let totalBytes = this.download.hasProgress ? this.download.totalBytes : -1; let transfer = DownloadUtils.getTransferTotal(this.download.currentBytes, totalBytes); // We use the same XUL label to display both the state and the amount // transferred, for example "Paused - 1.1 MB". status = s.statusSeparatorBeforeNumber(s.statePaused, transfer); hoverStatus = status; } else if (!this.download.succeeded && !this.download.canceled && !this.download.error) { status = s.stateStarting; hoverStatus = status; } else { let stateLabel; if (this.download.succeeded && !this.download.target.exists) { stateLabel = s.fileMovedOrMissing; hoverStatus = stateLabel; } else if (this.download.succeeded) { // For completed downloads, show the file size (e.g. "1.5 MB"). if (this.download.target.size !== undefined) { let [size, unit] = DownloadUtils.convertByteUnits(this.download.target.size); stateLabel = s.sizeWithUnits(size, unit); } else { // History downloads may not have a size defined. stateLabel = s.sizeUnknown; } status = s.stateCompleted; hoverStatus = status; } else if (this.download.canceled) { stateLabel = s.stateCanceled; } else if (this.download.error.becauseBlockedByParentalControls) { stateLabel = s.stateBlockedParentalControls; } else if (this.download.error.becauseBlockedByReputationCheck) { stateLabel = this.rawBlockedTitleAndDetails[0]; } else { stateLabel = s.stateFailed; } let referrer = this.download.source.referrer || this.download.source.url; let [displayHost /* ,fullHost */] = DownloadUtils.getURIHost(referrer); let date = new Date(this.download.endTime); let [displayDate /* ,fullDate */] = DownloadUtils.getReadableDates(date); let firstPart = s.statusSeparator(stateLabel, displayHost); fullStatus = s.statusSeparator(firstPart, displayDate); status = status || stateLabel; } return { status, hoverStatus: hoverStatus || fullStatus, fullStatus: fullStatus || status, }; }, /** * Returns [title, [details1, details2]] for blocked downloads. */ get rawBlockedTitleAndDetails() { let s = DownloadsCommon.strings; if (!this.download.error || !this.download.error.becauseBlockedByReputationCheck) { return [null, null]; } switch (this.download.error.reputationCheckVerdict) { case Downloads.Error.BLOCK_VERDICT_UNCOMMON: return [s.blockedUncommon2, [s.unblockTypeUncommon2, s.unblockTip2]]; case Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED: return [s.blockedPotentiallyUnwanted, [s.unblockTypePotentiallyUnwanted2, s.unblockTip2]]; case Downloads.Error.BLOCK_VERDICT_MALWARE: return [s.blockedMalware, [s.unblockTypeMalware, s.unblockTip2]]; } throw new Error("Unexpected reputationCheckVerdict: " + this.download.error.reputationCheckVerdict); }, /** * Shows the appropriate unblock dialog based on the verdict, and executes the * action selected by the user in the dialog, which may involve unblocking, * opening or removing the file. * * @param window * The window to which the dialog should be anchored. * @param dialogType * Can be "unblock", "chooseUnblock", or "chooseOpen". */ confirmUnblock(window, dialogType) { DownloadsCommon.confirmUnblockDownload({ verdict: this.download.error.reputationCheckVerdict, window, dialogType, }).then(action => { if (action == "open") { return this.unblockAndOpenDownload(); } else if (action == "unblock") { return this.download.unblock(); } else if (action == "confirmBlock") { return this.download.confirmBlock(); } return Promise.resolve(); }).catch(Cu.reportError); }, /** * Unblocks the downloaded file and opens it. * * @return A promise that's resolved after the file has been opened. */ unblockAndOpenDownload() { return this.download.unblock().then(() => this.downloadsCmd_open()); }, /** * Returns the name of the default command to use for the current state of the * download, when there is a double click or another default interaction. If * there is no default command for the current state, returns an empty string. * The commands are implemented as functions on this object or derived ones. */ get currentDefaultCommandName() { switch (DownloadsCommon.stateOfDownload(this.download)) { case DownloadsCommon.DOWNLOAD_NOTSTARTED: return "downloadsCmd_cancel"; case DownloadsCommon.DOWNLOAD_FAILED: case DownloadsCommon.DOWNLOAD_CANCELED: return "downloadsCmd_retry"; case DownloadsCommon.DOWNLOAD_PAUSED: return "downloadsCmd_pauseResume"; case DownloadsCommon.DOWNLOAD_FINISHED: return "downloadsCmd_open"; case DownloadsCommon.DOWNLOAD_BLOCKED_PARENTAL: return "downloadsCmd_openReferrer"; case DownloadsCommon.DOWNLOAD_DIRTY: return "downloadsCmd_showBlockedInfo"; } return ""; }, /** * Returns true if the specified command can be invoked on the current item. * The commands are implemented as functions on this object or derived ones. * * @param aCommand * Name of the command to check, for example "downloadsCmd_retry". */ isCommandEnabled(aCommand) { switch (aCommand) { case "downloadsCmd_retry": return this.download.canceled || this.download.error; case "downloadsCmd_pauseResume": return this.download.hasPartialData && !this.download.error; case "downloadsCmd_openReferrer": return !!this.download.source.referrer; case "downloadsCmd_confirmBlock": case "downloadsCmd_chooseUnblock": case "downloadsCmd_chooseOpen": case "downloadsCmd_unblock": case "downloadsCmd_unblockAndOpen": return this.download.hasBlockedData; } return false; }, downloadsCmd_cancel() { // This is the correct way to avoid race conditions when cancelling. this.download.cancel().catch(() => {}); this.download.removePartialData().catch(Cu.reportError); }, downloadsCmd_retry() { // Errors when retrying are already reported as download failures. this.download.start().catch(() => {}); }, downloadsCmd_pauseResume() { if (this.download.stopped) { this.download.start(); } else { this.download.cancel(); } }, downloadsCmd_confirmBlock() { this.download.confirmBlock().catch(Cu.reportError); }, };