/* 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/. */ "use strict"; // Globals const PREF_BDM_CLOSEWHENDONE = "browser.download.manager.closeWhenDone"; const PREF_BDM_CONFIRMOPENEXE = "browser.download.confirmOpenExecutable"; const PREF_BDM_SCANWHENDONE = "browser.download.manager.scanWhenDone"; const nsLocalFile = Components.Constructor("@mozilla.org/file/local;1", "nsILocalFile", "initWithPath"); var Cc = Components.classes; var Ci = Components.interfaces; var Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/DownloadUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/AppConstants.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm"); const nsIDM = Ci.nsIDownloadManager; var gDownloadManager = Cc["@mozilla.org/download-manager;1"].getService(nsIDM); var gDownloadManagerUI = Cc["@mozilla.org/download-manager-ui;1"]. getService(Ci.nsIDownloadManagerUI); var gDownloadListener = null; var gDownloadsView = null; var gSearchBox = null; var gSearchTerms = []; var gBuilder = 0; // This variable is used when performing commands on download items and gives // the command the ability to do something after all items have been operated // on. The following convention is used to handle the value of the variable: // whenever we aren't performing a command, the value is |undefined|; just // before executing commands, the value will be set to |null|; and when // commands want to create a callback, they set the value to be a callback // function to be executed after all download items have been visited. var gPerformAllCallback; // Control the performance of the incremental list building by setting how many // milliseconds to wait before building more of the list and how many items to // add between each delay. const gListBuildDelay = 300; const gListBuildChunk = 3; // Array of download richlistitem attributes to check when searching const gSearchAttributes = [ "target", "status", "dateTime", ]; // If the user has interacted with the window in a significant way, we should // not auto-close the window. Tough UI decisions about what is "significant." var gUserInteracted = false; // These strings will be converted to the corresponding ones from the string // bundle on startup. var gStr = { paused: "paused", cannotPause: "cannotPause", doneStatus: "doneStatus", doneSize: "doneSize", doneSizeUnknown: "doneSizeUnknown", stateFailed: "stateFailed", stateCanceled: "stateCanceled", stateBlockedParentalControls: "stateBlocked", stateBlockedPolicy: "stateBlockedPolicy", stateDirty: "stateDirty", downloadsTitleFiles: "downloadsTitleFiles", downloadsTitlePercent: "downloadsTitlePercent", fileExecutableSecurityWarningTitle: "fileExecutableSecurityWarningTitle", }; // The statement to query for downloads that are active or match the search var gStmt = null; // Start/Stop Observers function downloadCompleted(aDownload) { // The download is changing state, so update the clear list button updateClearListButton(); // Wrap this in try...catch since this can be called while shutting down... // it doesn't really matter if it fails then since well.. we're shutting down // and there's no UI to update! try { let dl = getDownload(aDownload.id); // Update attributes now that we've finished dl.setAttribute("startTime", Math.round(aDownload.startTime / 1000)); dl.setAttribute("endTime", Date.now()); dl.setAttribute("currBytes", aDownload.amountTransferred); dl.setAttribute("maxBytes", aDownload.size); // Move the download below active if it should stay in the list if (downloadMatchesSearch(dl)) { // Iterate down until we find a non-active download let next = dl.nextSibling; while (next && next.inProgress) next = next.nextSibling; // Move the item gDownloadsView.insertBefore(dl, next); } else { removeFromView(dl); } // getTypeFromFile fails if it can't find a type for this file. try { // Refresh the icon, so that executable icons are shown. var mimeService = Cc["@mozilla.org/mime;1"]. getService(Ci.nsIMIMEService); var contentType = mimeService.getTypeFromFile(aDownload.targetFile); var listItem = getDownload(aDownload.id) var oldImage = listItem.getAttribute("image"); // Tacking on contentType bypasses cache listItem.setAttribute("image", oldImage + "&contentType=" + contentType); } catch (e) { } if (gDownloadManager.activeDownloadCount == 0) document.title = document.documentElement.getAttribute("statictitle"); gDownloadManagerUI.getAttention(); } catch (e) { } } function autoRemoveAndClose(aDownload) { var pref = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefBranch); if (gDownloadManager.activeDownloadCount == 0) { // For the moment, just use the simple heuristic that if this window was // opened by the download process, rather than by the user, it should // auto-close if the pref is set that way. If the user opened it themselves, // it should not close until they explicitly close it. Additionally, the // preference to control the feature may not be set, so defaulting to // keeping the window open. let autoClose = false; try { autoClose = pref.getBoolPref(PREF_BDM_CLOSEWHENDONE); } catch (e) { } var autoOpened = !window.opener || window.opener.location.href == window.location.href; if (autoClose && autoOpened && !gUserInteracted) { gCloseDownloadManager(); return true; } } return false; } // This function can be overwritten by extensions that wish to place the // Download Window in another part of the UI. function gCloseDownloadManager() { window.close(); } // Download Event Handlers function cancelDownload(aDownload) { gDownloadManager.cancelDownload(aDownload.getAttribute("dlid")); // XXXben - // If we got here because we resumed the download, we weren't using a temp file // because we used saveURL instead. (this is because the proper download mechanism // employed by the helper app service isn't fully accessible yet... should be fixed... // talk to bz...) // the upshot is we have to delete the file if it exists. var f = getLocalFileFromNativePathOrUrl(aDownload.getAttribute("file")); if (f.exists()) f.remove(false); } function pauseDownload(aDownload) { var id = aDownload.getAttribute("dlid"); gDownloadManager.pauseDownload(id); } function resumeDownload(aDownload) { gDownloadManager.resumeDownload(aDownload.getAttribute("dlid")); } function removeDownload(aDownload) { gDownloadManager.removeDownload(aDownload.getAttribute("dlid")); } function retryDownload(aDownload) { removeFromView(aDownload); gDownloadManager.retryDownload(aDownload.getAttribute("dlid")); } function showDownload(aDownload) { var f = getLocalFileFromNativePathOrUrl(aDownload.getAttribute("file")); try { // Show the directory containing the file and select the file f.reveal(); } catch (e) { // 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 = f.parent.QueryInterface(Ci.nsILocalFile); if (!parent) return; try { // "Double click" the parent directory to show where the file should be parent.launch(); } catch (e) { // If launch also fails (probably because it's not implemented), let the // OS handler try to open the parent openExternal(parent); } } } function onDownloadDblClick(aEvent) { // Only do the default action for double primary clicks if (aEvent.button == 0 && aEvent.target.selected) doDefaultForSelected(); } function openDownload(aDownload) { var f = getLocalFileFromNativePathOrUrl(aDownload.getAttribute("file")); if (f.isExecutable()) { var dontAsk = false; var pref = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefBranch); try { dontAsk = !pref.getBoolPref(PREF_BDM_CONFIRMOPENEXE); } catch (e) { } if (AppConstants.platform == "win") { // On Vista and above, we rely on native security prompting for // downloaded content unless it's disabled. try { var sysInfo = Cc["@mozilla.org/system-info;1"]. getService(Ci.nsIPropertyBag2); if (parseFloat(sysInfo.getProperty("version")) >= 6 && pref.getBoolPref(PREF_BDM_SCANWHENDONE)) { dontAsk = true; } } catch (ex) { } } if (!dontAsk) { var strings = document.getElementById("downloadStrings"); var name = aDownload.getAttribute("target"); var message = strings.getFormattedString("fileExecutableSecurityWarning", [name, name]); let title = gStr.fileExecutableSecurityWarningTitle; var promptSvc = Cc["@mozilla.org/embedcomp/prompt-service;1"]. getService(Ci.nsIPromptService); var open = promptSvc.confirm(window, title, message); if (!open) return; } } try { try { let download = gDownloadManager.getDownload(aDownload.getAttribute("dlid")); let mimeInfo = download.MIMEInfo; if (mimeInfo.preferredAction == mimeInfo.useHelperApp) { mimeInfo.launchWithFile(f); return; } } catch (ex) { } f.launch(); } catch (ex) { // if launch fails, try sending it through the system's external // file: URL handler openExternal(f); } } function openReferrer(aDownload) { openURL(getReferrerOrSource(aDownload)); } function copySourceLocation(aDownload) { var uri = aDownload.getAttribute("uri"); var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]. getService(Ci.nsIClipboardHelper); // Check if we should initialize a callback if (gPerformAllCallback === null) { let uris = []; gPerformAllCallback = aURI => aURI ? uris.push(aURI) : clipboard.copyString(uris.join("\n")); } // We have a callback to use, so use it to add a uri if (typeof gPerformAllCallback == "function") gPerformAllCallback(uri); else { // It's a plain copy source, so copy it clipboard.copyString(uri); } } /** * Remove the currently shown downloads from the download list. */ function clearDownloadList() { // Clear the whole list if there's no search if (gSearchTerms == "") { gDownloadManager.cleanUp(); return; } // Remove each download starting from the end until we hit a download // that is in progress let item; while ((item = gDownloadsView.lastChild) && !item.inProgress) removeDownload(item); // Clear the input as if the user did it and move focus to the list gSearchBox.value = ""; gSearchBox.doCommand(); gDownloadsView.focus(); } // This is called by the progress listener. var gLastComputedMean = -1; var gLastActiveDownloads = 0; function onUpdateProgress() { let numActiveDownloads = gDownloadManager.activeDownloadCount; // Use the default title and reset "last" values if there's no downloads if (numActiveDownloads == 0) { document.title = document.documentElement.getAttribute("statictitle"); gLastComputedMean = -1; gLastActiveDownloads = 0; return; } // Establish the mean transfer speed and amount downloaded. var mean = 0; var base = 0; var dls = gDownloadManager.activeDownloads; while (dls.hasMoreElements()) { let dl = dls.getNext(); if (dl.percentComplete < 100 && dl.size > 0) { mean += dl.amountTransferred; base += dl.size; } } // Calculate the percent transferred, unless we don't have a total file size let title = gStr.downloadsTitlePercent; if (base == 0) title = gStr.downloadsTitleFiles; else mean = Math.floor((mean / base) * 100); // Update title of window if (mean != gLastComputedMean || gLastActiveDownloads != numActiveDownloads) { gLastComputedMean = mean; gLastActiveDownloads = numActiveDownloads; // Get the correct plural form and insert number of downloads and percent title = PluralForm.get(numActiveDownloads, title); title = replaceInsert(title, 1, numActiveDownloads); title = replaceInsert(title, 2, mean); document.title = title; } } // Startup, Shutdown function Startup() { gDownloadsView = document.getElementById("downloadView"); gSearchBox = document.getElementById("searchbox"); // convert strings to those in the string bundle let sb = document.getElementById("downloadStrings"); let getStr = string => sb.getString(string); for (let [name, value] of Object.entries(gStr)) gStr[name] = typeof value == "string" ? getStr(value) : value.map(getStr); initStatement(); buildDownloadList(true); // The DownloadProgressListener (DownloadProgressListener.js) handles progress // notifications. gDownloadListener = new DownloadProgressListener(); gDownloadManager.addListener(gDownloadListener); // If the UI was displayed because the user interacted, we need to make sure // we update gUserInteracted accordingly. if (window.arguments[1] == Ci.nsIDownloadManagerUI.REASON_USER_INTERACTED) gUserInteracted = true; // downloads can finish before Startup() does, so check if the window should // close and act accordingly if (!autoRemoveAndClose()) gDownloadsView.focus(); let obs = Cc["@mozilla.org/observer-service;1"]. getService(Ci.nsIObserverService); obs.addObserver(gDownloadObserver, "download-manager-remove-download", false); obs.addObserver(gDownloadObserver, "browser-lastwindow-close-granted", false); // Clear the search box and move focus to the list on escape from the box gSearchBox.addEventListener("keypress", function(e) { if (e.keyCode == e.DOM_VK_ESCAPE) { // Move focus to the list instead of closing the window gDownloadsView.focus(); e.preventDefault(); } }, false); let DownloadTaskbarProgress = Cu.import("resource://gre/modules/DownloadTaskbarProgress.jsm", {}).DownloadTaskbarProgress; DownloadTaskbarProgress.onDownloadWindowLoad(window); } function Shutdown() { gDownloadManager.removeListener(gDownloadListener); let obs = Cc["@mozilla.org/observer-service;1"]. getService(Ci.nsIObserverService); obs.removeObserver(gDownloadObserver, "download-manager-remove-download"); obs.removeObserver(gDownloadObserver, "browser-lastwindow-close-granted"); clearTimeout(gBuilder); gStmt.reset(); gStmt.finalize(); } var gDownloadObserver = { observe: function gdo_observe(aSubject, aTopic, aData) { switch (aTopic) { case "download-manager-remove-download": // A null subject here indicates "remove multiple", so we just rebuild. if (!aSubject) { // Rebuild the default view buildDownloadList(true); break; } // Otherwise, remove a single download let id = aSubject.QueryInterface(Ci.nsISupportsPRUint32); let dl = getDownload(id.data); removeFromView(dl); break; case "browser-lastwindow-close-granted": if (AppConstants.platform != "macosx" && gDownloadManager.activeDownloadCount == 0) { setTimeout(gCloseDownloadManager, 0); } break; } } }; // View Context Menus var gContextMenus = [ // DOWNLOAD_DOWNLOADING [ "menuitem_pause" , "menuitem_cancel" , "menuseparator" , "menuitem_show" , "menuseparator" , "menuitem_openReferrer" , "menuitem_copyLocation" , "menuseparator" , "menuitem_selectAll" ], // DOWNLOAD_FINISHED [ "menuitem_open" , "menuitem_show" , "menuseparator" , "menuitem_openReferrer" , "menuitem_copyLocation" , "menuseparator" , "menuitem_selectAll" , "menuseparator" , "menuitem_removeFromList" ], // DOWNLOAD_FAILED [ "menuitem_retry" , "menuseparator" , "menuitem_openReferrer" , "menuitem_copyLocation" , "menuseparator" , "menuitem_selectAll" , "menuseparator" , "menuitem_removeFromList" ], // DOWNLOAD_CANCELED [ "menuitem_retry" , "menuseparator" , "menuitem_openReferrer" , "menuitem_copyLocation" , "menuseparator" , "menuitem_selectAll" , "menuseparator" , "menuitem_removeFromList" ], // DOWNLOAD_PAUSED [ "menuitem_resume" , "menuitem_cancel" , "menuseparator" , "menuitem_show" , "menuseparator" , "menuitem_openReferrer" , "menuitem_copyLocation" , "menuseparator" , "menuitem_selectAll" ], // DOWNLOAD_QUEUED [ "menuitem_cancel" , "menuseparator" , "menuitem_show" , "menuseparator" , "menuitem_openReferrer" , "menuitem_copyLocation" , "menuseparator" , "menuitem_selectAll" ], // DOWNLOAD_BLOCKED_PARENTAL [ "menuitem_openReferrer" , "menuitem_copyLocation" , "menuseparator" , "menuitem_selectAll" , "menuseparator" , "menuitem_removeFromList" ], // DOWNLOAD_SCANNING [ "menuitem_show" , "menuseparator" , "menuitem_openReferrer" , "menuitem_copyLocation" , "menuseparator" , "menuitem_selectAll" ], // DOWNLOAD_DIRTY [ "menuitem_openReferrer" , "menuitem_copyLocation" , "menuseparator" , "menuitem_selectAll" , "menuseparator" , "menuitem_removeFromList" ], // DOWNLOAD_BLOCKED_POLICY [ "menuitem_openReferrer" , "menuitem_copyLocation" , "menuseparator" , "menuitem_selectAll" , "menuseparator" , "menuitem_removeFromList" ] ]; function buildContextMenu(aEvent) { if (aEvent.target.id != "downloadContextMenu") return false; var popup = document.getElementById("downloadContextMenu"); while (popup.hasChildNodes()) popup.removeChild(popup.firstChild); if (gDownloadsView.selectedItem) { let dl = gDownloadsView.selectedItem; let idx = parseInt(dl.getAttribute("state")); if (idx < 0) idx = 0; var menus = gContextMenus[idx]; for (let i = 0; i < menus.length; ++i) { let menuitem = document.getElementById(menus[i]).cloneNode(true); let cmd = menuitem.getAttribute("cmd"); if (cmd) menuitem.disabled = !gDownloadViewController.isCommandEnabled(cmd, dl); popup.appendChild(menuitem); } return true; } return false; } // Drag and Drop var gDownloadDNDObserver = { onDragStart: function (aEvent) { if (!gDownloadsView.selectedItem) return; var dl = gDownloadsView.selectedItem; var f = getLocalFileFromNativePathOrUrl(dl.getAttribute("file")); if (!f.exists()) return; var dt = aEvent.dataTransfer; dt.mozSetDataAt("application/x-moz-file", f, 0); var url = Services.io.newFileURI(f).spec; dt.setData("text/uri-list", url); dt.setData("text/plain", url); dt.effectAllowed = "copyMove"; dt.addElement(dl); }, onDragOver: function (aEvent) { var types = aEvent.dataTransfer.types; if (types.includes("text/uri-list") || types.includes("text/x-moz-url") || types.includes("text/plain")) aEvent.preventDefault(); }, onDrop: function(aEvent) { var dt = aEvent.dataTransfer; // If dragged item is from our source, do not try to // redownload already downloaded file. if (dt.mozGetDataAt("application/x-moz-file", 0)) return; var url = dt.getData("URL"); var name; if (!url) { url = dt.getData("text/x-moz-url") || dt.getData("text/plain"); [url, name] = url.split("\n"); } if (url) { let sourceDoc = dt.mozSourceNode ? dt.mozSourceNode.ownerDocument : document; saveURL(url, name ? name : url, null, true, true, null, sourceDoc); } } } function pasteHandler() { let trans = Cc["@mozilla.org/widget/transferable;1"]. createInstance(Ci.nsITransferable); trans.init(null); let flavors = ["text/x-moz-url", "text/unicode"]; flavors.forEach(trans.addDataFlavor); Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard); // Getting the data or creating the nsIURI might fail try { let data = {}; trans.getAnyTransferData({}, data, {}); let [url, name] = data.value.QueryInterface(Ci.nsISupportsString).data.split("\n"); if (!url) return; let uri = Services.io.newURI(url, null, null); saveURL(uri.spec, name || uri.spec, null, true, true, null, document); } catch (ex) {} } // Command Updating and Command Handlers var gDownloadViewController = { isCommandEnabled: function(aCommand, aItem) { let dl = aItem; let download = null; // used for getting an nsIDownload object switch (aCommand) { case "cmd_cancel": return dl.inProgress; case "cmd_open": { let file = getLocalFileFromNativePathOrUrl(dl.getAttribute("file")); return dl.openable && file.exists(); } case "cmd_show": { let file = getLocalFileFromNativePathOrUrl(dl.getAttribute("file")); return file.exists(); } case "cmd_pause": download = gDownloadManager.getDownload(dl.getAttribute("dlid")); return dl.inProgress && !dl.paused && download.resumable; case "cmd_pauseResume": download = gDownloadManager.getDownload(dl.getAttribute("dlid")); return (dl.inProgress || dl.paused) && download.resumable; case "cmd_resume": download = gDownloadManager.getDownload(dl.getAttribute("dlid")); return dl.paused && download.resumable; case "cmd_openReferrer": return dl.hasAttribute("referrer"); case "cmd_removeFromList": case "cmd_retry": return dl.removable; case "cmd_copyLocation": return true; } return false; }, doCommand: function(aCommand, aItem) { if (this.isCommandEnabled(aCommand, aItem)) this.commands[aCommand](aItem); }, commands: { cmd_cancel: function(aSelectedItem) { cancelDownload(aSelectedItem); }, cmd_open: function(aSelectedItem) { openDownload(aSelectedItem); }, cmd_openReferrer: function(aSelectedItem) { openReferrer(aSelectedItem); }, cmd_pause: function(aSelectedItem) { pauseDownload(aSelectedItem); }, cmd_pauseResume: function(aSelectedItem) { if (aSelectedItem.paused) this.cmd_resume(aSelectedItem); else this.cmd_pause(aSelectedItem); }, cmd_removeFromList: function(aSelectedItem) { removeDownload(aSelectedItem); }, cmd_resume: function(aSelectedItem) { resumeDownload(aSelectedItem); }, cmd_retry: function(aSelectedItem) { retryDownload(aSelectedItem); }, cmd_show: function(aSelectedItem) { showDownload(aSelectedItem); }, cmd_copyLocation: function(aSelectedItem) { copySourceLocation(aSelectedItem); }, } }; /** * Helper function to do commands. * * @param aCmd * The command to be performed. * @param aItem * The richlistitem that represents the download that will have the * command performed on it. If this is null, the command is performed on * all downloads. If the item passed in is not a richlistitem that * represents a download, it will walk up the parent nodes until it finds * a DOM node that is. */ function performCommand(aCmd, aItem) { let elm = aItem; if (!elm) { // If we don't have a desired download item, do the command for all // selected items. Initialize the callback to null so commands know to add // a callback if they want. We will call the callback with empty arguments // after performing the command on each selected download item. gPerformAllCallback = null; // Convert the nodelist into an array to keep a copy of the download items let items = []; for (let i = gDownloadsView.selectedItems.length; --i >= 0; ) items.unshift(gDownloadsView.selectedItems[i]); // Do the command for each download item for (let item of items) performCommand(aCmd, item); // Call the callback with no arguments and reset because we're done if (typeof gPerformAllCallback == "function") gPerformAllCallback(); gPerformAllCallback = undefined; return; } while (elm.nodeName != "richlistitem" || elm.getAttribute("type") != "download") { elm = elm.parentNode; } gDownloadViewController.doCommand(aCmd, elm); } function setSearchboxFocus() { gSearchBox.focus(); gSearchBox.select(); } function openExternal(aFile) { var uri = Cc["@mozilla.org/network/io-service;1"]. getService(Ci.nsIIOService).newFileURI(aFile); var protocolSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"]. getService(Ci.nsIExternalProtocolService); protocolSvc.loadUrl(uri); return; } // Utility Functions /** * Create a download richlistitem with the provided attributes. Some attributes * are *required* while optional ones will only be set on the item if provided. * * @param aAttrs * An object that must have the following properties: dlid, file, * target, uri, state, progress, startTime, endTime, currBytes, * maxBytes; optional properties: referrer * @return An initialized download richlistitem */ function createDownloadItem(aAttrs) { let dl = document.createElement("richlistitem"); // Copy the attributes from the argument into the item for (let attr in aAttrs) dl.setAttribute(attr, aAttrs[attr]); // Initialize other attributes dl.setAttribute("type", "download"); dl.setAttribute("id", "dl" + aAttrs.dlid); dl.setAttribute("image", "moz-icon://" + aAttrs.file + "?size=32"); dl.setAttribute("lastSeconds", Infinity); // Initialize more complex attributes updateTime(dl); updateStatus(dl); try { let file = getLocalFileFromNativePathOrUrl(aAttrs.file); dl.setAttribute("path", file.nativePath || file.path); return dl; } catch (e) { // aFile might not be a file: url or a valid native path // see bug #392386 for details } return null; } /** * Updates the disabled state of the buttons of a downlaod. * * @param aItem * The richlistitem representing the download. */ function updateButtons(aItem) { let buttons = aItem.buttons; for (let i = 0; i < buttons.length; ++i) { let cmd = buttons[i].getAttribute("cmd"); let enabled = gDownloadViewController.isCommandEnabled(cmd, aItem); buttons[i].disabled = !enabled; if ("cmd_pause" == cmd && !enabled) { // We need to add the tooltip indicating that the download cannot be // paused now. buttons[i].setAttribute("tooltiptext", gStr.cannotPause); } } } /** * Updates the status for a download item depending on its state * * @param aItem * The richlistitem that has various download attributes. * @param aDownload * The nsDownload from the backend. This is an optional parameter, but * is useful for certain states such as DOWNLOADING. */ function updateStatus(aItem, aDownload) { let status = ""; let statusTip = ""; let state = Number(aItem.getAttribute("state")); switch (state) { case nsIDM.DOWNLOAD_PAUSED: { let currBytes = Number(aItem.getAttribute("currBytes")); let maxBytes = Number(aItem.getAttribute("maxBytes")); let transfer = DownloadUtils.getTransferTotal(currBytes, maxBytes); status = replaceInsert(gStr.paused, 1, transfer); break; } case nsIDM.DOWNLOAD_DOWNLOADING: { let currBytes = Number(aItem.getAttribute("currBytes")); let maxBytes = Number(aItem.getAttribute("maxBytes")); // If we don't have an active download, assume 0 bytes/sec let speed = aDownload ? aDownload.speed : 0; let lastSec = Number(aItem.getAttribute("lastSeconds")); let newLast; [status, newLast] = DownloadUtils.getDownloadStatus(currBytes, maxBytes, speed, lastSec); // Update lastSeconds to be the new value aItem.setAttribute("lastSeconds", newLast); break; } case nsIDM.DOWNLOAD_FINISHED: case nsIDM.DOWNLOAD_FAILED: case nsIDM.DOWNLOAD_CANCELED: case nsIDM.DOWNLOAD_BLOCKED_PARENTAL: case nsIDM.DOWNLOAD_BLOCKED_POLICY: case nsIDM.DOWNLOAD_DIRTY: { let stateSize = {}; stateSize[nsIDM.DOWNLOAD_FINISHED] = function() { // Display the file size, but show "Unknown" for negative sizes let fileSize = Number(aItem.getAttribute("maxBytes")); let sizeText = gStr.doneSizeUnknown; if (fileSize >= 0) { let [size, unit] = DownloadUtils.convertByteUnits(fileSize); sizeText = replaceInsert(gStr.doneSize, 1, size); sizeText = replaceInsert(sizeText, 2, unit); } return sizeText; }; stateSize[nsIDM.DOWNLOAD_FAILED] = () => gStr.stateFailed; stateSize[nsIDM.DOWNLOAD_CANCELED] = () => gStr.stateCanceled; stateSize[nsIDM.DOWNLOAD_BLOCKED_PARENTAL] = () => gStr.stateBlockedParentalControls; stateSize[nsIDM.DOWNLOAD_BLOCKED_POLICY] = () => gStr.stateBlockedPolicy; stateSize[nsIDM.DOWNLOAD_DIRTY] = () => gStr.stateDirty; // Insert 1 is the download size or download state status = replaceInsert(gStr.doneStatus, 1, stateSize[state]()); let [displayHost, fullHost] = DownloadUtils.getURIHost(getReferrerOrSource(aItem)); // Insert 2 is the eTLD + 1 or other variations of the host status = replaceInsert(status, 2, displayHost); // Set the tooltip to be the full host statusTip = fullHost; break; } } aItem.setAttribute("status", status); aItem.setAttribute("statusTip", statusTip != "" ? statusTip : status); } /** * Updates the time that gets shown for completed download items * * @param aItem * The richlistitem representing a download in the UI */ function updateTime(aItem) { // Don't bother updating for things that aren't finished if (aItem.inProgress) return; let end = new Date(parseInt(aItem.getAttribute("endTime"))); let [dateCompact, dateComplete] = DownloadUtils.getReadableDates(end); aItem.setAttribute("dateTime", dateCompact); aItem.setAttribute("dateTimeTip", dateComplete); } /** * Helper function to replace a placeholder string with a real string * * @param aText * Source text containing placeholder (e.g., #1) * @param aIndex * Index number of placeholder to replace * @param aValue * New string to put in place of placeholder * @return The string with placeholder replaced with the new string */ function replaceInsert(aText, aIndex, aValue) { return aText.replace("#" + aIndex, aValue); } /** * Perform the default action for the currently selected download item */ function doDefaultForSelected() { // Make sure we have something selected let item = gDownloadsView.selectedItem; if (!item) return; // Get the default action (first item in the menu) let state = Number(item.getAttribute("state")); let menuitem = document.getElementById(gContextMenus[state][0]); // Try to do the action if the command is enabled gDownloadViewController.doCommand(menuitem.getAttribute("cmd"), item); } function removeFromView(aDownload) { // Make sure we have an item to remove if (!aDownload) return; let index = gDownloadsView.selectedIndex; gDownloadsView.removeChild(aDownload); gDownloadsView.selectedIndex = Math.min(index, gDownloadsView.itemCount - 1); // We might have removed the last item, so update the clear list button updateClearListButton(); } function getReferrerOrSource(aDownload) { // Give the referrer if we have it set if (aDownload.hasAttribute("referrer")) return aDownload.getAttribute("referrer"); // Otherwise, provide the source return aDownload.getAttribute("uri"); } /** * Initiate building the download list to have the active downloads followed by * completed ones filtered by the search term if necessary. * * @param aForceBuild * Force the list to be built even if the search terms don't change */ function buildDownloadList(aForceBuild) { // Stringify the previous search let prevSearch = gSearchTerms.join(" "); // Array of space-separated lower-case search terms gSearchTerms = gSearchBox.value.replace(/^\s+|\s+$/g, ""). toLowerCase().split(/\s+/); // Unless forced, don't rebuild the download list if the search didn't change if (!aForceBuild && gSearchTerms.join(" ") == prevSearch) return; // Clear out values before using them clearTimeout(gBuilder); gStmt.reset(); // Clear the list before adding items by replacing with a shallow copy let empty = gDownloadsView.cloneNode(false); gDownloadsView.parentNode.replaceChild(empty, gDownloadsView); gDownloadsView = empty; try { gStmt.bindByIndex(0, nsIDM.DOWNLOAD_NOTSTARTED); gStmt.bindByIndex(1, nsIDM.DOWNLOAD_DOWNLOADING); gStmt.bindByIndex(2, nsIDM.DOWNLOAD_PAUSED); gStmt.bindByIndex(3, nsIDM.DOWNLOAD_QUEUED); gStmt.bindByIndex(4, nsIDM.DOWNLOAD_SCANNING); } catch (e) { // Something must have gone wrong when binding, so clear and quit gStmt.reset(); return; } // Take a quick break before we actually start building the list gBuilder = setTimeout(function() { // Start building the list stepListBuilder(1); // We just tried to add a single item, so we probably need to enable updateClearListButton(); }, 0); } /** * Incrementally build the download list by adding at most the requested number * of items if there are items to add. After doing that, it will schedule * another chunk of items specified by gListBuildDelay and gListBuildChunk. * * @param aNumItems * Number of items to add to the list before taking a break */ function stepListBuilder(aNumItems) { try { // If we're done adding all items, we can quit if (!gStmt.executeStep()) { // Send a notification that we finished, but wait for clear list to update updateClearListButton(); setTimeout(() => Cc["@mozilla.org/observer-service;1"]. getService(Ci.nsIObserverService). notifyObservers(window, "download-manager-ui-done", null), 0); return; } // Try to get the attribute values from the statement let attrs = { dlid: gStmt.getInt64(0), file: gStmt.getString(1), target: gStmt.getString(2), uri: gStmt.getString(3), state: gStmt.getInt32(4), startTime: Math.round(gStmt.getInt64(5) / 1000), endTime: Math.round(gStmt.getInt64(6) / 1000), currBytes: gStmt.getInt64(8), maxBytes: gStmt.getInt64(9) }; // Only add the referrer if it's not null let referrer = gStmt.getString(7); if (referrer) attrs.referrer = referrer; // If the download is active, grab the real progress, otherwise default 100 let isActive = gStmt.getInt32(10); attrs.progress = isActive ? gDownloadManager.getDownload(attrs.dlid). percentComplete : 100; // Make the item and add it to the end if it's active or matches the search let item = createDownloadItem(attrs); if (item && (isActive || downloadMatchesSearch(item))) { // Add item to the end gDownloadsView.appendChild(item); // Because of the joys of XBL, we can't update the buttons until the // download object is in the document. updateButtons(item); } else { // We didn't add an item, so bump up the number of items to process, but // not a whole number so that we eventually do pause for a chunk break aNumItems += .9; } } catch (e) { // Something went wrong when stepping or getting values, so clear and quit gStmt.reset(); return; } // Add another item to the list if we should; otherwise, let the UI update // and continue later if (aNumItems > 1) { stepListBuilder(aNumItems - 1); } else { // Use a shorter delay for earlier downloads to display them faster let delay = Math.min(gDownloadsView.itemCount * 10, gListBuildDelay); gBuilder = setTimeout(stepListBuilder, delay, gListBuildChunk); } } /** * Add a download to the front of the download list * * @param aDownload * The nsIDownload to make into a richlistitem */ function prependList(aDownload) { let attrs = { dlid: aDownload.id, file: aDownload.target.spec, target: aDownload.displayName, uri: aDownload.source.spec, state: aDownload.state, progress: aDownload.percentComplete, startTime: Math.round(aDownload.startTime / 1000), endTime: Date.now(), currBytes: aDownload.amountTransferred, maxBytes: aDownload.size }; // Make the item and add it to the beginning let item = createDownloadItem(attrs); if (item) { // Add item to the beginning gDownloadsView.insertBefore(item, gDownloadsView.firstChild); // Because of the joys of XBL, we can't update the buttons until the // download object is in the document. updateButtons(item); // We might have added an item to an empty list, so update button updateClearListButton(); } } /** * Check if the download matches the current search term based on the texts * shown to the user. All search terms are checked to see if each matches any * of the displayed texts. * * @param aItem * Download richlistitem to check if it matches the current search * @return Boolean true if it matches the search; false otherwise */ function downloadMatchesSearch(aItem) { // Search through the download attributes that are shown to the user and // make it into one big string for easy combined searching let combinedSearch = ""; for (let attr of gSearchAttributes) combinedSearch += aItem.getAttribute(attr).toLowerCase() + " "; // Make sure each of the terms are found for (let term of gSearchTerms) if (combinedSearch.indexOf(term) == -1) return false; return true; } // we should be using real URLs all the time, but until // bug 239948 is fully fixed, this will do... // // note, this will thrown an exception if the native path // is not valid (for example a native Windows path on a Mac) // see bug #392386 for details function getLocalFileFromNativePathOrUrl(aPathOrUrl) { if (aPathOrUrl.substring(0, 7) == "file://") { // if this is a URL, get the file from that let ioSvc = Cc["@mozilla.org/network/io-service;1"]. getService(Ci.nsIIOService); // XXX it's possible that using a null char-set here is bad const fileUrl = ioSvc.newURI(aPathOrUrl, null, null). QueryInterface(Ci.nsIFileURL); return fileUrl.file.clone().QueryInterface(Ci.nsILocalFile); } // if it's a pathname, create the nsILocalFile directly var f = new nsLocalFile(aPathOrUrl); return f; } /** * Update the disabled state of the clear list button based on whether or not * there are items in the list that can potentially be removed. */ function updateClearListButton() { let button = document.getElementById("clearListButton"); // The button is enabled if we have items in the list and we can clean up button.disabled = !(gDownloadsView.itemCount && gDownloadManager.canCleanUp); } function getDownload(aID) { return document.getElementById("dl" + aID); } /** * Initialize the statement which is used to retrieve the list of downloads. */ function initStatement() { if (gStmt) gStmt.finalize(); gStmt = gDownloadManager.DBConnection.createStatement( "SELECT id, target, name, source, state, startTime, endTime, referrer, " + "currBytes, maxBytes, state IN (?1, ?2, ?3, ?4, ?5) isActive " + "FROM moz_downloads " + "ORDER BY isActive DESC, endTime DESC, startTime DESC"); }