From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- toolkit/mozapps/downloads/DownloadLastDir.jsm | 195 +++ toolkit/mozapps/downloads/DownloadPaths.jsm | 89 ++ .../mozapps/downloads/DownloadTaskbarProgress.jsm | 399 ++++++ toolkit/mozapps/downloads/DownloadUtils.jsm | 600 +++++++++ .../downloads/content/DownloadProgressListener.js | 117 ++ toolkit/mozapps/downloads/content/download.xml | 327 +++++ toolkit/mozapps/downloads/content/downloads.css | 50 + toolkit/mozapps/downloads/content/downloads.js | 1320 ++++++++++++++++++++ toolkit/mozapps/downloads/content/downloads.xul | 164 +++ .../downloads/content/unknownContentType.xul | 107 ++ toolkit/mozapps/downloads/jar.mn | 12 + toolkit/mozapps/downloads/moz.build | 24 + toolkit/mozapps/downloads/nsHelperAppDlg.js | 1147 +++++++++++++++++ toolkit/mozapps/downloads/nsHelperAppDlg.manifest | 2 + .../mozapps/downloads/tests/chrome/.eslintrc.js | 7 + toolkit/mozapps/downloads/tests/chrome/chrome.ini | 10 + .../test_unknownContentType_delayedbutton.xul | 117 ++ .../test_unknownContentType_dialog_layout.xul | 108 ++ .../unknownContentType_dialog_layout_data.pif | 1 + ...nownContentType_dialog_layout_data.pif^headers^ | 1 + .../unknownContentType_dialog_layout_data.txt | 1 + ...nownContentType_dialog_layout_data.txt^headers^ | 2 + toolkit/mozapps/downloads/tests/moz.build | 8 + toolkit/mozapps/downloads/tests/unit/.eslintrc.js | 7 + .../mozapps/downloads/tests/unit/head_downloads.js | 5 + .../downloads/tests/unit/test_DownloadPaths.js | 131 ++ .../downloads/tests/unit/test_DownloadUtils.js | 237 ++++ .../downloads/tests/unit/test_lowMinutes.js | 55 + .../tests/unit/test_syncedDownloadUtils.js | 26 + .../tests/unit/test_unspecified_arguments.js | 25 + toolkit/mozapps/downloads/tests/unit/xpcshell.ini | 10 + 31 files changed, 5304 insertions(+) create mode 100644 toolkit/mozapps/downloads/DownloadLastDir.jsm create mode 100644 toolkit/mozapps/downloads/DownloadPaths.jsm create mode 100644 toolkit/mozapps/downloads/DownloadTaskbarProgress.jsm create mode 100644 toolkit/mozapps/downloads/DownloadUtils.jsm create mode 100644 toolkit/mozapps/downloads/content/DownloadProgressListener.js create mode 100644 toolkit/mozapps/downloads/content/download.xml create mode 100644 toolkit/mozapps/downloads/content/downloads.css create mode 100644 toolkit/mozapps/downloads/content/downloads.js create mode 100644 toolkit/mozapps/downloads/content/downloads.xul create mode 100644 toolkit/mozapps/downloads/content/unknownContentType.xul create mode 100644 toolkit/mozapps/downloads/jar.mn create mode 100644 toolkit/mozapps/downloads/moz.build create mode 100644 toolkit/mozapps/downloads/nsHelperAppDlg.js create mode 100644 toolkit/mozapps/downloads/nsHelperAppDlg.manifest create mode 100644 toolkit/mozapps/downloads/tests/chrome/.eslintrc.js create mode 100644 toolkit/mozapps/downloads/tests/chrome/chrome.ini create mode 100644 toolkit/mozapps/downloads/tests/chrome/test_unknownContentType_delayedbutton.xul create mode 100644 toolkit/mozapps/downloads/tests/chrome/test_unknownContentType_dialog_layout.xul create mode 100644 toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.pif create mode 100644 toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.pif^headers^ create mode 100644 toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.txt create mode 100644 toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.txt^headers^ create mode 100644 toolkit/mozapps/downloads/tests/moz.build create mode 100644 toolkit/mozapps/downloads/tests/unit/.eslintrc.js create mode 100644 toolkit/mozapps/downloads/tests/unit/head_downloads.js create mode 100644 toolkit/mozapps/downloads/tests/unit/test_DownloadPaths.js create mode 100644 toolkit/mozapps/downloads/tests/unit/test_DownloadUtils.js create mode 100644 toolkit/mozapps/downloads/tests/unit/test_lowMinutes.js create mode 100644 toolkit/mozapps/downloads/tests/unit/test_syncedDownloadUtils.js create mode 100644 toolkit/mozapps/downloads/tests/unit/test_unspecified_arguments.js create mode 100644 toolkit/mozapps/downloads/tests/unit/xpcshell.ini (limited to 'toolkit/mozapps/downloads') diff --git a/toolkit/mozapps/downloads/DownloadLastDir.jsm b/toolkit/mozapps/downloads/DownloadLastDir.jsm new file mode 100644 index 000000000..552fd3ef1 --- /dev/null +++ b/toolkit/mozapps/downloads/DownloadLastDir.jsm @@ -0,0 +1,195 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +/* + * The behavior implemented by gDownloadLastDir is documented here. + * + * In normal browsing sessions, gDownloadLastDir uses the browser.download.lastDir + * preference to store the last used download directory. The first time the user + * switches into the private browsing mode, the last download directory is + * preserved to the pref value, but if the user switches to another directory + * during the private browsing mode, that directory is not stored in the pref, + * and will be merely kept in memory. When leaving the private browsing mode, + * this in-memory value will be discarded, and the last download directory + * will be reverted to the pref value. + * + * Both the pref and the in-memory value will be cleared when clearing the + * browsing history. This effectively changes the last download directory + * to the default download directory on each platform. + * + * If passed a URI, the last used directory is also stored with that URI in the + * content preferences database. This can be disabled by setting the pref + * browser.download.lastDir.savePerSite to false. + */ + +const LAST_DIR_PREF = "browser.download.lastDir"; +const SAVE_PER_SITE_PREF = LAST_DIR_PREF + ".savePerSite"; +const nsIFile = Components.interfaces.nsIFile; + +this.EXPORTED_SYMBOLS = [ "DownloadLastDir" ]; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); + +var observer = { + QueryInterface: function (aIID) { + if (aIID.equals(Components.interfaces.nsIObserver) || + aIID.equals(Components.interfaces.nsISupports) || + aIID.equals(Components.interfaces.nsISupportsWeakReference)) + return this; + throw Components.results.NS_NOINTERFACE; + }, + observe: function (aSubject, aTopic, aData) { + switch (aTopic) { + case "last-pb-context-exited": + gDownloadLastDirFile = null; + break; + case "browser:purge-session-history": + gDownloadLastDirFile = null; + if (Services.prefs.prefHasUserValue(LAST_DIR_PREF)) + Services.prefs.clearUserPref(LAST_DIR_PREF); + // Ensure that purging session history causes both the session-only PB cache + // and persistent prefs to be cleared. + let cps2 = Components.classes["@mozilla.org/content-pref/service;1"]. + getService(Components.interfaces.nsIContentPrefService2); + + cps2.removeByName(LAST_DIR_PREF, {usePrivateBrowsing: false}); + cps2.removeByName(LAST_DIR_PREF, {usePrivateBrowsing: true}); + break; + } + } +}; + +var os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); +os.addObserver(observer, "last-pb-context-exited", true); +os.addObserver(observer, "browser:purge-session-history", true); + +function readLastDirPref() { + try { + return Services.prefs.getComplexValue(LAST_DIR_PREF, nsIFile); + } + catch (e) { + return null; + } +} + +function isContentPrefEnabled() { + try { + return Services.prefs.getBoolPref(SAVE_PER_SITE_PREF); + } + catch (e) { + return true; + } +} + +var gDownloadLastDirFile = readLastDirPref(); + +this.DownloadLastDir = function DownloadLastDir(aWindow) { + let loadContext = aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIWebNavigation) + .QueryInterface(Components.interfaces.nsILoadContext); + // Need this in case the real thing has gone away by the time we need it. + // We only care about the private browsing state. All the rest of the + // load context isn't of interest to the content pref service. + this.fakeContext = { + QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsILoadContext]), + usePrivateBrowsing: loadContext.usePrivateBrowsing, + originAttributes: {}, + }; +} + +DownloadLastDir.prototype = { + isPrivate: function DownloadLastDir_isPrivate() { + return this.fakeContext.usePrivateBrowsing; + }, + // compat shims + get file() { return this._getLastFile(); }, + set file(val) { this.setFile(null, val); }, + cleanupPrivateFile: function () { + gDownloadLastDirFile = null; + }, + // This function is now deprecated as it uses the sync nsIContentPrefService + // interface. New consumers should use the getFileAsync function. + getFile: function (aURI) { + let Deprecated = Components.utils.import("resource://gre/modules/Deprecated.jsm", {}).Deprecated; + Deprecated.warning("DownloadLastDir.getFile is deprecated. Please use getFileAsync instead.", + "https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/DownloadLastDir.jsm", + Components.stack.caller); + + if (aURI && isContentPrefEnabled()) { + let lastDir = Services.contentPrefs.getPref(aURI, LAST_DIR_PREF, this.fakeContext); + if (lastDir) { + var lastDirFile = Components.classes["@mozilla.org/file/local;1"] + .createInstance(Components.interfaces.nsIFile); + lastDirFile.initWithPath(lastDir); + return lastDirFile; + } + } + return this._getLastFile(); + }, + + _getLastFile: function () { + if (gDownloadLastDirFile && !gDownloadLastDirFile.exists()) + gDownloadLastDirFile = null; + + if (this.isPrivate()) { + if (!gDownloadLastDirFile) + gDownloadLastDirFile = readLastDirPref(); + return gDownloadLastDirFile; + } + return readLastDirPref(); + }, + + getFileAsync: function(aURI, aCallback) { + let plainPrefFile = this._getLastFile(); + if (!aURI || !isContentPrefEnabled()) { + Services.tm.mainThread.dispatch(() => aCallback(plainPrefFile), + Components.interfaces.nsIThread.DISPATCH_NORMAL); + return; + } + + let uri = aURI instanceof Components.interfaces.nsIURI ? aURI.spec : aURI; + let cps2 = Components.classes["@mozilla.org/content-pref/service;1"] + .getService(Components.interfaces.nsIContentPrefService2); + let result = null; + cps2.getByDomainAndName(uri, LAST_DIR_PREF, this.fakeContext, { + handleResult: aResult => result = aResult, + handleCompletion: function(aReason) { + let file = plainPrefFile; + if (aReason == Components.interfaces.nsIContentPrefCallback2.COMPLETE_OK && + result instanceof Components.interfaces.nsIContentPref) { + file = Components.classes["@mozilla.org/file/local;1"] + .createInstance(Components.interfaces.nsIFile); + file.initWithPath(result.value); + } + aCallback(file); + } + }); + }, + + setFile: function (aURI, aFile) { + if (aURI && isContentPrefEnabled()) { + let uri = aURI instanceof Components.interfaces.nsIURI ? aURI.spec : aURI; + let cps2 = Components.classes["@mozilla.org/content-pref/service;1"] + .getService(Components.interfaces.nsIContentPrefService2); + if (aFile instanceof Components.interfaces.nsIFile) + cps2.set(uri, LAST_DIR_PREF, aFile.path, this.fakeContext); + else + cps2.removeByDomainAndName(uri, LAST_DIR_PREF, this.fakeContext); + } + if (this.isPrivate()) { + if (aFile instanceof Components.interfaces.nsIFile) + gDownloadLastDirFile = aFile.clone(); + else + gDownloadLastDirFile = null; + } else if (aFile instanceof Components.interfaces.nsIFile) { + Services.prefs.setComplexValue(LAST_DIR_PREF, nsIFile, aFile); + } else if (Services.prefs.prefHasUserValue(LAST_DIR_PREF)) { + Services.prefs.clearUserPref(LAST_DIR_PREF); + } + } +}; diff --git a/toolkit/mozapps/downloads/DownloadPaths.jsm b/toolkit/mozapps/downloads/DownloadPaths.jsm new file mode 100644 index 000000000..6ca6dfc13 --- /dev/null +++ b/toolkit/mozapps/downloads/DownloadPaths.jsm @@ -0,0 +1,89 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ +/* 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.EXPORTED_SYMBOLS = [ + "DownloadPaths", +]; + +/** + * This module provides the DownloadPaths object which contains methods for + * giving names and paths to files being downloaded. + * + * List of methods: + * + * nsILocalFile + * createNiceUniqueFile(nsILocalFile aLocalFile) + * + * [string base, string ext] + * splitBaseNameAndExtension(string aLeafName) + */ + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; + +this.DownloadPaths = { + /** + * Creates a uniquely-named file starting from the name of the provided file. + * If a file with the provided name already exists, the function attempts to + * create nice alternatives, like "base(1).ext" (instead of "base-1.ext"). + * + * If a unique name cannot be found, the function throws the XPCOM exception + * NS_ERROR_FILE_TOO_BIG. Other exceptions, like NS_ERROR_FILE_ACCESS_DENIED, + * can also be expected. + * + * @param aTemplateFile + * nsILocalFile whose leaf name is going to be used as a template. The + * provided object is not modified. + * @returns A new instance of an nsILocalFile object pointing to the newly + * created empty file. On platforms that support permission bits, the + * file is created with permissions 644. + */ + createNiceUniqueFile: function DP_createNiceUniqueFile(aTemplateFile) { + // Work on a clone of the provided template file object. + var curFile = aTemplateFile.clone().QueryInterface(Ci.nsILocalFile); + var [base, ext] = DownloadPaths.splitBaseNameAndExtension(curFile.leafName); + // Try other file names, for example "base(1).txt" or "base(1).tar.gz", + // only if the file name initially set already exists. + for (let i = 1; i < 10000 && curFile.exists(); i++) { + curFile.leafName = base + "(" + i + ")" + ext; + } + // At this point we hand off control to createUnique, which will create the + // file with the name we chose, if it is valid. If not, createUnique will + // attempt to modify it again, for example it will shorten very long names + // that can't be created on some platforms, and for which a normal call to + // nsIFile.create would result in NS_ERROR_FILE_NOT_FOUND. This can result + // very rarely in strange names like "base(9999).tar-1.gz" or "ba-1.gz". + curFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644); + return curFile; + }, + + /** + * Separates the base name from the extension in a file name, recognizing some + * double extensions like ".tar.gz". + * + * @param aLeafName + * The full leaf name to be parsed. Be careful when processing names + * containing leading or trailing dots or spaces. + * @returns [base, ext] + * The base name of the file, which can be empty, and its extension, + * which always includes the leading dot unless it's an empty string. + * Concatenating the two items always results in the original name. + */ + splitBaseNameAndExtension: function DP_splitBaseNameAndExtension(aLeafName) { + // The following regular expression is built from these key parts: + // .*? Matches the base name non-greedily. + // \.[A-Z0-9]{1,3} Up to three letters or numbers preceding a + // double extension. + // \.(?:gz|bz2|Z) The second part of common double extensions. + // \.[^.]* Matches any extension or a single trailing dot. + var [, base, ext] = /(.*?)(\.[A-Z0-9]{1,3}\.(?:gz|bz2|Z)|\.[^.]*)?$/i + .exec(aLeafName); + // Return an empty string instead of undefined if no extension is found. + return [base, ext || ""]; + } +}; diff --git a/toolkit/mozapps/downloads/DownloadTaskbarProgress.jsm b/toolkit/mozapps/downloads/DownloadTaskbarProgress.jsm new file mode 100644 index 000000000..bccbeda56 --- /dev/null +++ b/toolkit/mozapps/downloads/DownloadTaskbarProgress.jsm @@ -0,0 +1,399 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 sts=2 et filetype=javascript + * 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.EXPORTED_SYMBOLS = [ + "DownloadTaskbarProgress", +]; + +// Constants + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/AppConstants.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); + +const kTaskbarIDWin = "@mozilla.org/windows-taskbar;1"; +const kTaskbarIDMac = "@mozilla.org/widget/macdocksupport;1"; + +// DownloadTaskbarProgress Object + +this.DownloadTaskbarProgress = +{ + init: function DTP_init() + { + if (DownloadTaskbarProgressUpdater) { + DownloadTaskbarProgressUpdater._init(); + } + }, + + /** + * Called when a browser window appears. This has an effect only when we + * don't already have an active window. + * + * @param aWindow + * The browser window that we'll potentially use to display the + * progress. + */ + onBrowserWindowLoad: function DTP_onBrowserWindowLoad(aWindow) + { + this.init(); + if (!DownloadTaskbarProgressUpdater) { + return; + } + if (!DownloadTaskbarProgressUpdater._activeTaskbarProgress) { + DownloadTaskbarProgressUpdater._setActiveWindow(aWindow, false); + } + }, + + /** + * Called when the download window appears. The download window will take + * over as the active window. + */ + onDownloadWindowLoad: function DTP_onDownloadWindowLoad(aWindow) + { + if (!DownloadTaskbarProgressUpdater) { + return; + } + DownloadTaskbarProgressUpdater._setActiveWindow(aWindow, true); + }, + + /** + * Getters for internal DownloadTaskbarProgressUpdater values + */ + + get activeTaskbarProgress() { + if (!DownloadTaskbarProgressUpdater) { + return null; + } + return DownloadTaskbarProgressUpdater._activeTaskbarProgress; + }, + + get activeWindowIsDownloadWindow() { + if (!DownloadTaskbarProgressUpdater) { + return null; + } + return DownloadTaskbarProgressUpdater._activeWindowIsDownloadWindow; + }, + + get taskbarState() { + if (!DownloadTaskbarProgressUpdater) { + return null; + } + return DownloadTaskbarProgressUpdater._taskbarState; + }, + +}; + +// DownloadTaskbarProgressUpdater Object + +var DownloadTaskbarProgressUpdater = +{ + // / Whether the taskbar is initialized. + _initialized: false, + + // / Reference to the taskbar. + _taskbar: null, + + // / Reference to the download manager. + _dm: null, + + /** + * Initialize and register ourselves as a download progress listener. + */ + _init: function DTPU_init() + { + if (this._initialized) { + return; // Already initialized + } + this._initialized = true; + + if (kTaskbarIDWin in Cc) { + this._taskbar = Cc[kTaskbarIDWin].getService(Ci.nsIWinTaskbar); + if (!this._taskbar.available) { + // The Windows version is probably too old + DownloadTaskbarProgressUpdater = null; + return; + } + } else if (kTaskbarIDMac in Cc) { + this._activeTaskbarProgress = Cc[kTaskbarIDMac]. + getService(Ci.nsITaskbarProgress); + } else { + DownloadTaskbarProgressUpdater = null; + return; + } + + this._taskbarState = Ci.nsITaskbarProgress.STATE_NO_PROGRESS; + + this._dm = Cc["@mozilla.org/download-manager;1"]. + getService(Ci.nsIDownloadManager); + this._dm.addPrivacyAwareListener(this); + + this._os = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + this._os.addObserver(this, "quit-application-granted", false); + + this._updateStatus(); + // onBrowserWindowLoad/onDownloadWindowLoad are going to set the active + // window, so don't do it here. + }, + + /** + * Unregisters ourselves as a download progress listener. + */ + _uninit: function DTPU_uninit() { + this._dm.removeListener(this); + this._os.removeObserver(this, "quit-application-granted"); + this._activeTaskbarProgress = null; + this._initialized = false; + }, + + /** + * This holds a reference to the taskbar progress for the window we're + * working with. This window would preferably be download window, but can be + * another window if it isn't open. + */ + _activeTaskbarProgress: null, + + // / Whether the active window is the download window + _activeWindowIsDownloadWindow: false, + + /** + * Sets the active window, and whether it's the download window. This takes + * care of clearing out the previous active window's taskbar item, updating + * the taskbar, and setting an onunload listener. + * + * @param aWindow + * The window to set as active. + * @param aIsDownloadWindow + * Whether this window is a download window. + */ + _setActiveWindow: function DTPU_setActiveWindow(aWindow, aIsDownloadWindow) + { + if (AppConstants.platform == "win") { + // Clear out the taskbar for the old active window. (If there was no active + // window, this is a no-op.) + this._clearTaskbar(); + + this._activeWindowIsDownloadWindow = aIsDownloadWindow; + if (aWindow) { + // Get the taskbar progress for this window + let docShell = aWindow.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIWebNavigation). + QueryInterface(Ci.nsIDocShellTreeItem).treeOwner. + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIXULWindow).docShell; + let taskbarProgress = this._taskbar.getTaskbarProgress(docShell); + this._activeTaskbarProgress = taskbarProgress; + + this._updateTaskbar(); + // _onActiveWindowUnload is idempotent, so we don't need to check whether + // we've already set this before or not. + aWindow.addEventListener("unload", function () { + DownloadTaskbarProgressUpdater._onActiveWindowUnload(taskbarProgress); + }, false); + } + else { + this._activeTaskbarProgress = null; + } + } + }, + + // / Current state displayed on the active window's taskbar item + _taskbarState: null, + _totalSize: 0, + _totalTransferred: 0, + + _shouldSetState: function DTPU_shouldSetState() + { + if (AppConstants.platform == "win") { + // If the active window is not the download manager window, set the state + // only if it is normal or indeterminate. + return this._activeWindowIsDownloadWindow || + (this._taskbarState == Ci.nsITaskbarProgress.STATE_NORMAL || + this._taskbarState == Ci.nsITaskbarProgress.STATE_INDETERMINATE); + } + return true; + }, + + /** + * Update the active window's taskbar indicator with the current state. There + * are two cases here: + * 1. If the active window is the download window, then we always update + * the taskbar indicator. + * 2. If the active window isn't the download window, then we update only if + * the status is normal or indeterminate. i.e. one or more downloads are + * currently progressing or in scan mode. If we aren't, then we clear the + * indicator. + */ + _updateTaskbar: function DTPU_updateTaskbar() + { + if (!this._activeTaskbarProgress) { + return; + } + + if (this._shouldSetState()) { + this._activeTaskbarProgress.setProgressState(this._taskbarState, + this._totalTransferred, + this._totalSize); + } + // Clear any state otherwise + else { + this._clearTaskbar(); + } + }, + + /** + * Clear taskbar state. This is needed: + * - to transfer the indicator off a window before transferring it onto + * another one + * - whenever we don't want to show it for a non-download window. + */ + _clearTaskbar: function DTPU_clearTaskbar() + { + if (this._activeTaskbarProgress) { + this._activeTaskbarProgress.setProgressState( + Ci.nsITaskbarProgress.STATE_NO_PROGRESS + ); + } + }, + + /** + * Update this._taskbarState, this._totalSize and this._totalTransferred. + * This is called when the download manager is initialized or when the + * progress or state of a download changes. + * We compute the number of active and paused downloads, and the total size + * and total amount already transferred across whichever downloads we have + * the data for. + * - If there are no active downloads, then we don't want to show any + * progress. + * - If the number of active downloads is equal to the number of paused + * downloads, then we show a paused indicator if we know the size of at + * least one download, and no indicator if we don't. + * - If the number of active downloads is more than the number of paused + * downloads, then we show a "normal" indicator if we know the size of at + * least one download, and an indeterminate indicator if we don't. + */ + _updateStatus: function DTPU_updateStatus() + { + let numActive = this._dm.activeDownloadCount + this._dm.activePrivateDownloadCount; + let totalSize = 0, totalTransferred = 0; + + if (numActive == 0) { + this._taskbarState = Ci.nsITaskbarProgress.STATE_NO_PROGRESS; + } + else { + let numPaused = 0, numScanning = 0; + + // Enumerate all active downloads + [this._dm.activeDownloads, this._dm.activePrivateDownloads].forEach(function(downloads) { + while (downloads.hasMoreElements()) { + let download = downloads.getNext().QueryInterface(Ci.nsIDownload); + // Only set values if we actually know the download size + if (download.percentComplete != -1) { + totalSize += download.size; + totalTransferred += download.amountTransferred; + } + // We might need to display a paused state, so track this + if (download.state == this._dm.DOWNLOAD_PAUSED) { + numPaused++; + } else if (download.state == this._dm.DOWNLOAD_SCANNING) { + numScanning++; + } + } + }.bind(this)); + + // If all downloads are paused, show the progress as paused, unless we + // don't have any information about sizes, in which case we don't + // display anything + if (numActive == numPaused) { + if (totalSize == 0) { + this._taskbarState = Ci.nsITaskbarProgress.STATE_NO_PROGRESS; + totalTransferred = 0; + } + else { + this._taskbarState = Ci.nsITaskbarProgress.STATE_PAUSED; + } + } + // If at least one download is not paused, and we don't have any + // information about download sizes, display an indeterminate indicator + else if (totalSize == 0 || numActive == numScanning) { + this._taskbarState = Ci.nsITaskbarProgress.STATE_INDETERMINATE; + totalSize = 0; + totalTransferred = 0; + } + // Otherwise display a normal progress bar + else { + this._taskbarState = Ci.nsITaskbarProgress.STATE_NORMAL; + } + } + + this._totalSize = totalSize; + this._totalTransferred = totalTransferred; + }, + + /** + * Called when a window that at one point has been an active window is + * closed. If this window is currently the active window, we need to look for + * another window and make that our active window. + * + * This function is idempotent, so multiple calls for the same window are not + * a problem. + * + * @param aTaskbarProgress + * The taskbar progress for the window that is being unloaded. + */ + _onActiveWindowUnload: function DTPU_onActiveWindowUnload(aTaskbarProgress) + { + if (this._activeTaskbarProgress == aTaskbarProgress) { + let windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); + let windows = windowMediator.getEnumerator(null); + let newActiveWindow = null; + if (windows.hasMoreElements()) { + newActiveWindow = windows.getNext().QueryInterface(Ci.nsIDOMWindow); + } + + // We aren't ever going to reach this point while the download manager is + // open, so it's safe to assume false for the second operand + this._setActiveWindow(newActiveWindow, false); + } + }, + + // nsIDownloadProgressListener + + /** + * Update status if a download's progress has changed. + */ + onProgressChange: function DTPU_onProgressChange() + { + this._updateStatus(); + this._updateTaskbar(); + }, + + /** + * Update status if a download's state has changed. + */ + onDownloadStateChange: function DTPU_onDownloadStateChange() + { + this._updateStatus(); + this._updateTaskbar(); + }, + + onSecurityChange: function() { }, + + onStateChange: function() { }, + + observe: function DTPU_observe(aSubject, aTopic, aData) { + if (aTopic == "quit-application-granted") { + this._uninit(); + } + } +}; diff --git a/toolkit/mozapps/downloads/DownloadUtils.jsm b/toolkit/mozapps/downloads/DownloadUtils.jsm new file mode 100644 index 000000000..3ebdd605e --- /dev/null +++ b/toolkit/mozapps/downloads/DownloadUtils.jsm @@ -0,0 +1,600 @@ +/* vim: sw=2 ts=2 sts=2 expandtab filetype=javascript + * 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"; + +this.EXPORTED_SYMBOLS = [ "DownloadUtils" ]; + +/** + * This module provides the DownloadUtils object which contains useful methods + * for downloads such as displaying file sizes, transfer times, and download + * locations. + * + * List of methods: + * + * [string status, double newLast] + * getDownloadStatus(int aCurrBytes, [optional] int aMaxBytes, + * [optional] double aSpeed, [optional] double aLastSec) + * + * string progress + * getTransferTotal(int aCurrBytes, [optional] int aMaxBytes) + * + * [string timeLeft, double newLast] + * getTimeLeft(double aSeconds, [optional] double aLastSec) + * + * [string dateCompact, string dateComplete] + * getReadableDates(Date aDate, [optional] Date aNow) + * + * [string displayHost, string fullHost] + * getURIHost(string aURIString) + * + * [string convertedBytes, string units] + * convertByteUnits(int aBytes) + * + * [int time, string units, int subTime, string subUnits] + * convertTimeUnits(double aSecs) + */ + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", + "resource://gre/modules/PluralForm.jsm"); + +this.__defineGetter__("gDecimalSymbol", function() { + delete this.gDecimalSymbol; + return this.gDecimalSymbol = Number(5.4).toLocaleString().match(/\D/); +}); + +var localeNumberFormatCache = new Map(); +function getLocaleNumberFormat(fractionDigits) { + // Backward compatibility: don't use localized digits + let locale = Intl.NumberFormat().resolvedOptions().locale + + "-u-nu-latn"; + let key = locale + "_" + fractionDigits; + if (!localeNumberFormatCache.has(key)) { + localeNumberFormatCache.set(key, + Intl.NumberFormat(locale, + { maximumFractionDigits: fractionDigits, + minimumFractionDigits: fractionDigits })); + } + return localeNumberFormatCache.get(key); +} + +const kDownloadProperties = + "chrome://mozapps/locale/downloads/downloads.properties"; + +var gStr = { + statusFormat: "statusFormat3", + statusFormatInfiniteRate: "statusFormatInfiniteRate", + statusFormatNoRate: "statusFormatNoRate", + transferSameUnits: "transferSameUnits2", + transferDiffUnits: "transferDiffUnits2", + transferNoTotal: "transferNoTotal2", + timePair: "timePair2", + timeLeftSingle: "timeLeftSingle2", + timeLeftDouble: "timeLeftDouble2", + timeFewSeconds: "timeFewSeconds", + timeUnknown: "timeUnknown", + monthDate: "monthDate2", + yesterday: "yesterday", + doneScheme: "doneScheme2", + doneFileScheme: "doneFileScheme", + units: ["bytes", "kilobyte", "megabyte", "gigabyte"], + // Update timeSize in convertTimeUnits if changing the length of this array + timeUnits: ["seconds", "minutes", "hours", "days"], + infiniteRate: "infiniteRate", +}; + +// This lazily initializes the string bundle upon first use. +this.__defineGetter__("gBundle", function() { + delete this.gBundle; + return this.gBundle = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle(kDownloadProperties); +}); + +// Keep track of at most this many second/lastSec pairs so that multiple calls +// to getTimeLeft produce the same time left +const kCachedLastMaxSize = 10; +var gCachedLast = []; + +this.DownloadUtils = { + /** + * Generate a full status string for a download given its current progress, + * total size, speed, last time remaining + * + * @param aCurrBytes + * Number of bytes transferred so far + * @param [optional] aMaxBytes + * Total number of bytes or -1 for unknown + * @param [optional] aSpeed + * Current transfer rate in bytes/sec or -1 for unknown + * @param [optional] aLastSec + * Last time remaining in seconds or Infinity for unknown + * @return A pair: [download status text, new value of "last seconds"] + */ + getDownloadStatus: function DU_getDownloadStatus(aCurrBytes, aMaxBytes, + aSpeed, aLastSec) + { + let [transfer, timeLeft, newLast, normalizedSpeed] + = this._deriveTransferRate(aCurrBytes, aMaxBytes, aSpeed, aLastSec); + + let [rate, unit] = DownloadUtils.convertByteUnits(normalizedSpeed); + + let status; + if (rate === "Infinity") { + // Infinity download speed doesn't make sense. Show a localized phrase instead. + let params = [transfer, gBundle.GetStringFromName(gStr.infiniteRate), timeLeft]; + status = gBundle.formatStringFromName(gStr.statusFormatInfiniteRate, params, + params.length); + } + else { + let params = [transfer, rate, unit, timeLeft]; + status = gBundle.formatStringFromName(gStr.statusFormat, params, + params.length); + } + return [status, newLast]; + }, + + /** + * Generate a status string for a download given its current progress, + * total size, speed, last time remaining. The status string contains the + * time remaining, as well as the total bytes downloaded. Unlike + * getDownloadStatus, it does not include the rate of download. + * + * @param aCurrBytes + * Number of bytes transferred so far + * @param [optional] aMaxBytes + * Total number of bytes or -1 for unknown + * @param [optional] aSpeed + * Current transfer rate in bytes/sec or -1 for unknown + * @param [optional] aLastSec + * Last time remaining in seconds or Infinity for unknown + * @return A pair: [download status text, new value of "last seconds"] + */ + getDownloadStatusNoRate: + function DU_getDownloadStatusNoRate(aCurrBytes, aMaxBytes, aSpeed, + aLastSec) + { + let [transfer, timeLeft, newLast] + = this._deriveTransferRate(aCurrBytes, aMaxBytes, aSpeed, aLastSec); + + let params = [transfer, timeLeft]; + let status = gBundle.formatStringFromName(gStr.statusFormatNoRate, params, + params.length); + return [status, newLast]; + }, + + /** + * Helper function that returns a transfer string, a time remaining string, + * and a new value of "last seconds". + * @param aCurrBytes + * Number of bytes transferred so far + * @param [optional] aMaxBytes + * Total number of bytes or -1 for unknown + * @param [optional] aSpeed + * Current transfer rate in bytes/sec or -1 for unknown + * @param [optional] aLastSec + * Last time remaining in seconds or Infinity for unknown + * @return A triple: [amount transferred string, time remaining string, + * new value of "last seconds"] + */ + _deriveTransferRate: function DU__deriveTransferRate(aCurrBytes, + aMaxBytes, aSpeed, + aLastSec) + { + if (aMaxBytes == null) + aMaxBytes = -1; + if (aSpeed == null) + aSpeed = -1; + if (aLastSec == null) + aLastSec = Infinity; + + // Calculate the time remaining if we have valid values + let seconds = (aSpeed > 0) && (aMaxBytes > 0) ? + (aMaxBytes - aCurrBytes) / aSpeed : -1; + + let transfer = DownloadUtils.getTransferTotal(aCurrBytes, aMaxBytes); + let [timeLeft, newLast] = DownloadUtils.getTimeLeft(seconds, aLastSec); + return [transfer, timeLeft, newLast, aSpeed]; + }, + + /** + * Generate the transfer progress string to show the current and total byte + * size. Byte units will be as large as possible and the same units for + * current and max will be suppressed for the former. + * + * @param aCurrBytes + * Number of bytes transferred so far + * @param [optional] aMaxBytes + * Total number of bytes or -1 for unknown + * @return The transfer progress text + */ + getTransferTotal: function DU_getTransferTotal(aCurrBytes, aMaxBytes) + { + if (aMaxBytes == null) + aMaxBytes = -1; + + let [progress, progressUnits] = DownloadUtils.convertByteUnits(aCurrBytes); + let [total, totalUnits] = DownloadUtils.convertByteUnits(aMaxBytes); + + // Figure out which byte progress string to display + let name, values; + if (aMaxBytes < 0) { + name = gStr.transferNoTotal; + values = [ + progress, + progressUnits, + ]; + } else if (progressUnits == totalUnits) { + name = gStr.transferSameUnits; + values = [ + progress, + total, + totalUnits, + ]; + } else { + name = gStr.transferDiffUnits; + values = [ + progress, + progressUnits, + total, + totalUnits, + ]; + } + + return gBundle.formatStringFromName(name, values, values.length); + }, + + /** + * Generate a "time left" string given an estimate on the time left and the + * last time. The extra time is used to give a better estimate on the time to + * show. Both the time values are doubles instead of integers to help get + * sub-second accuracy for current and future estimates. + * + * @param aSeconds + * Current estimate on number of seconds left for the download + * @param [optional] aLastSec + * Last time remaining in seconds or Infinity for unknown + * @return A pair: [time left text, new value of "last seconds"] + */ + getTimeLeft: function DU_getTimeLeft(aSeconds, aLastSec) + { + if (aLastSec == null) + aLastSec = Infinity; + + if (aSeconds < 0) + return [gBundle.GetStringFromName(gStr.timeUnknown), aLastSec]; + + // Try to find a cached lastSec for the given second + aLastSec = gCachedLast.reduce((aResult, aItem) => + aItem[0] == aSeconds ? aItem[1] : aResult, aLastSec); + + // Add the current second/lastSec pair unless we have too many + gCachedLast.push([aSeconds, aLastSec]); + if (gCachedLast.length > kCachedLastMaxSize) + gCachedLast.shift(); + + // Apply smoothing only if the new time isn't a huge change -- e.g., if the + // new time is more than half the previous time; this is useful for + // downloads that start/resume slowly + if (aSeconds > aLastSec / 2) { + // Apply hysteresis to favor downward over upward swings + // 30% of down and 10% of up (exponential smoothing) + let diff = aSeconds - aLastSec; + aSeconds = aLastSec + (diff < 0 ? .3 : .1) * diff; + + // If the new time is similar, reuse something close to the last seconds, + // but subtract a little to provide forward progress + let diffPct = diff / aLastSec * 100; + if (Math.abs(diff) < 5 || Math.abs(diffPct) < 5) + aSeconds = aLastSec - (diff < 0 ? .4 : .2); + } + + // Decide what text to show for the time + let timeLeft; + if (aSeconds < 4) { + // Be friendly in the last few seconds + timeLeft = gBundle.GetStringFromName(gStr.timeFewSeconds); + } else { + // Convert the seconds into its two largest units to display + let [time1, unit1, time2, unit2] = + DownloadUtils.convertTimeUnits(aSeconds); + + let pair1 = + gBundle.formatStringFromName(gStr.timePair, [time1, unit1], 2); + let pair2 = + gBundle.formatStringFromName(gStr.timePair, [time2, unit2], 2); + + // Only show minutes for under 1 hour unless there's a few minutes left; + // or the second pair is 0. + if ((aSeconds < 3600 && time1 >= 4) || time2 == 0) { + timeLeft = gBundle.formatStringFromName(gStr.timeLeftSingle, + [pair1], 1); + } else { + // We've got 2 pairs of times to display + timeLeft = gBundle.formatStringFromName(gStr.timeLeftDouble, + [pair1, pair2], 2); + } + } + + return [timeLeft, aSeconds]; + }, + + /** + * Converts a Date object to two readable formats, one compact, one complete. + * The compact format is relative to the current date, and is not an accurate + * representation. For example, only the time is displayed for today. The + * complete format always includes both the date and the time, excluding the + * seconds, and is often shown when hovering the cursor over the compact + * representation. + * + * @param aDate + * Date object representing the date and time to format. It is assumed + * that this value represents a past date. + * @param [optional] aNow + * Date object representing the current date and time. The real date + * and time of invocation is used if this parameter is omitted. + * @return A pair: [compact text, complete text] + */ + getReadableDates: function DU_getReadableDates(aDate, aNow) + { + if (!aNow) { + aNow = new Date(); + } + + let dts = Cc["@mozilla.org/intl/scriptabledateformat;1"] + .getService(Ci.nsIScriptableDateFormat); + + // Figure out when today begins + let today = new Date(aNow.getFullYear(), aNow.getMonth(), aNow.getDate()); + + // Get locale to use for date/time formatting + // TODO: Remove Intl fallback when bug 1215247 is fixed. + const locale = typeof Intl === "undefined" + ? undefined + : Cc["@mozilla.org/chrome/chrome-registry;1"] + .getService(Ci.nsIXULChromeRegistry) + .getSelectedLocale("global", true); + + // Figure out if the time is from today, yesterday, this week, etc. + let dateTimeCompact; + if (aDate >= today) { + // After today started, show the time + dateTimeCompact = dts.FormatTime("", + dts.timeFormatNoSeconds, + aDate.getHours(), + aDate.getMinutes(), + 0); + } else if (today - aDate < (24 * 60 * 60 * 1000)) { + // After yesterday started, show yesterday + dateTimeCompact = gBundle.GetStringFromName(gStr.yesterday); + } else if (today - aDate < (6 * 24 * 60 * 60 * 1000)) { + // After last week started, show day of week + dateTimeCompact = typeof Intl === "undefined" + ? aDate.toLocaleFormat("%A") + : aDate.toLocaleDateString(locale, { weekday: "long" }); + } else { + // Show month/day + let month = typeof Intl === "undefined" + ? aDate.toLocaleFormat("%B") + : aDate.toLocaleDateString(locale, { month: "long" }); + let date = aDate.getDate(); + dateTimeCompact = gBundle.formatStringFromName(gStr.monthDate, [month, date], 2); + } + + let dateTimeFull = dts.FormatDateTime("", + dts.dateFormatLong, + dts.timeFormatNoSeconds, + aDate.getFullYear(), + aDate.getMonth() + 1, + aDate.getDate(), + aDate.getHours(), + aDate.getMinutes(), + 0); + + return [dateTimeCompact, dateTimeFull]; + }, + + /** + * Get the appropriate display host string for a URI string depending on if + * the URI has an eTLD + 1, is an IP address, a local file, or other protocol + * + * @param aURIString + * The URI string to try getting an eTLD + 1, etc. + * @return A pair: [display host for the URI string, full host name] + */ + getURIHost: function DU_getURIHost(aURIString) + { + let ioService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + let eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"]. + getService(Ci.nsIEffectiveTLDService); + let idnService = Cc["@mozilla.org/network/idn-service;1"]. + getService(Ci.nsIIDNService); + + // Get a URI that knows about its components + let uri; + try { + uri = ioService.newURI(aURIString, null, null); + } catch (ex) { + return ["", ""]; + } + + // Get the inner-most uri for schemes like jar: + if (uri instanceof Ci.nsINestedURI) + uri = uri.innermostURI; + + let fullHost; + try { + // Get the full host name; some special URIs fail (data: jar:) + fullHost = uri.host; + } catch (e) { + fullHost = ""; + } + + let displayHost; + try { + // This might fail if it's an IP address or doesn't have more than 1 part + let baseDomain = eTLDService.getBaseDomain(uri); + + // Convert base domain for display; ignore the isAscii out param + displayHost = idnService.convertToDisplayIDN(baseDomain, {}); + } catch (e) { + // Default to the host name + displayHost = fullHost; + } + + // Check if we need to show something else for the host + if (uri.scheme == "file") { + // Display special text for file protocol + displayHost = gBundle.GetStringFromName(gStr.doneFileScheme); + fullHost = displayHost; + } else if (displayHost.length == 0) { + // Got nothing; show the scheme (data: about: moz-icon:) + displayHost = + gBundle.formatStringFromName(gStr.doneScheme, [uri.scheme], 1); + fullHost = displayHost; + } else if (uri.port != -1) { + // Tack on the port if it's not the default port + let port = ":" + uri.port; + displayHost += port; + fullHost += port; + } + + return [displayHost, fullHost]; + }, + + /** + * Converts a number of bytes to the appropriate unit that results in an + * internationalized number that needs fewer than 4 digits. + * + * @param aBytes + * Number of bytes to convert + * @return A pair: [new value with 3 sig. figs., its unit] + */ + convertByteUnits: function DU_convertByteUnits(aBytes) + { + let unitIndex = 0; + + // Convert to next unit if it needs 4 digits (after rounding), but only if + // we know the name of the next unit + while ((aBytes >= 999.5) && (unitIndex < gStr.units.length - 1)) { + aBytes /= 1024; + unitIndex++; + } + + // Get rid of insignificant bits by truncating to 1 or 0 decimal points + // 0 -> 0; 1.2 -> 1.2; 12.3 -> 12.3; 123.4 -> 123; 234.5 -> 235 + // added in bug 462064: (unitIndex != 0) makes sure that no decimal digit for bytes appears when aBytes < 100 + let fractionDigits = (aBytes > 0) && (aBytes < 100) && (unitIndex != 0) ? 1 : 0; + + // Don't try to format Infinity values using NumberFormat. + if (aBytes === Infinity) { + aBytes = "Infinity"; + } else if (typeof Intl != "undefined") { + aBytes = getLocaleNumberFormat(fractionDigits) + .format(aBytes); + } else { + // FIXME: Fall back to the old hack, will be fixed in bug 1200494. + aBytes = aBytes.toFixed(fractionDigits); + if (gDecimalSymbol != ".") { + aBytes = aBytes.replace(".", gDecimalSymbol); + } + } + + return [aBytes, gBundle.GetStringFromName(gStr.units[unitIndex])]; + }, + + /** + * Converts a number of seconds to the two largest units. Time values are + * whole numbers, and units have the correct plural/singular form. + * + * @param aSecs + * Seconds to convert into the appropriate 2 units + * @return 4-item array [first value, its unit, second value, its unit] + */ + convertTimeUnits: function DU_convertTimeUnits(aSecs) + { + // These are the maximum values for seconds, minutes, hours corresponding + // with gStr.timeUnits without the last item + let timeSize = [60, 60, 24]; + + let time = aSecs; + let scale = 1; + let unitIndex = 0; + + // Keep converting to the next unit while we have units left and the + // current one isn't the largest unit possible + while ((unitIndex < timeSize.length) && (time >= timeSize[unitIndex])) { + time /= timeSize[unitIndex]; + scale *= timeSize[unitIndex]; + unitIndex++; + } + + let value = convertTimeUnitsValue(time); + let units = convertTimeUnitsUnits(value, unitIndex); + + let extra = aSecs - value * scale; + let nextIndex = unitIndex - 1; + + // Convert the extra time to the next largest unit + for (let index = 0; index < nextIndex; index++) + extra /= timeSize[index]; + + let value2 = convertTimeUnitsValue(extra); + let units2 = convertTimeUnitsUnits(value2, nextIndex); + + return [value, units, value2, units2]; + }, +}; + +/** + * Private helper for convertTimeUnits that gets the display value of a time + * + * @param aTime + * Time value for display + * @return An integer value for the time rounded down + */ +function convertTimeUnitsValue(aTime) +{ + return Math.floor(aTime); +} + +/** + * Private helper for convertTimeUnits that gets the display units of a time + * + * @param aTime + * Time value for display + * @param aIndex + * Index into gStr.timeUnits for the appropriate unit + * @return The appropriate plural form of the unit for the time + */ +function convertTimeUnitsUnits(aTime, aIndex) +{ + // Negative index would be an invalid unit, so just give empty + if (aIndex < 0) + return ""; + + return PluralForm.get(aTime, gBundle.GetStringFromName(gStr.timeUnits[aIndex])); +} + +/** + * Private helper function to log errors to the error console and command line + * + * @param aMsg + * Error message to log or an array of strings to concat + */ +function log(aMsg) +{ + let msg = "DownloadUtils.jsm: " + (aMsg.join ? aMsg.join("") : aMsg); + Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService). + logStringMessage(msg); + dump(msg + "\n"); +} diff --git a/toolkit/mozapps/downloads/content/DownloadProgressListener.js b/toolkit/mozapps/downloads/content/DownloadProgressListener.js new file mode 100644 index 000000000..ab349baf2 --- /dev/null +++ b/toolkit/mozapps/downloads/content/DownloadProgressListener.js @@ -0,0 +1,117 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + +/* 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/. */ + +/** + * DownloadProgressListener "class" is used to help update download items shown + * in the Download Manager UI such as displaying amount transferred, transfer + * rate, and time left for each download. + * + * This class implements the nsIDownloadProgressListener interface. + */ +function DownloadProgressListener() {} + +DownloadProgressListener.prototype = { + // nsISupports + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDownloadProgressListener]), + + // nsIDownloadProgressListener + + onDownloadStateChange: function dlPL_onDownloadStateChange(aState, aDownload) + { + // Update window title in-case we don't get all progress notifications + onUpdateProgress(); + + let dl; + let state = aDownload.state; + switch (state) { + case nsIDM.DOWNLOAD_QUEUED: + prependList(aDownload); + break; + + case nsIDM.DOWNLOAD_BLOCKED_POLICY: + prependList(aDownload); + // Should fall through, this is a final state but DOWNLOAD_QUEUED + // is skipped. See nsDownloadManager::AddDownload. + case nsIDM.DOWNLOAD_FAILED: + case nsIDM.DOWNLOAD_CANCELED: + case nsIDM.DOWNLOAD_BLOCKED_PARENTAL: + case nsIDM.DOWNLOAD_DIRTY: + case nsIDM.DOWNLOAD_FINISHED: + downloadCompleted(aDownload); + if (state == nsIDM.DOWNLOAD_FINISHED) + autoRemoveAndClose(aDownload); + break; + case nsIDM.DOWNLOAD_DOWNLOADING: { + dl = getDownload(aDownload.id); + + // At this point, we know if we are an indeterminate download or not + dl.setAttribute("progressmode", aDownload.percentComplete == -1 ? + "undetermined" : "normal"); + + // As well as knowing the referrer + let referrer = aDownload.referrer; + if (referrer) + dl.setAttribute("referrer", referrer.spec); + + break; + } + } + + // autoRemoveAndClose could have already closed our window... + try { + if (!dl) + dl = getDownload(aDownload.id); + + // Update to the new state + dl.setAttribute("state", state); + + // Update ui text values after switching states + updateTime(dl); + updateStatus(dl); + updateButtons(dl); + } catch (e) { } + }, + + onProgressChange: function dlPL_onProgressChange(aWebProgress, aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress, aDownload) + { + var download = getDownload(aDownload.id); + + // Update this download's progressmeter + if (aDownload.percentComplete != -1) { + download.setAttribute("progress", aDownload.percentComplete); + + // Dispatch ValueChange for a11y + let event = document.createEvent("Events"); + event.initEvent("ValueChange", true, true); + document.getAnonymousElementByAttribute(download, "anonid", "progressmeter") + .dispatchEvent(event); + } + + // Update the progress so the status can be correctly updated + download.setAttribute("currBytes", aDownload.amountTransferred); + download.setAttribute("maxBytes", aDownload.size); + + // Update the rest of the UI (bytes transferred, bytes total, download rate, + // time remaining). + updateStatus(download, aDownload); + + // Update window title + onUpdateProgress(); + }, + + onStateChange: function(aWebProgress, aRequest, aState, aStatus, aDownload) + { + }, + + onSecurityChange: function(aWebProgress, aRequest, aState, aDownload) + { + } +}; diff --git a/toolkit/mozapps/downloads/content/download.xml b/toolkit/mozapps/downloads/content/download.xml new file mode 100644 index 000000000..1d4b87270 --- /dev/null +++ b/toolkit/mozapps/downloads/content/download.xml @@ -0,0 +1,327 @@ + + + + + + %downloadDTD; +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/toolkit/mozapps/downloads/content/downloads.css b/toolkit/mozapps/downloads/content/downloads.css new file mode 100644 index 000000000..dcb648d62 --- /dev/null +++ b/toolkit/mozapps/downloads/content/downloads.css @@ -0,0 +1,50 @@ +/* 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/. */ + +richlistitem[type="download"] { + -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-starting'); + -moz-box-orient: vertical; +} + +richlistitem[type="download"][state="0"] { + -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-downloading'); +} + +richlistitem[type="download"][state="1"] { + -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-done'); +} + +richlistitem[type="download"][state="2"] { + -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-failed'); +} + +richlistitem[type="download"][state="3"] { + -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-canceled'); +} + +richlistitem[type="download"][state="4"] { + -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-paused'); +} + +richlistitem[type="download"][state="6"] { + -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-blocked-parental'); +} + +richlistitem[type="download"][state="7"] { + -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-scanning'); +} + +richlistitem[type="download"][state="8"] { + -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-dirty'); +} + +richlistitem[type="download"][state="9"] { + -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-blocked-policy'); +} + +/* Only focus buttons in the selected item*/ +richlistitem[type="download"]:not([selected="true"]) button { + -moz-user-focus: none; +} + diff --git a/toolkit/mozapps/downloads/content/downloads.js b/toolkit/mozapps/downloads/content/downloads.js new file mode 100644 index 000000000..92a9f7593 --- /dev/null +++ b/toolkit/mozapps/downloads/content/downloads.js @@ -0,0 +1,1320 @@ +/* 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_ALERTONEXEOPEN = "browser.download.manager.alertOnEXEOpen"; +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", + fileExecutableSecurityWarningDontAsk: "fileExecutableSecurityWarningDontAsk" +}; + +// 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_ALERTONEXEOPEN); + } 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; + let dontAsk = gStr.fileExecutableSecurityWarningDontAsk; + + var promptSvc = Cc["@mozilla.org/embedcomp/prompt-service;1"]. + getService(Ci.nsIPromptService); + var checkbox = { value: false }; + var open = promptSvc.confirmCheck(window, title, message, dontAsk, checkbox); + + if (!open) + return; + pref.setBoolPref(PREF_BDM_ALERTONEXEOPEN, !checkbox.value); + } + } + 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"); +} diff --git a/toolkit/mozapps/downloads/content/downloads.xul b/toolkit/mozapps/downloads/content/downloads.xul new file mode 100644 index 000000000..5ca9eec2d --- /dev/null +++ b/toolkit/mozapps/downloads/content/downloads.xul @@ -0,0 +1,164 @@ + + +# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- +# 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/. + +#ifdef XP_UNIX +#ifndef XP_MACOSX +#define XP_GNOME 1 +#endif +#endif + + + + + + +%downloadManagerDTD; + +%editMenuOverlayDTD; +]> + + + + + + +

+ +

+  
+
+  
+
diff --git a/toolkit/mozapps/downloads/tests/chrome/test_unknownContentType_dialog_layout.xul b/toolkit/mozapps/downloads/tests/chrome/test_unknownContentType_dialog_layout.xul new file mode 100644 index 000000000..1210b908d --- /dev/null +++ b/toolkit/mozapps/downloads/tests/chrome/test_unknownContentType_dialog_layout.xul @@ -0,0 +1,108 @@ + + + + + + + + + +

+ +

+  
+
+  
+
diff --git a/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.pif b/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.pif new file mode 100644 index 000000000..9353d1312 --- /dev/null +++ b/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.pif @@ -0,0 +1 @@ +Dummy content for unknownContentType_dialog_layout_data.pif diff --git a/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.pif^headers^ b/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.pif^headers^ new file mode 100644 index 000000000..09b22facc --- /dev/null +++ b/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.pif^headers^ @@ -0,0 +1 @@ +Content-Type: application/octet-stream diff --git a/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.txt b/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.txt new file mode 100644 index 000000000..77e719559 --- /dev/null +++ b/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.txt @@ -0,0 +1 @@ +Dummy content for unknownContentType_dialog_layout_data.txt diff --git a/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.txt^headers^ b/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.txt^headers^ new file mode 100644 index 000000000..2a3c472e2 --- /dev/null +++ b/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.txt^headers^ @@ -0,0 +1,2 @@ +Content-Type: text/plain +Content-Disposition: attachment diff --git a/toolkit/mozapps/downloads/tests/moz.build b/toolkit/mozapps/downloads/tests/moz.build new file mode 100644 index 000000000..a4b1efb9a --- /dev/null +++ b/toolkit/mozapps/downloads/tests/moz.build @@ -0,0 +1,8 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini'] +MOCHITEST_CHROME_MANIFESTS += ['chrome/chrome.ini'] diff --git a/toolkit/mozapps/downloads/tests/unit/.eslintrc.js b/toolkit/mozapps/downloads/tests/unit/.eslintrc.js new file mode 100644 index 000000000..d35787cd2 --- /dev/null +++ b/toolkit/mozapps/downloads/tests/unit/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/toolkit/mozapps/downloads/tests/unit/head_downloads.js b/toolkit/mozapps/downloads/tests/unit/head_downloads.js new file mode 100644 index 000000000..4f199e5cf --- /dev/null +++ b/toolkit/mozapps/downloads/tests/unit/head_downloads.js @@ -0,0 +1,5 @@ +Components.utils.import("resource://gre/modules/Services.jsm"); + +do_register_cleanup(function() { + Services.obs.notifyObservers(null, "quit-application", null); +}); diff --git a/toolkit/mozapps/downloads/tests/unit/test_DownloadPaths.js b/toolkit/mozapps/downloads/tests/unit/test_DownloadPaths.js new file mode 100644 index 000000000..77249169d --- /dev/null +++ b/toolkit/mozapps/downloads/tests/unit/test_DownloadPaths.js @@ -0,0 +1,131 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ +/* ***** BEGIN LICENSE BLOCK ***** + * + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * ***** END LICENSE BLOCK ***** */ + +/** + * Tests for the "DownloadPaths.jsm" JavaScript module. + */ + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cr = Components.results; + +Cu.import("resource://gre/modules/DownloadPaths.jsm"); + +/** + * Provides a temporary save directory. + * + * @returns nsIFile pointing to the new or existing directory. + */ +function createTemporarySaveDirectory() +{ + var saveDir = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties).get("TmpD", Ci.nsIFile); + saveDir.append("testsavedir"); + if (!saveDir.exists()) { + saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + } + return saveDir; +} + +function testSplitBaseNameAndExtension(aLeafName, [aBase, aExt]) +{ + var [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName); + do_check_eq(base, aBase); + do_check_eq(ext, aExt); + + // If we modify the base name and concatenate it with the extension again, + // another roundtrip through the function should give a consistent result. + // The only exception is when we introduce an extension in a file name that + // didn't have one or that ended with one of the special cases like ".gz". If + // we avoid using a dot and we introduce at least another special character, + // the results are always consistent. + [base, ext] = DownloadPaths.splitBaseNameAndExtension("(" + base + ")" + ext); + do_check_eq(base, "(" + aBase + ")"); + do_check_eq(ext, aExt); +} + +function testCreateNiceUniqueFile(aTempFile, aExpectedLeafName) +{ + var createdFile = DownloadPaths.createNiceUniqueFile(aTempFile); + do_check_eq(createdFile.leafName, aExpectedLeafName); +} + +function run_test() +{ + // Usual file names. + testSplitBaseNameAndExtension("base", ["base", ""]); + testSplitBaseNameAndExtension("base.ext", ["base", ".ext"]); + testSplitBaseNameAndExtension("base.application", ["base", ".application"]); + testSplitBaseNameAndExtension("base.x.Z", ["base", ".x.Z"]); + testSplitBaseNameAndExtension("base.ext.Z", ["base", ".ext.Z"]); + testSplitBaseNameAndExtension("base.ext.gz", ["base", ".ext.gz"]); + testSplitBaseNameAndExtension("base.ext.Bz2", ["base", ".ext.Bz2"]); + testSplitBaseNameAndExtension("base..ext", ["base.", ".ext"]); + testSplitBaseNameAndExtension("base..Z", ["base.", ".Z"]); + testSplitBaseNameAndExtension("base. .Z", ["base. ", ".Z"]); + testSplitBaseNameAndExtension("base.base.Bz2", ["base.base", ".Bz2"]); + testSplitBaseNameAndExtension("base .ext", ["base ", ".ext"]); + + // Corner cases. A name ending with a dot technically has no extension, but + // we consider the ending dot separately from the base name so that modifying + // the latter never results in an extension being introduced accidentally. + // Names beginning with a dot are hidden files on Unix-like platforms and if + // their name doesn't contain another dot they should have no extension, but + // on Windows the whole name is considered as an extension. + testSplitBaseNameAndExtension("base.", ["base", "."]); + testSplitBaseNameAndExtension(".ext", ["", ".ext"]); + + // Unusual file names (not recommended as input to the function). + testSplitBaseNameAndExtension("base. ", ["base", ". "]); + testSplitBaseNameAndExtension("base ", ["base ", ""]); + testSplitBaseNameAndExtension("", ["", ""]); + testSplitBaseNameAndExtension(" ", [" ", ""]); + testSplitBaseNameAndExtension(" . ", [" ", ". "]); + testSplitBaseNameAndExtension(" .. ", [" .", ". "]); + testSplitBaseNameAndExtension(" .ext", [" ", ".ext"]); + testSplitBaseNameAndExtension(" .ext. ", [" .ext", ". "]); + testSplitBaseNameAndExtension(" .ext.gz ", [" .ext", ".gz "]); + + var destDir = createTemporarySaveDirectory(); + try { + // Single extension. + var tempFile = destDir.clone(); + tempFile.append("test.txt"); + testCreateNiceUniqueFile(tempFile, "test.txt"); + testCreateNiceUniqueFile(tempFile, "test(1).txt"); + testCreateNiceUniqueFile(tempFile, "test(2).txt"); + + // Double extension. + tempFile.leafName = "test.tar.gz"; + testCreateNiceUniqueFile(tempFile, "test.tar.gz"); + testCreateNiceUniqueFile(tempFile, "test(1).tar.gz"); + testCreateNiceUniqueFile(tempFile, "test(2).tar.gz"); + + // Test automatic shortening of long file names. We don't know exactly how + // many characters are removed, because it depends on the name of the folder + // where the file is located. + tempFile.leafName = new Array(256).join("T") + ".txt"; + var newFile = DownloadPaths.createNiceUniqueFile(tempFile); + do_check_true(newFile.leafName.length < tempFile.leafName.length); + do_check_eq(newFile.leafName.slice(-4), ".txt"); + + // Creating a valid file name from an invalid one is not always possible. + tempFile.append("file-under-long-directory.txt"); + try { + DownloadPaths.createNiceUniqueFile(tempFile); + do_throw("Exception expected with a long parent directory name.") + } catch (e) { + // An exception is expected, but we don't know which one exactly. + } + } finally { + // Clean up the temporary directory. + destDir.remove(true); + } +} diff --git a/toolkit/mozapps/downloads/tests/unit/test_DownloadUtils.js b/toolkit/mozapps/downloads/tests/unit/test_DownloadUtils.js new file mode 100644 index 000000000..11e7776a7 --- /dev/null +++ b/toolkit/mozapps/downloads/tests/unit/test_DownloadUtils.js @@ -0,0 +1,237 @@ +/* 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/. */ + +var Cu = Components.utils; +Cu.import("resource://gre/modules/DownloadUtils.jsm"); + +const gDecimalSymbol = Number(5.4).toLocaleString().match(/\D/); +function _(str) { + return str.replace(/\./g, gDecimalSymbol); +} + +function testConvertByteUnits(aBytes, aValue, aUnit) +{ + let [value, unit] = DownloadUtils.convertByteUnits(aBytes); + do_check_eq(value, aValue); + do_check_eq(unit, aUnit); +} + +function testTransferTotal(aCurrBytes, aMaxBytes, aTransfer) +{ + let transfer = DownloadUtils.getTransferTotal(aCurrBytes, aMaxBytes); + do_check_eq(transfer, aTransfer); +} + +// Get the em-dash character because typing it directly here doesn't work :( +var gDash = DownloadUtils.getDownloadStatus(0)[0].match(/remaining (.) 0 bytes/)[1]; + +var gVals = [0, 100, 2345, 55555, 982341, 23194134, 1482, 58, 9921949201, 13498132, Infinity]; + +function testStatus(aFunc, aCurr, aMore, aRate, aTest) +{ + dump("Status Test: " + [aCurr, aMore, aRate, aTest] + "\n"); + let curr = gVals[aCurr]; + let max = curr + gVals[aMore]; + let speed = gVals[aRate]; + + let [status, last] = aFunc(curr, max, speed); + + if (0) { + dump("testStatus(" + aCurr + ", " + aMore + ", " + aRate + ", [\"" + + status.replace(gDash, "--") + "\", " + last.toFixed(3) + "]);\n"); + } + + // Make sure the status text matches + do_check_eq(status, _(aTest[0].replace(/--/, gDash))); + + // Make sure the lastSeconds matches + if (last == Infinity) + do_check_eq(last, aTest[1]); + else + do_check_true(Math.abs(last - aTest[1]) < .1); +} + +function testURI(aURI, aDisp, aHost) +{ + dump("URI Test: " + [aURI, aDisp, aHost] + "\n"); + + let [disp, host] = DownloadUtils.getURIHost(aURI); + + // Make sure we have the right display host and full host + do_check_eq(disp, aDisp); + do_check_eq(host, aHost); +} + + +function testGetReadableDates(aDate, aCompactValue) +{ + const now = new Date(2000, 11, 31, 11, 59, 59); + + let [dateCompact] = DownloadUtils.getReadableDates(aDate, now); + do_check_eq(dateCompact, aCompactValue); +} + +function testAllGetReadableDates() +{ + // This test cannot depend on the current date and time, or the date format. + // It depends on being run with the English localization, however. + const today_11_30 = new Date(2000, 11, 31, 11, 30, 15); + const today_12_30 = new Date(2000, 11, 31, 12, 30, 15); + const yesterday_11_30 = new Date(2000, 11, 30, 11, 30, 15); + const yesterday_12_30 = new Date(2000, 11, 30, 12, 30, 15); + const twodaysago = new Date(2000, 11, 29, 11, 30, 15); + const sixdaysago = new Date(2000, 11, 25, 11, 30, 15); + const sevendaysago = new Date(2000, 11, 24, 11, 30, 15); + + // TODO: Remove Intl fallback when bug 1215247 is fixed. + const locale = typeof Intl === "undefined" + ? undefined + : Components.classes["@mozilla.org/chrome/chrome-registry;1"] + .getService(Components.interfaces.nsIXULChromeRegistry) + .getSelectedLocale("global", true); + + let dts = Components.classes["@mozilla.org/intl/scriptabledateformat;1"]. + getService(Components.interfaces.nsIScriptableDateFormat); + + testGetReadableDates(today_11_30, dts.FormatTime("", dts.timeFormatNoSeconds, + 11, 30, 0)); + testGetReadableDates(today_12_30, dts.FormatTime("", dts.timeFormatNoSeconds, + 12, 30, 0)); + testGetReadableDates(yesterday_11_30, "Yesterday"); + testGetReadableDates(yesterday_12_30, "Yesterday"); + testGetReadableDates(twodaysago, + typeof Intl === "undefined" + ? twodaysago.toLocaleFormat("%A") + : twodaysago.toLocaleDateString(locale, { weekday: "long" })); + testGetReadableDates(sixdaysago, + typeof Intl === "undefined" + ? sixdaysago.toLocaleFormat("%A") + : sixdaysago.toLocaleDateString(locale, { weekday: "long" })); + testGetReadableDates(sevendaysago, + (typeof Intl === "undefined" + ? sevendaysago.toLocaleFormat("%B") + : sevendaysago.toLocaleDateString(locale, { month: "long" })) + " " + + sevendaysago.getDate().toString().padStart(2, "0")); + + let [, dateTimeFull] = DownloadUtils.getReadableDates(today_11_30); + do_check_eq(dateTimeFull, dts.FormatDateTime("", dts.dateFormatLong, + dts.timeFormatNoSeconds, + 2000, 12, 31, 11, 30, 0)); +} + +function run_test() +{ + testConvertByteUnits(-1, "-1", "bytes"); + testConvertByteUnits(1, _("1"), "bytes"); + testConvertByteUnits(42, _("42"), "bytes"); + testConvertByteUnits(123, _("123"), "bytes"); + testConvertByteUnits(1024, _("1.0"), "KB"); + testConvertByteUnits(8888, _("8.7"), "KB"); + testConvertByteUnits(59283, _("57.9"), "KB"); + testConvertByteUnits(640000, _("625"), "KB"); + testConvertByteUnits(1048576, _("1.0"), "MB"); + testConvertByteUnits(307232768, _("293"), "MB"); + testConvertByteUnits(1073741824, _("1.0"), "GB"); + + testTransferTotal(1, 1, _("1 of 1 bytes")); + testTransferTotal(234, 4924, _("234 bytes of 4.8 KB")); + testTransferTotal(94923, 233923, _("92.7 of 228 KB")); + testTransferTotal(4924, 94923, _("4.8 of 92.7 KB")); + testTransferTotal(2342, 294960345, _("2.3 KB of 281 MB")); + testTransferTotal(234, undefined, _("234 bytes")); + testTransferTotal(4889023, undefined, _("4.7 MB")); + + if (0) { + // Help find some interesting test cases + let r = () => Math.floor(Math.random() * 10); + for (let i = 0; i < 100; i++) { + testStatus(r(), r(), r()); + } + } + + // First, test with rates, via getDownloadStatus... + let statusFunc = DownloadUtils.getDownloadStatus.bind(DownloadUtils); + + testStatus(statusFunc, 2, 1, 7, ["A few seconds remaining -- 2.3 of 2.4 KB (58 bytes/sec)", 1.724]); + testStatus(statusFunc, 1, 2, 6, ["A few seconds remaining -- 100 bytes of 2.4 KB (1.4 KB/sec)", 1.582]); + testStatus(statusFunc, 4, 3, 9, ["A few seconds remaining -- 959 KB of 1.0 MB (12.9 MB/sec)", 0.004]); + testStatus(statusFunc, 2, 3, 8, ["A few seconds remaining -- 2.3 of 56.5 KB (9.2 GB/sec)", 0.000]); + + testStatus(statusFunc, 8, 4, 3, ["17 seconds remaining -- 9.2 of 9.2 GB (54.3 KB/sec)", 17.682]); + testStatus(statusFunc, 1, 3, 2, ["23 seconds remaining -- 100 bytes of 54.4 KB (2.3 KB/sec)", 23.691]); + testStatus(statusFunc, 9, 3, 2, ["23 seconds remaining -- 12.9 of 12.9 MB (2.3 KB/sec)", 23.691]); + testStatus(statusFunc, 5, 6, 7, ["25 seconds remaining -- 22.1 of 22.1 MB (58 bytes/sec)", 25.552]); + + testStatus(statusFunc, 3, 9, 3, ["4 minutes remaining -- 54.3 KB of 12.9 MB (54.3 KB/sec)", 242.969]); + testStatus(statusFunc, 2, 3, 1, ["9 minutes remaining -- 2.3 of 56.5 KB (100 bytes/sec)", 555.550]); + testStatus(statusFunc, 4, 3, 7, ["15 minutes remaining -- 959 KB of 1.0 MB (58 bytes/sec)", 957.845]); + testStatus(statusFunc, 5, 3, 7, ["15 minutes remaining -- 22.1 of 22.2 MB (58 bytes/sec)", 957.845]); + + testStatus(statusFunc, 1, 9, 2, ["1 hour, 35 minutes remaining -- 100 bytes of 12.9 MB (2.3 KB/sec)", 5756.133]); + testStatus(statusFunc, 2, 9, 6, ["2 hours, 31 minutes remaining -- 2.3 KB of 12.9 MB (1.4 KB/sec)", 9108.051]); + testStatus(statusFunc, 2, 4, 1, ["2 hours, 43 minutes remaining -- 2.3 of 962 KB (100 bytes/sec)", 9823.410]); + testStatus(statusFunc, 6, 4, 7, ["4 hours, 42 minutes remaining -- 1.4 of 961 KB (58 bytes/sec)", 16936.914]); + + testStatus(statusFunc, 6, 9, 1, ["1 day, 13 hours remaining -- 1.4 KB of 12.9 MB (100 bytes/sec)", 134981.320]); + testStatus(statusFunc, 3, 8, 3, ["2 days, 1 hour remaining -- 54.3 KB of 9.2 GB (54.3 KB/sec)", 178596.872]); + testStatus(statusFunc, 1, 8, 6, ["77 days, 11 hours remaining -- 100 bytes of 9.2 GB (1.4 KB/sec)", 6694972.470]); + testStatus(statusFunc, 6, 8, 7, ["1979 days, 22 hours remaining -- 1.4 KB of 9.2 GB (58 bytes/sec)", 171068089.672]); + + testStatus(statusFunc, 0, 0, 5, ["Unknown time remaining -- 0 of 0 bytes (22.1 MB/sec)", Infinity]); + testStatus(statusFunc, 0, 6, 0, ["Unknown time remaining -- 0 bytes of 1.4 KB (0 bytes/sec)", Infinity]); + testStatus(statusFunc, 6, 6, 0, ["Unknown time remaining -- 1.4 of 2.9 KB (0 bytes/sec)", Infinity]); + testStatus(statusFunc, 8, 5, 0, ["Unknown time remaining -- 9.2 of 9.3 GB (0 bytes/sec)", Infinity]); + + // With rate equal to Infinity + testStatus(statusFunc, 0, 0, 10, ["Unknown time remaining -- 0 of 0 bytes (Really fast)", Infinity]); + testStatus(statusFunc, 1, 2, 10, ["A few seconds remaining -- 100 bytes of 2.4 KB (Really fast)", 0]); + + // Now test without rates, via getDownloadStatusNoRate. + statusFunc = DownloadUtils.getDownloadStatusNoRate.bind(DownloadUtils); + + testStatus(statusFunc, 2, 1, 7, ["A few seconds remaining -- 2.3 of 2.4 KB", 1.724]); + testStatus(statusFunc, 1, 2, 6, ["A few seconds remaining -- 100 bytes of 2.4 KB", 1.582]); + testStatus(statusFunc, 4, 3, 9, ["A few seconds remaining -- 959 KB of 1.0 MB", 0.004]); + testStatus(statusFunc, 2, 3, 8, ["A few seconds remaining -- 2.3 of 56.5 KB", 0.000]); + + testStatus(statusFunc, 8, 4, 3, ["17 seconds remaining -- 9.2 of 9.2 GB", 17.682]); + testStatus(statusFunc, 1, 3, 2, ["23 seconds remaining -- 100 bytes of 54.4 KB", 23.691]); + testStatus(statusFunc, 9, 3, 2, ["23 seconds remaining -- 12.9 of 12.9 MB", 23.691]); + testStatus(statusFunc, 5, 6, 7, ["25 seconds remaining -- 22.1 of 22.1 MB", 25.552]); + + testStatus(statusFunc, 3, 9, 3, ["4 minutes remaining -- 54.3 KB of 12.9 MB", 242.969]); + testStatus(statusFunc, 2, 3, 1, ["9 minutes remaining -- 2.3 of 56.5 KB", 555.550]); + testStatus(statusFunc, 4, 3, 7, ["15 minutes remaining -- 959 KB of 1.0 MB", 957.845]); + testStatus(statusFunc, 5, 3, 7, ["15 minutes remaining -- 22.1 of 22.2 MB", 957.845]); + + testStatus(statusFunc, 1, 9, 2, ["1 hour, 35 minutes remaining -- 100 bytes of 12.9 MB", 5756.133]); + testStatus(statusFunc, 2, 9, 6, ["2 hours, 31 minutes remaining -- 2.3 KB of 12.9 MB", 9108.051]); + testStatus(statusFunc, 2, 4, 1, ["2 hours, 43 minutes remaining -- 2.3 of 962 KB", 9823.410]); + testStatus(statusFunc, 6, 4, 7, ["4 hours, 42 minutes remaining -- 1.4 of 961 KB", 16936.914]); + + testStatus(statusFunc, 6, 9, 1, ["1 day, 13 hours remaining -- 1.4 KB of 12.9 MB", 134981.320]); + testStatus(statusFunc, 3, 8, 3, ["2 days, 1 hour remaining -- 54.3 KB of 9.2 GB", 178596.872]); + testStatus(statusFunc, 1, 8, 6, ["77 days, 11 hours remaining -- 100 bytes of 9.2 GB", 6694972.470]); + testStatus(statusFunc, 6, 8, 7, ["1979 days, 22 hours remaining -- 1.4 KB of 9.2 GB", 171068089.672]); + + testStatus(statusFunc, 0, 0, 5, ["Unknown time remaining -- 0 of 0 bytes", Infinity]); + testStatus(statusFunc, 0, 6, 0, ["Unknown time remaining -- 0 bytes of 1.4 KB", Infinity]); + testStatus(statusFunc, 6, 6, 0, ["Unknown time remaining -- 1.4 of 2.9 KB", Infinity]); + testStatus(statusFunc, 8, 5, 0, ["Unknown time remaining -- 9.2 of 9.3 GB", Infinity]); + + testURI("http://www.mozilla.org/", "mozilla.org", "www.mozilla.org"); + testURI("http://www.city.mikasa.hokkaido.jp/", "city.mikasa.hokkaido.jp", "www.city.mikasa.hokkaido.jp"); + testURI("data:text/html,Hello World", "data resource", "data resource"); + testURI("jar:http://www.mozilla.com/file!/magic", "mozilla.com", "www.mozilla.com"); + testURI("file:///C:/Cool/Stuff/", "local file", "local file"); + // Don't test for moz-icon if we don't have a protocol handler for it (e.g. b2g): + if ("@mozilla.org/network/protocol;1?name=moz-icon" in Components.classes) { + testURI("moz-icon:file:///test.extension", "local file", "local file"); + testURI("moz-icon://.extension", "moz-icon resource", "moz-icon resource"); + } + testURI("about:config", "about resource", "about resource"); + testURI("invalid.uri", "", ""); + + testAllGetReadableDates(); +} diff --git a/toolkit/mozapps/downloads/tests/unit/test_lowMinutes.js b/toolkit/mozapps/downloads/tests/unit/test_lowMinutes.js new file mode 100644 index 000000000..75eff3370 --- /dev/null +++ b/toolkit/mozapps/downloads/tests/unit/test_lowMinutes.js @@ -0,0 +1,55 @@ +/* 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/. */ + +/** + * Test bug 448344 to make sure when we're in low minutes, we show both minutes + * and seconds; but continue to show only minutes when we have plenty. + */ + +Components.utils.import("resource://gre/modules/DownloadUtils.jsm"); + +/** + * Print some debug message to the console. All arguments will be printed, + * separated by spaces. + * + * @param [arg0, arg1, arg2, ...] + * Any number of arguments to print out + * @usage _("Hello World") -> prints "Hello World" + * @usage _(1, 2, 3) -> prints "1 2 3" + */ +var _ = function(some, debug, text, to) { + print(Array.slice(arguments).join(" ")); +}; + +_("Make an array of time lefts and expected string to be shown for that time"); +var expectedTimes = [ + [1.1, "A few seconds remaining", "under 4sec -> few"], + [2.5, "A few seconds remaining", "under 4sec -> few"], + [3.9, "A few seconds remaining", "under 4sec -> few"], + [5.3, "5 seconds remaining", "truncate seconds"], + [1.1 * 60, "1 minute, 6 seconds remaining", "under 4min -> show sec"], + [2.5 * 60, "2 minutes, 30 seconds remaining", "under 4min -> show sec"], + [3.9 * 60, "3 minutes, 54 seconds remaining", "under 4min -> show sec"], + [5.3 * 60, "5 minutes remaining", "over 4min -> only show min"], + [1.1 * 3600, "1 hour, 6 minutes remaining", "over 1hr -> show min/sec"], + [2.5 * 3600, "2 hours, 30 minutes remaining", "over 1hr -> show min/sec"], + [3.9 * 3600, "3 hours, 54 minutes remaining", "over 1hr -> show min/sec"], + [5.3 * 3600, "5 hours, 18 minutes remaining", "over 1hr -> show min/sec"], +]; +_(expectedTimes.join("\n")); + +function run_test() +{ + expectedTimes.forEach(function([time, expectStatus, comment]) { + _("Running test with time", time); + _("Test comment:", comment); + let [status, last] = DownloadUtils.getTimeLeft(time); + + _("Got status:", status, "last:", last); + _("Expecting..", expectStatus); + do_check_eq(status, expectStatus); + + _(); + }); +} diff --git a/toolkit/mozapps/downloads/tests/unit/test_syncedDownloadUtils.js b/toolkit/mozapps/downloads/tests/unit/test_syncedDownloadUtils.js new file mode 100644 index 000000000..86d810a9b --- /dev/null +++ b/toolkit/mozapps/downloads/tests/unit/test_syncedDownloadUtils.js @@ -0,0 +1,26 @@ +/* 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/. */ + +/** + * Test bug 420482 by making sure multiple consumers of DownloadUtils gets the + * same time remaining time if they provide the same time left but a different + * "last time". + */ + +var Cu = Components.utils; +Cu.import("resource://gre/modules/DownloadUtils.jsm"); + +function run_test() +{ + // Simulate having multiple downloads requesting time left + let downloadTimes = {}; + for (let time of [1, 30, 60, 3456, 9999]) + downloadTimes[time] = DownloadUtils.getTimeLeft(time)[0]; + + // Pretend we're a download status bar also asking for a time left, but we're + // using a different "last sec". We need to make sure we get the same time. + let lastSec = 314; + for (let [time, text] of Object.entries(downloadTimes)) + do_check_eq(DownloadUtils.getTimeLeft(time, lastSec)[0], text); +} diff --git a/toolkit/mozapps/downloads/tests/unit/test_unspecified_arguments.js b/toolkit/mozapps/downloads/tests/unit/test_unspecified_arguments.js new file mode 100644 index 000000000..02e27c92c --- /dev/null +++ b/toolkit/mozapps/downloads/tests/unit/test_unspecified_arguments.js @@ -0,0 +1,25 @@ +/* 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/. */ + +/** + * Make sure passing null and nothing to various variable-arg DownloadUtils + * methods provide the same result. + */ + +var Cu = Components.utils; +Cu.import("resource://gre/modules/DownloadUtils.jsm"); + +function run_test() +{ + do_check_eq(DownloadUtils.getDownloadStatus(1000, null, null, null) + "", + DownloadUtils.getDownloadStatus(1000) + ""); + do_check_eq(DownloadUtils.getDownloadStatus(1000, null, null) + "", + DownloadUtils.getDownloadStatus(1000, null) + ""); + + do_check_eq(DownloadUtils.getTransferTotal(1000, null) + "", + DownloadUtils.getTransferTotal(1000) + ""); + + do_check_eq(DownloadUtils.getTimeLeft(1000, null) + "", + DownloadUtils.getTimeLeft(1000) + ""); +} diff --git a/toolkit/mozapps/downloads/tests/unit/xpcshell.ini b/toolkit/mozapps/downloads/tests/unit/xpcshell.ini new file mode 100644 index 000000000..877816ef6 --- /dev/null +++ b/toolkit/mozapps/downloads/tests/unit/xpcshell.ini @@ -0,0 +1,10 @@ +[DEFAULT] +head = head_downloads.js +tail = +skip-if = toolkit == 'android' + +[test_DownloadPaths.js] +[test_DownloadUtils.js] +[test_lowMinutes.js] +[test_syncedDownloadUtils.js] +[test_unspecified_arguments.js] -- cgit v1.2.3