diff options
Diffstat (limited to 'toolkit/mozapps/downloads')
31 files changed, 5304 insertions, 0 deletions
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 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<!DOCTYPE bindings [ + <!ENTITY % downloadDTD SYSTEM "chrome://mozapps/locale/downloads/downloads.dtd" > + %downloadDTD; +]> + +<bindings id="downloadBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="download-base" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem"> + <resources> + <stylesheet src="chrome://mozapps/skin/downloads/downloads.css"/> + </resources> + <implementation> + <property name="paused"> + <getter> + <![CDATA[ + return parseInt(this.getAttribute("state")) == Components.interfaces.nsIDownloadManager.DOWNLOAD_PAUSED; + ]]> + </getter> + </property> + <property name="openable"> + <getter> + <![CDATA[ + return parseInt(this.getAttribute("state")) == Components.interfaces.nsIDownloadManager.DOWNLOAD_FINISHED; + ]]> + </getter> + </property> + <property name="inProgress"> + <getter> + <![CDATA[ + var state = parseInt(this.getAttribute("state")); + const dl = Components.interfaces.nsIDownloadManager; + return state == dl.DOWNLOAD_NOTSTARTED || + state == dl.DOWNLOAD_QUEUED || + state == dl.DOWNLOAD_DOWNLOADING || + state == dl.DOWNLOAD_PAUSED || + state == dl.DOWNLOAD_SCANNING; + ]]> + </getter> + </property> + <property name="removable"> + <getter> + <![CDATA[ + var state = parseInt(this.getAttribute("state")); + const dl = Components.interfaces.nsIDownloadManager; + return state == dl.DOWNLOAD_FINISHED || + state == dl.DOWNLOAD_CANCELED || + state == dl.DOWNLOAD_BLOCKED_PARENTAL || + state == dl.DOWNLOAD_BLOCKED_POLICY || + state == dl.DOWNLOAD_DIRTY || + state == dl.DOWNLOAD_FAILED; + ]]> + </getter> + </property> + <property name="buttons"> + <getter> + <![CDATA[ + var startEl = document.getAnonymousNodes(this); + if (!startEl.length) + startEl = [this]; + + const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + return startEl[0].getElementsByTagNameNS(XULNS, "button"); + ]]> + </getter> + </property> + </implementation> + </binding> + + <binding id="download-starting" extends="chrome://mozapps/content/downloads/download.xml#download-base"> + <content> + <xul:hbox flex="1"> + <xul:vbox pack="center"> + <xul:image class="downloadTypeIcon" validate="always" + xbl:inherits="src=image"/> + </xul:vbox> + <xul:vbox pack="start" flex="1"> + <xul:label xbl:inherits="value=target,tooltiptext=target" + crop="center" class="name"/> + <xul:progressmeter mode="normal" value="0" flex="1" + anonid="progressmeter"/> + <xul:label value="&starting.label;" class="status"/> + <xul:spacer flex="1"/> + </xul:vbox> + <xul:vbox pack="center"> + <xul:button class="cancel mini-button" tooltiptext="&cmd.cancel.label;" + cmd="cmd_cancel" ondblclick="event.stopPropagation();" + oncommand="performCommand('cmd_cancel', this);"/> + </xul:vbox> + </xul:hbox> + </content> + </binding> + + <binding id="download-downloading" extends="chrome://mozapps/content/downloads/download.xml#download-base"> + <content> + <xul:hbox flex="1" class="downloadContentBox"> + <xul:vbox pack="center"> + <xul:image class="downloadTypeIcon" validate="always" + xbl:inherits="src=image"/> + </xul:vbox> + <xul:vbox flex="1"> + <xul:label xbl:inherits="value=target,tooltiptext=target" + crop="center" flex="2" class="name"/> + <xul:hbox> + <xul:vbox flex="1"> + <xul:progressmeter mode="normal" value="0" flex="1" + anonid="progressmeter" + xbl:inherits="value=progress,mode=progressmode"/> + </xul:vbox> + <xul:button class="pause mini-button" tooltiptext="&cmd.pause.label;" + cmd="cmd_pause" ondblclick="event.stopPropagation();" + oncommand="performCommand('cmd_pause', this);"/> + <xul:button class="cancel mini-button" tooltiptext="&cmd.cancel.label;" + cmd="cmd_cancel" ondblclick="event.stopPropagation();" + oncommand="performCommand('cmd_cancel', this);"/> + </xul:hbox> + <xul:label xbl:inherits="value=status,tooltiptext=statusTip" flex="1" + crop="right" class="status"/> + <xul:spacer flex="1"/> + </xul:vbox> + </xul:hbox> + </content> + </binding> + + <binding id="download-paused" extends="chrome://mozapps/content/downloads/download.xml#download-base"> + <content> + <xul:hbox flex="1"> + <xul:vbox pack="center"> + <xul:image class="downloadTypeIcon" validate="always" + xbl:inherits="src=image"/> + </xul:vbox> + <xul:vbox flex="1"> + <xul:label xbl:inherits="value=target,tooltiptext=target" + crop="center" flex="2" class="name"/> + <xul:hbox> + <xul:vbox flex="1"> + <xul:progressmeter mode="normal" value="0" flex="1" + anonid="progressmeter" + xbl:inherits="value=progress,mode=progressmode"/> + </xul:vbox> + <xul:button class="resume mini-button" tooltiptext="&cmd.resume.label;" + cmd="cmd_resume" ondblclick="event.stopPropagation();" + oncommand="performCommand('cmd_resume', this);"/> + <xul:button class="cancel mini-button" tooltiptext="&cmd.cancel.label;" + cmd="cmd_cancel" ondblclick="event.stopPropagation();" + oncommand="performCommand('cmd_cancel', this);"/> + </xul:hbox> + <xul:label xbl:inherits="value=status,tooltiptext=statusTip" flex="1" + crop="right" class="status"/> + <xul:spacer flex="1"/> + </xul:vbox> + </xul:hbox> + </content> + </binding> + + <binding id="download-done" extends="chrome://mozapps/content/downloads/download.xml#download-base"> + <content> + <xul:hbox flex="1"> + <xul:vbox pack="center"> + <xul:image class="downloadTypeIcon" validate="always" + xbl:inherits="src=image"/> + </xul:vbox> + <xul:vbox pack="start" flex="1"> + <xul:hbox align="center" flex="1"> + <xul:label xbl:inherits="value=target,tooltiptext=target" + crop="center" flex="1" class="name"/> + <xul:label xbl:inherits="value=dateTime,tooltiptext=dateTimeTip" + class="dateTime"/> + </xul:hbox> + <xul:hbox align="center" flex="1"> + <xul:label xbl:inherits="value=status,tooltiptext=statusTip" + crop="end" flex="1" class="status"/> + </xul:hbox> + </xul:vbox> + </xul:hbox> + </content> + </binding> + + <binding id="download-canceled" extends="chrome://mozapps/content/downloads/download.xml#download-base"> + <content> + <xul:hbox flex="1"> + <xul:vbox pack="center"> + <xul:image class="downloadTypeIcon" validate="always" + xbl:inherits="src=image"/> + </xul:vbox> + <xul:vbox pack="start" flex="1"> + <xul:hbox align="center" flex="1"> + <xul:label xbl:inherits="value=target,tooltiptext=target" + crop="center" flex="1" class="name"/> + <xul:label xbl:inherits="value=dateTime,tooltiptext=dateTimeTip" + class="dateTime"/> + </xul:hbox> + <xul:hbox align="center" flex="1"> + <xul:label xbl:inherits="value=status,tooltiptext=statusTip" + crop="end" flex="1" class="status"/> + <xul:button class="retry mini-button" tooltiptext="&cmd.retry.label;" + cmd="cmd_retry" ondblclick="event.stopPropagation();" + oncommand="performCommand('cmd_retry', this);"/> + </xul:hbox> + </xul:vbox> + </xul:hbox> + </content> + </binding> + + <binding id="download-failed" extends="chrome://mozapps/content/downloads/download.xml#download-base"> + <content> + <xul:hbox flex="1"> + <xul:vbox pack="center"> + <xul:image class="downloadTypeIcon" validate="always" + xbl:inherits="src=image"/> + </xul:vbox> + <xul:vbox pack="start" flex="1"> + <xul:hbox align="center" flex="1"> + <xul:label xbl:inherits="value=target,tooltiptext=target" + crop="center" flex="1" class="name"/> + <xul:label xbl:inherits="value=dateTime,tooltiptext=dateTimeTip" + class="dateTime"/> + </xul:hbox> + <xul:hbox align="center" flex="1"> + <xul:label xbl:inherits="value=status,tooltiptext=statusTip" + crop="end" flex="1" class="status"/> + <xul:button class="retry mini-button" tooltiptext="&cmd.retry.label;" + cmd="cmd_retry" ondblclick="event.stopPropagation();" + oncommand="performCommand('cmd_retry', this);"/> + </xul:hbox> + </xul:vbox> + </xul:hbox> + </content> + </binding> + + <binding id="download-blocked-parental" extends="chrome://mozapps/content/downloads/download.xml#download-base"> + <content> + <xul:hbox flex="1"> + <xul:vbox pack="center"> + <xul:image class="downloadTypeIcon blockedIcon"/> + </xul:vbox> + <xul:vbox pack="start" flex="1"> + <xul:hbox align="center" flex="1"> + <xul:label xbl:inherits="value=target,tooltiptext=target" + crop="center" flex="1" class="name"/> + <xul:label xbl:inherits="value=dateTime,tooltiptext=dateTimeTip" + class="dateTime"/> + </xul:hbox> + <xul:hbox align="center" flex="1"> + <xul:label xbl:inherits="value=status,tooltiptext=statusTip" + crop="end" flex="1" class="status"/> + </xul:hbox> + </xul:vbox> + </xul:hbox> + </content> + </binding> + + <binding id="download-blocked-policy" extends="chrome://mozapps/content/downloads/download.xml#download-base"> + <content> + <xul:hbox flex="1"> + <xul:vbox pack="center"> + <xul:image class="downloadTypeIcon blockedIcon"/> + </xul:vbox> + <xul:vbox pack="start" flex="1"> + <xul:hbox align="center" flex="1"> + <xul:label xbl:inherits="value=target,tooltiptext=target" + crop="center" flex="1" class="name"/> + <xul:label xbl:inherits="value=dateTime,tooltiptext=dateTimeTip" + class="dateTime"/> + </xul:hbox> + <xul:hbox align="center" flex="1"> + <xul:label xbl:inherits="value=status,tooltiptext=statusTip" + crop="end" flex="1" class="status"/> + </xul:hbox> + </xul:vbox> + </xul:hbox> + </content> + </binding> + + <binding id="download-scanning" extends="chrome://mozapps/content/downloads/download.xml#download-base"> + <content> + <xul:hbox flex="1"> + <xul:vbox pack="center"> + <xul:image class="downloadTypeIcon" validate="always" + xbl:inherits="src=image"/> + </xul:vbox> + <xul:vbox pack="start" flex="1"> + <xul:label xbl:inherits="value=target,tooltiptext=target" + crop="center" flex="2" class="name"/> + <xul:hbox> + <xul:vbox flex="1"> + <xul:progressmeter mode="undetermined" flex="1" /> + </xul:vbox> + </xul:hbox> + <xul:label value="&scanning.label;" class="status"/> + <xul:spacer flex="1"/> + </xul:vbox> + </xul:hbox> + </content> + </binding> + + <binding id="download-dirty" extends="chrome://mozapps/content/downloads/download.xml#download-base"> + <content> + <xul:hbox flex="1"> + <xul:vbox pack="center"> + <xul:image class="downloadTypeIcon blockedIcon"/> + </xul:vbox> + <xul:vbox pack="start" flex="1"> + <xul:hbox align="center" flex="1"> + <xul:label xbl:inherits="value=target,tooltiptext=target" + crop="center" flex="1" class="name"/> + <xul:label xbl:inherits="value=dateTime,tooltiptext=dateTimeTip" + class="dateTime"/> + </xul:hbox> + <xul:hbox align="center" flex="1"> + <xul:label xbl:inherits="value=status,tooltiptext=statusTip" + crop="end" flex="1" class="status"/> + </xul:hbox> + </xul:vbox> + </xul:hbox> + </content> + </binding> + +</bindings> 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 @@ +<?xml version="1.0"?> + +# -*- 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 + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://mozapps/content/downloads/downloads.css"?> +<?xml-stylesheet href="chrome://mozapps/skin/downloads/downloads.css"?> + +<!DOCTYPE window [ +<!ENTITY % downloadManagerDTD SYSTEM "chrome://mozapps/locale/downloads/downloads.dtd"> +%downloadManagerDTD; +<!ENTITY % editMenuOverlayDTD SYSTEM "chrome://global/locale/editMenuOverlay.dtd"> +%editMenuOverlayDTD; +]> + +<window xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="downloadManager" windowtype="Download:Manager" + orient="vertical" title="&downloads.title;" statictitle="&downloads.title;" + width="&window.width2;" height="&window.height;" screenX="10" screenY="10" + persist="width height screenX screenY sizemode" + onload="Startup();" onunload="Shutdown();" + onclose="return closeWindow(false);"> + + <script type="application/javascript" src="chrome://mozapps/content/downloads/downloads.js"/> + <script type="application/javascript" src="chrome://mozapps/content/downloads/DownloadProgressListener.js"/> + <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/> + <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/> + + <stringbundleset id="downloadSet"> + <stringbundle id="brandStrings" src="chrome://branding/locale/brand.properties"/> + <stringbundle id="downloadStrings" src="chrome://mozapps/locale/downloads/downloads.properties"/> + </stringbundleset> + + <!-- Use this commandset for command which do not depened on focus or selection --> + <commandset id="generalCommands"> + <command id="cmd_findDownload" oncommand="setSearchboxFocus();"/> + <command id="cmd_selectAllDownloads" oncommand="gDownloadsView.selectAll();"/> + <command id="cmd_clearList" oncommand="clearDownloadList();"/> + </commandset> + + <keyset id="downloadKeys"> + <key keycode="VK_RETURN" oncommand="doDefaultForSelected();"/> + <key id="key_pauseResume" key=" " oncommand="performCommand('cmd_pauseResume');"/> + <key id="key_removeFromList" keycode="VK_DELETE" oncommand="performCommand('cmd_removeFromList');"/> +#ifdef XP_MACOSX + <key id="key_removeFromList2" keycode="VK_BACK" oncommand="performCommand('cmd_removeFromList');"/> +#endif + <key id="key_close" key="&cmd.close.commandKey;" oncommand="closeWindow(true);" modifiers="accel"/> +#ifdef XP_GNOME + <key id="key_close2" key="&cmd.close2Unix.commandKey;" oncommand="closeWindow(true);" modifiers="accel,shift"/> +#else + <key id="key_close2" key="&cmd.close2.commandKey;" oncommand="closeWindow(true);" modifiers="accel"/> +#endif + <key keycode="VK_ESCAPE" oncommand="closeWindow(true);"/> + + <key id="key_findDownload" + key="&cmd.find.commandKey;" + modifiers="accel" + command="cmd_findDownload"/> + <key id="key_findDownload2" + key="&cmd.search.commandKey;" + modifiers="accel" + command="cmd_findDownload"/> + <key id="key_selectAllDownloads" + key="&selectAllCmd.key;" + modifiers="accel" + command="cmd_selectAllDownloads"/> + <key id="pasteKey" + key="V" + modifiers="accel" + oncommand="pasteHandler();"/> + </keyset> + + <vbox id="contextMenuPalette" hidden="true"> + <menuitem id="menuitem_pause" + label="&cmd.pause.label;" accesskey="&cmd.pause.accesskey;" + oncommand="performCommand('cmd_pause');" + cmd="cmd_pause"/> + <menuitem id="menuitem_resume" + label="&cmd.resume.label;" accesskey="&cmd.resume.accesskey;" + oncommand="performCommand('cmd_resume');" + cmd="cmd_resume"/> + <menuitem id="menuitem_cancel" + label="&cmd.cancel.label;" accesskey="&cmd.cancel.accesskey;" + oncommand="performCommand('cmd_cancel');" + cmd="cmd_cancel"/> + + <menuitem id="menuitem_open" default="true" + label="&cmd.open.label;" accesskey="&cmd.open.accesskey;" + oncommand="performCommand('cmd_open');" + cmd="cmd_open"/> + <menuitem id="menuitem_show" +#ifdef XP_MACOSX + label="&cmd.showMac.label;" + accesskey="&cmd.showMac.accesskey;" +#else + label="&cmd.show.label;" + accesskey="&cmd.show.accesskey;" +#endif + oncommand="performCommand('cmd_show');" + cmd="cmd_show"/> + + <menuitem id="menuitem_retry" default="true" + label="&cmd.retry.label;" accesskey="&cmd.retry.accesskey;" + oncommand="performCommand('cmd_retry');" + cmd="cmd_retry"/> + + <menuitem id="menuitem_removeFromList" + label="&cmd.removeFromList.label;" accesskey="&cmd.removeFromList.accesskey;" + oncommand="performCommand('cmd_removeFromList');" + cmd="cmd_removeFromList"/> + + <menuseparator id="menuseparator"/> + + <menuitem id="menuitem_openReferrer" + label="&cmd.goToDownloadPage.label;" + accesskey="&cmd.goToDownloadPage.accesskey;" + oncommand="performCommand('cmd_openReferrer');" + cmd="cmd_openReferrer"/> + + <menuitem id="menuitem_copyLocation" + label="&cmd.copyDownloadLink.label;" + accesskey="&cmd.copyDownloadLink.accesskey;" + oncommand="performCommand('cmd_copyLocation');" + cmd="cmd_copyLocation"/> + + <menuitem id="menuitem_selectAll" + label="&selectAllCmd.label;" + accesskey="&selectAllCmd.accesskey;" + command="cmd_selectAllDownloads"/> + </vbox> + + <menupopup id="downloadContextMenu" onpopupshowing="return buildContextMenu(event);"/> + + <richlistbox id="downloadView" seltype="multiple" flex="1" + context="downloadContextMenu" + ondblclick="onDownloadDblClick(event);" + ondragstart="gDownloadDNDObserver.onDragStart(event);" + ondragover="gDownloadDNDObserver.onDragOver(event);event.stopPropagation();" + ondrop="gDownloadDNDObserver.onDrop(event)"> + </richlistbox> + + <windowdragbox id="search" align="center"> + <button id="clearListButton" command="cmd_clearList" + label="&cmd.clearList.label;" + accesskey="&cmd.clearList.accesskey;" + tooltiptext="&cmd.clearList.tooltip;"/> + <spacer flex="1"/> + <textbox type="search" id="searchbox" class="compact" + aria-controls="downloadView" + oncommand="buildDownloadList();" placeholder="&searchBox.label;"/> + </windowdragbox> + +</window> diff --git a/toolkit/mozapps/downloads/content/unknownContentType.xul b/toolkit/mozapps/downloads/content/unknownContentType.xul new file mode 100644 index 000000000..af8b7b016 --- /dev/null +++ b/toolkit/mozapps/downloads/content/unknownContentType.xul @@ -0,0 +1,107 @@ +<?xml version="1.0"?> +# -*- 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/. + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://mozapps/skin/downloads/unknownContentType.css" type="text/css"?> + +<!DOCTYPE dialog [ + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > + %brandDTD; + <!ENTITY % uctDTD SYSTEM "chrome://mozapps/locale/downloads/unknownContentType.dtd" > + %uctDTD; + <!ENTITY % scDTD SYSTEM "chrome://mozapps/locale/downloads/settingsChange.dtd" > + %scDTD; +]> + +<dialog id="unknownContentType" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="dialog.initDialog();" onunload="if (dialog) dialog.onCancel();" +#ifdef XP_WIN + style="width: 36em;" +#else + style="width: 34em;" +#endif + screenX="" screenY="" + persist="screenX screenY" + aria-describedby="intro location whichIs type from source unknownPrompt" + ondialogaccept="return dialog.onOK()" + ondialogcancel="return dialog.onCancel()"> + + + <stringbundle id="strings" src="chrome://mozapps/locale/downloads/unknownContentType.properties"/> + + <vbox flex="1" id="container"> + <description id="intro">&intro2.label;</description> + <separator class="thin"/> + <hbox align="start" class="small-indent"> + <image id="contentTypeImage"/> + <vbox flex="1"> + <description id="location" class="plain" crop="start" flex="1"/> + <separator class="thin"/> + <hbox align="center"> + <label id="whichIs" value="&whichIs.label;"/> + <textbox id="type" class="plain" readonly="true" flex="1" noinitialfocus="true"/> + </hbox> + <hbox align="center"> + <label value="&from.label;" id="from"/> + <description id="source" class="plain" crop="start" flex="1"/> + </hbox> + </vbox> + </hbox> + + <separator class="thin"/> + + <hbox align="center" id="basicBox" collapsed="true"> + <label id="unknownPrompt" value="&unknownPromptText.label;" flex="1"/> + </hbox> + + <groupbox flex="1" id="normalBox"> + <caption label="&actionQuestion.label;"/> + <separator class="thin"/> + <radiogroup id="mode" class="small-indent"> + <hbox> + <radio id="open" label="&openWith.label;" accesskey="&openWith.accesskey;"/> + <deck id="modeDeck" flex="1"> + <hbox id="openHandlerBox" flex="1" align="center"/> + <hbox flex="1" align="center"> + <button id="chooseButton" oncommand="dialog.chooseApp();" +#ifdef XP_MACOSX + label="&chooseHandlerMac.label;" accesskey="&chooseHandlerMac.accesskey;"/> +#else + label="&chooseHandler.label;" accesskey="&chooseHandler.accesskey;"/> +#endif + </hbox> + </deck> + </hbox> + + <radio id="save" label="&saveFile.label;" accesskey="&saveFile.accesskey;"/> + </radiogroup> + <separator class="thin"/> + <hbox class="small-indent"> + <checkbox id="rememberChoice" label="&rememberChoice.label;" + accesskey="&rememberChoice.accesskey;" + oncommand="dialog.toggleRememberChoice(event.target);"/> + </hbox> + + <separator/> +#ifdef XP_UNIX + <description id="settingsChange" hidden="true">&settingsChangePreferences.label;</description> +#else + <description id="settingsChange" hidden="true">&settingsChangeOptions.label;</description> +#endif + <separator class="thin"/> + </groupbox> + </vbox> + + <menulist id="openHandler" flex="1"> + <menupopup id="openHandlerPopup" oncommand="dialog.openHandlerCommand();"> + <menuitem id="defaultHandler" default="true" crop="right"/> + <menuitem id="otherHandler" hidden="true" crop="left"/> + <menuseparator/> + <menuitem id="choose" label="&other.label;"/> + </menupopup> + </menulist> +</dialog> diff --git a/toolkit/mozapps/downloads/jar.mn b/toolkit/mozapps/downloads/jar.mn new file mode 100644 index 000000000..eb761b0d9 --- /dev/null +++ b/toolkit/mozapps/downloads/jar.mn @@ -0,0 +1,12 @@ +# 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/. + +toolkit.jar: +% content mozapps %content/mozapps/ +* content/mozapps/downloads/unknownContentType.xul (content/unknownContentType.xul) +* content/mozapps/downloads/downloads.xul (content/downloads.xul) + content/mozapps/downloads/downloads.js (content/downloads.js) + content/mozapps/downloads/DownloadProgressListener.js (content/DownloadProgressListener.js) + content/mozapps/downloads/downloads.css (content/downloads.css) + content/mozapps/downloads/download.xml (content/download.xml) diff --git a/toolkit/mozapps/downloads/moz.build b/toolkit/mozapps/downloads/moz.build new file mode 100644 index 000000000..1850ea7de --- /dev/null +++ b/toolkit/mozapps/downloads/moz.build @@ -0,0 +1,24 @@ +# -*- 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/. + +TEST_DIRS += ['tests'] + +EXTRA_COMPONENTS += [ + 'nsHelperAppDlg.manifest', +] + +EXTRA_PP_COMPONENTS += [ + 'nsHelperAppDlg.js', +] + +EXTRA_JS_MODULES += [ + 'DownloadLastDir.jsm', + 'DownloadPaths.jsm', + 'DownloadTaskbarProgress.jsm', + 'DownloadUtils.jsm', +] + +JAR_MANIFESTS += ['jar.mn']
\ No newline at end of file diff --git a/toolkit/mozapps/downloads/nsHelperAppDlg.js b/toolkit/mozapps/downloads/nsHelperAppDlg.js new file mode 100644 index 000000000..58697cc77 --- /dev/null +++ b/toolkit/mozapps/downloads/nsHelperAppDlg.js @@ -0,0 +1,1147 @@ +/* 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/. */ + +const {utils: Cu, interfaces: Ci, classes: Cc, results: Cr} = Components; +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/AppConstants.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "EnableDelayHelper", + "resource://gre/modules/SharedPromptUtils.jsm"); + +/////////////////////////////////////////////////////////////////////////////// +//// Helper Functions + +/** + * Determines if a given directory is able to be used to download to. + * + * @param aDirectory + * The directory to check. + * @return true if we can use the directory, false otherwise. + */ +function isUsableDirectory(aDirectory) +{ + return aDirectory.exists() && aDirectory.isDirectory() && + aDirectory.isWritable(); +} + +// Web progress listener so we can detect errors while mLauncher is +// streaming the data to a temporary file. +function nsUnknownContentTypeDialogProgressListener(aHelperAppDialog) { + this.helperAppDlg = aHelperAppDialog; +} + +nsUnknownContentTypeDialogProgressListener.prototype = { + // nsIWebProgressListener methods. + // Look for error notifications and display alert to user. + onStatusChange: function( aWebProgress, aRequest, aStatus, aMessage ) { + if ( aStatus != Components.results.NS_OK ) { + // Display error alert (using text supplied by back-end). + // FIXME this.dialog is undefined? + Services.prompt.alert( this.dialog, this.helperAppDlg.mTitle, aMessage ); + // Close the dialog. + this.helperAppDlg.onCancel(); + if ( this.helperAppDlg.mDialog ) { + this.helperAppDlg.mDialog.close(); + } + } + }, + + // Ignore onProgressChange, onProgressChange64, onStateChange, onLocationChange, onSecurityChange, and onRefreshAttempted notifications. + onProgressChange: function( aWebProgress, + aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress ) { + }, + + onProgressChange64: function( aWebProgress, + aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress ) { + }, + + + + onStateChange: function( aWebProgress, aRequest, aStateFlags, aStatus ) { + }, + + onLocationChange: function( aWebProgress, aRequest, aLocation, aFlags ) { + }, + + onSecurityChange: function( aWebProgress, aRequest, state ) { + }, + + onRefreshAttempted: function( aWebProgress, aURI, aDelay, aSameURI ) { + return true; + } +}; + +/////////////////////////////////////////////////////////////////////////////// +//// nsUnknownContentTypeDialog + +/* This file implements the nsIHelperAppLauncherDialog interface. + * + * The implementation consists of a JavaScript "class" named nsUnknownContentTypeDialog, + * comprised of: + * - a JS constructor function + * - a prototype providing all the interface methods and implementation stuff + * + * In addition, this file implements an nsIModule object that registers the + * nsUnknownContentTypeDialog component. + */ + +const PREF_BD_USEDOWNLOADDIR = "browser.download.useDownloadDir"; +const nsITimer = Components.interfaces.nsITimer; + +var downloadModule = {}; +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/DownloadLastDir.jsm", downloadModule); +Components.utils.import("resource://gre/modules/DownloadPaths.jsm"); +Components.utils.import("resource://gre/modules/DownloadUtils.jsm"); +Components.utils.import("resource://gre/modules/Downloads.jsm"); +Components.utils.import("resource://gre/modules/FileUtils.jsm"); +Components.utils.import("resource://gre/modules/Task.jsm"); + +/* ctor + */ +function nsUnknownContentTypeDialog() { + // Initialize data properties. + this.mLauncher = null; + this.mContext = null; + this.mReason = null; + this.chosenApp = null; + this.givenDefaultApp = false; + this.updateSelf = true; + this.mTitle = ""; +} + +nsUnknownContentTypeDialog.prototype = { + classID: Components.ID("{F68578EB-6EC2-4169-AE19-8C6243F0ABE1}"), + + nsIMIMEInfo : Components.interfaces.nsIMIMEInfo, + + QueryInterface: function (iid) { + if (!iid.equals(Components.interfaces.nsIHelperAppLauncherDialog) && + !iid.equals(Components.interfaces.nsITimerCallback) && + !iid.equals(Components.interfaces.nsISupports)) { + throw Components.results.NS_ERROR_NO_INTERFACE; + } + return this; + }, + + // ---------- nsIHelperAppLauncherDialog methods ---------- + + // show: Open XUL dialog using window watcher. Since the dialog is not + // modal, it needs to be a top level window and the way to open + // one of those is via that route). + show: function(aLauncher, aContext, aReason) { + this.mLauncher = aLauncher; + this.mContext = aContext; + this.mReason = aReason; + + // Cache some information in case this context goes away: + try { + let parent = aContext.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); + this._mDownloadDir = new downloadModule.DownloadLastDir(parent); + } catch (ex) { + Cu.reportError("Missing window information when showing nsIHelperAppLauncherDialog: " + ex); + } + + const nsITimer = Components.interfaces.nsITimer; + this._showTimer = Components.classes["@mozilla.org/timer;1"] + .createInstance(nsITimer); + this._showTimer.initWithCallback(this, 0, nsITimer.TYPE_ONE_SHOT); + }, + + // When opening from new tab, if tab closes while dialog is opening, + // (which is a race condition on the XUL file being cached and the timer + // in nsExternalHelperAppService), the dialog gets a blur and doesn't + // activate the OK button. So we wait a bit before doing opening it. + reallyShow: function() { + try { + let ir = this.mContext.QueryInterface(Components.interfaces.nsIInterfaceRequestor); + let docShell = ir.getInterface(Components.interfaces.nsIDocShell); + let rootWin = docShell.QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + let ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] + .getService(Components.interfaces.nsIWindowWatcher); + this.mDialog = ww.openWindow(rootWin, + "chrome://mozapps/content/downloads/unknownContentType.xul", + null, + "chrome,centerscreen,titlebar,dialog=yes,dependent", + null); + } catch (ex) { + // The containing window may have gone away. Break reference + // cycles and stop doing the download. + this.mLauncher.cancel(Components.results.NS_BINDING_ABORTED); + return; + } + + // Hook this object to the dialog. + this.mDialog.dialog = this; + + // Hook up utility functions. + this.getSpecialFolderKey = this.mDialog.getSpecialFolderKey; + + // Watch for error notifications. + var progressListener = new nsUnknownContentTypeDialogProgressListener(this); + this.mLauncher.setWebProgressListener(progressListener); + }, + + // + // displayBadPermissionAlert() + // + // Diplay an alert panel about the bad permission of folder/directory. + // + displayBadPermissionAlert: function () { + let bundle = + Services.strings.createBundle("chrome://mozapps/locale/downloads/unknownContentType.properties"); + + Services.prompt.alert(this.dialog, + bundle.GetStringFromName("badPermissions.title"), + bundle.GetStringFromName("badPermissions")); + }, + + promptForSaveToFileAsync: function(aLauncher, aContext, aDefaultFile, aSuggestedFileExtension, aForcePrompt) { + var result = null; + + this.mLauncher = aLauncher; + + let prefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + let bundle = + Services.strings + .createBundle("chrome://mozapps/locale/downloads/unknownContentType.properties"); + + let parent; + let gDownloadLastDir; + try { + parent = aContext.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); + } catch (ex) {} + + if (parent) { + gDownloadLastDir = new downloadModule.DownloadLastDir(parent); + } else { + // Use the cached download info, but pick an arbitrary parent window + // because the original one is definitely gone (and nsIFilePicker doesn't like + // a null parent): + gDownloadLastDir = this._mDownloadDir; + let windowsEnum = Services.wm.getEnumerator(""); + while (windowsEnum.hasMoreElements()) { + let someWin = windowsEnum.getNext(); + // We need to make sure we don't end up with this dialog, because otherwise + // that's going to go away when the user clicks "Save", and that breaks the + // windows file picker that's supposed to show up if we let the user choose + // where to save files... + if (someWin != this.mDialog) { + parent = someWin; + } + } + if (!parent) { + Cu.reportError("No candidate parent windows were found for the save filepicker." + + "This should never happen."); + } + } + + Task.spawn(function() { + if (!aForcePrompt) { + // Check to see if the user wishes to auto save to the default download + // folder without prompting. Note that preference might not be set. + let autodownload = false; + try { + autodownload = prefs.getBoolPref(PREF_BD_USEDOWNLOADDIR); + } catch (e) { } + + if (autodownload) { + // Retrieve the user's default download directory + let preferredDir = yield Downloads.getPreferredDownloadsDirectory(); + let defaultFolder = new FileUtils.File(preferredDir); + + try { + result = this.validateLeafName(defaultFolder, aDefaultFile, aSuggestedFileExtension); + } + catch (ex) { + // When the default download directory is write-protected, + // prompt the user for a different target file. + } + + // Check to make sure we have a valid directory, otherwise, prompt + if (result) { + // This path is taken when we have a writable default download directory. + aLauncher.saveDestinationAvailable(result); + return; + } + } + } + + // Use file picker to show dialog. + var nsIFilePicker = Components.interfaces.nsIFilePicker; + var picker = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker); + var windowTitle = bundle.GetStringFromName("saveDialogTitle"); + picker.init(parent, windowTitle, nsIFilePicker.modeSave); + picker.defaultString = aDefaultFile; + + if (aSuggestedFileExtension) { + // aSuggestedFileExtension includes the period, so strip it + picker.defaultExtension = aSuggestedFileExtension.substring(1); + } + else { + try { + picker.defaultExtension = this.mLauncher.MIMEInfo.primaryExtension; + } + catch (ex) { } + } + + var wildCardExtension = "*"; + if (aSuggestedFileExtension) { + wildCardExtension += aSuggestedFileExtension; + picker.appendFilter(this.mLauncher.MIMEInfo.description, wildCardExtension); + } + + picker.appendFilters( nsIFilePicker.filterAll ); + + // Default to lastDir if it is valid, otherwise use the user's default + // downloads directory. getPreferredDownloadsDirectory should always + // return a valid directory path, so we can safely default to it. + let preferredDir = yield Downloads.getPreferredDownloadsDirectory(); + picker.displayDirectory = new FileUtils.File(preferredDir); + + gDownloadLastDir.getFileAsync(aLauncher.source, function LastDirCallback(lastDir) { + if (lastDir && isUsableDirectory(lastDir)) + picker.displayDirectory = lastDir; + + if (picker.show() == nsIFilePicker.returnCancel) { + // null result means user cancelled. + aLauncher.saveDestinationAvailable(null); + return; + } + + // Be sure to save the directory the user chose through the Save As... + // dialog as the new browser.download.dir since the old one + // didn't exist. + result = picker.file; + + if (result) { + try { + // Remove the file so that it's not there when we ensure non-existence later; + // this is safe because for the file to exist, the user would have had to + // confirm that he wanted the file overwritten. + // Only remove file if final name exists + if (result.exists() && this.getFinalLeafName(result.leafName) == result.leafName) + result.remove(false); + } + catch (ex) { + // As it turns out, the failure to remove the file, for example due to + // permission error, will be handled below eventually somehow. + } + + var newDir = result.parent.QueryInterface(Components.interfaces.nsILocalFile); + + // Do not store the last save directory as a pref inside the private browsing mode + gDownloadLastDir.setFile(aLauncher.source, newDir); + + try { + result = this.validateLeafName(newDir, result.leafName, null); + } + catch (ex) { + // When the chosen download directory is write-protected, + // display an informative error message. + // In all cases, download will be stopped. + + if (ex.result == Components.results.NS_ERROR_FILE_ACCESS_DENIED) { + this.displayBadPermissionAlert(); + aLauncher.saveDestinationAvailable(null); + return; + } + + } + } + aLauncher.saveDestinationAvailable(result); + }.bind(this)); + }.bind(this)).then(null, Components.utils.reportError); + }, + + getFinalLeafName: function (aLeafName, aFileExt) + { + // Remove any leading periods, since we don't want to save hidden files + // automatically. + aLeafName = aLeafName.replace(/^\.+/, ""); + + if (aLeafName == "") + aLeafName = "unnamed" + (aFileExt ? "." + aFileExt : ""); + + return aLeafName; + }, + + /** + * Ensures that a local folder/file combination does not already exist in + * the file system (or finds such a combination with a reasonably similar + * leaf name), creates the corresponding file, and returns it. + * + * @param aLocalFolder + * the folder where the file resides + * @param aLeafName + * the string name of the file (may be empty if no name is known, + * in which case a name will be chosen) + * @param aFileExt + * the extension of the file, if one is known; this will be ignored + * if aLeafName is non-empty + * @return nsILocalFile + * the created file + * @throw an error such as permission doesn't allow creation of + * file, etc. + */ + validateLeafName: function (aLocalFolder, aLeafName, aFileExt) + { + if (!(aLocalFolder && isUsableDirectory(aLocalFolder))) { + throw new Components.Exception("Destination directory non-existing or permission error", + Components.results.NS_ERROR_FILE_ACCESS_DENIED); + } + + aLeafName = this.getFinalLeafName(aLeafName, aFileExt); + aLocalFolder.append(aLeafName); + + // The following assignment can throw an exception, but + // is now caught properly in the caller of validateLeafName. + var createdFile = DownloadPaths.createNiceUniqueFile(aLocalFolder); + + if (AppConstants.platform == "win") { + let ext; + try { + // We can fail here if there's no primary extension set + ext = "." + this.mLauncher.MIMEInfo.primaryExtension; + } catch (e) { } + + // Append a file extension if it's an executable that doesn't have one + // but make sure we actually have an extension to add + let leaf = createdFile.leafName; + if (ext && leaf.slice(-ext.length) != ext && createdFile.isExecutable()) { + createdFile.remove(false); + aLocalFolder.leafName = leaf + ext; + createdFile = DownloadPaths.createNiceUniqueFile(aLocalFolder); + } + } + + return createdFile; + }, + + // ---------- implementation methods ---------- + + // initDialog: Fill various dialog fields with initial content. + initDialog : function() { + // Put file name in window title. + var suggestedFileName = this.mLauncher.suggestedFileName; + + // Some URIs do not implement nsIURL, so we can't just QI. + var url = this.mLauncher.source; + if (url instanceof Components.interfaces.nsINestedURI) + url = url.innermostURI; + + var fname = ""; + var iconPath = "goat"; + this.mSourcePath = url.prePath; + if (url instanceof Components.interfaces.nsIURL) { + // A url, use file name from it. + fname = iconPath = url.fileName; + this.mSourcePath += url.directory; + } else { + // A generic uri, use path. + fname = url.path; + this.mSourcePath += url.path; + } + + if (suggestedFileName) + fname = iconPath = suggestedFileName; + + var displayName = fname.replace(/ +/g, " "); + + this.mTitle = this.dialogElement("strings").getFormattedString("title", [displayName]); + this.mDialog.document.title = this.mTitle; + + // Put content type, filename and location into intro. + this.initIntro(url, fname, displayName); + + var iconString = "moz-icon://" + iconPath + "?size=16&contentType=" + this.mLauncher.MIMEInfo.MIMEType; + this.dialogElement("contentTypeImage").setAttribute("src", iconString); + + // if always-save and is-executable and no-handler + // then set up simple ui + var mimeType = this.mLauncher.MIMEInfo.MIMEType; + var shouldntRememberChoice = (mimeType == "application/octet-stream" || + mimeType == "application/x-msdownload" || + this.mLauncher.targetFileIsExecutable); + if ((shouldntRememberChoice && !this.openWithDefaultOK()) || + Services.prefs.getBoolPref("browser.download.forbid_open_with")) { + // hide featured choice + this.dialogElement("normalBox").collapsed = true; + // show basic choice + this.dialogElement("basicBox").collapsed = false; + // change button labels and icons; use "save" icon for the accept + // button since it's the only action possible + let acceptButton = this.mDialog.document.documentElement + .getButton("accept"); + acceptButton.label = this.dialogElement("strings") + .getString("unknownAccept.label"); + acceptButton.setAttribute("icon", "save"); + this.mDialog.document.documentElement.getButton("cancel").label = this.dialogElement("strings").getString("unknownCancel.label"); + // hide other handler + this.dialogElement("openHandler").collapsed = true; + // set save as the selected option + this.dialogElement("mode").selectedItem = this.dialogElement("save"); + } + else { + this.initAppAndSaveToDiskValues(); + + // Initialize "always ask me" box. This should always be disabled + // and set to true for the ambiguous type application/octet-stream. + // We don't also check for application/x-msdownload here since we + // want users to be able to autodownload .exe files. + var rememberChoice = this.dialogElement("rememberChoice"); + + // Just because we have a content-type of application/octet-stream + // here doesn't actually mean that the content is of that type. Many + // servers default to sending text/plain for file types they don't know + // about. To account for this, the uriloader does some checking to see + // if a file sent as text/plain contains binary characters, and if so (*) + // it morphs the content-type into application/octet-stream so that + // the file can be properly handled. Since this is not generic binary + // data, rather, a data format that the system probably knows about, + // we don't want to use the content-type provided by this dialog's + // opener, as that's the generic application/octet-stream that the + // uriloader has passed, rather we want to ask the MIME Service. + // This is so we don't needlessly disable the "autohandle" checkbox. + + // commented out to close the opening brace in the if statement. + // var mimeService = Components.classes["@mozilla.org/mime;1"].getService(Components.interfaces.nsIMIMEService); + // var type = mimeService.getTypeFromURI(this.mLauncher.source); + // this.realMIMEInfo = mimeService.getFromTypeAndExtension(type, ""); + + // if (type == "application/octet-stream") { + if (shouldntRememberChoice) { + rememberChoice.checked = false; + rememberChoice.disabled = true; + } + else { + rememberChoice.checked = !this.mLauncher.MIMEInfo.alwaysAskBeforeHandling && + this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.handleInternally; + } + this.toggleRememberChoice(rememberChoice); + + // XXXben - menulist won't init properly, hack. + var openHandler = this.dialogElement("openHandler"); + openHandler.parentNode.removeChild(openHandler); + var openHandlerBox = this.dialogElement("openHandlerBox"); + openHandlerBox.appendChild(openHandler); + } + + this.mDialog.setTimeout("dialog.postShowCallback()", 0); + + this.delayHelper = new EnableDelayHelper({ + disableDialog: () => { + this.mDialog.document.documentElement.getButton("accept").disabled = true; + }, + enableDialog: () => { + this.mDialog.document.documentElement.getButton("accept").disabled = false; + }, + focusTarget: this.mDialog + }); + }, + + notify: function (aTimer) { + if (aTimer == this._showTimer) { + if (!this.mDialog) { + this.reallyShow(); + } + // The timer won't release us, so we have to release it. + this._showTimer = null; + } + else if (aTimer == this._saveToDiskTimer) { + // Since saveToDisk may open a file picker and therefore block this routine, + // we should only call it once the dialog is closed. + this.mLauncher.saveToDisk(null, false); + this._saveToDiskTimer = null; + } + }, + + postShowCallback: function () { + this.mDialog.sizeToContent(); + + // Set initial focus + this.dialogElement("mode").focus(); + }, + + // initIntro: + initIntro: function(url, filename, displayname) { + this.dialogElement( "location" ).value = displayname; + this.dialogElement( "location" ).setAttribute("realname", filename); + this.dialogElement( "location" ).setAttribute("tooltiptext", displayname); + + // if mSourcePath is a local file, then let's use the pretty path name + // instead of an ugly url... + var pathString; + if (url instanceof Components.interfaces.nsIFileURL) { + try { + // Getting .file might throw, or .parent could be null + pathString = url.file.parent.path; + } catch (ex) {} + } + + if (!pathString) { + // wasn't a fileURL + var tmpurl = url.clone(); // don't want to change the real url + try { + tmpurl.userPass = ""; + } catch (ex) {} + pathString = tmpurl.prePath; + } + + // Set the location text, which is separate from the intro text so it can be cropped + var location = this.dialogElement( "source" ); + location.value = pathString; + location.setAttribute("tooltiptext", this.mSourcePath); + + // Show the type of file. + var type = this.dialogElement("type"); + var mimeInfo = this.mLauncher.MIMEInfo; + + // 1. Try to use the pretty description of the type, if one is available. + var typeString = mimeInfo.description; + + if (typeString == "") { + // 2. If there is none, use the extension to identify the file, e.g. "ZIP file" + var primaryExtension = ""; + try { + primaryExtension = mimeInfo.primaryExtension; + } + catch (ex) { + } + if (primaryExtension != "") + typeString = this.dialogElement("strings").getFormattedString("fileType", [primaryExtension.toUpperCase()]); + // 3. If we can't even do that, just give up and show the MIME type. + else + typeString = mimeInfo.MIMEType; + } + // When the length is unknown, contentLength would be -1 + if (this.mLauncher.contentLength >= 0) { + let [size, unit] = DownloadUtils. + convertByteUnits(this.mLauncher.contentLength); + type.value = this.dialogElement("strings") + .getFormattedString("orderedFileSizeWithType", + [typeString, size, unit]); + } + else { + type.value = typeString; + } + }, + + // Returns true if opening the default application makes sense. + openWithDefaultOK: function() { + // The checking is different on Windows... + if (AppConstants.platform == "win") { + // Windows presents some special cases. + // We need to prevent use of "system default" when the file is + // executable (so the user doesn't launch nasty programs downloaded + // from the web), and, enable use of "system default" if it isn't + // executable (because we will prompt the user for the default app + // in that case). + + // Default is Ok if the file isn't executable (and vice-versa). + return !this.mLauncher.targetFileIsExecutable; + } + // On other platforms, default is Ok if there is a default app. + // Note that nsIMIMEInfo providers need to ensure that this holds true + // on each platform. + return this.mLauncher.MIMEInfo.hasDefaultHandler; + }, + + // Set "default" application description field. + initDefaultApp: function() { + // Use description, if we can get one. + var desc = this.mLauncher.MIMEInfo.defaultDescription; + if (desc) { + var defaultApp = this.dialogElement("strings").getFormattedString("defaultApp", [desc]); + this.dialogElement("defaultHandler").label = defaultApp; + } + else { + this.dialogElement("modeDeck").setAttribute("selectedIndex", "1"); + // Hide the default handler item too, in case the user picks a + // custom handler at a later date which triggers the menulist to show. + this.dialogElement("defaultHandler").hidden = true; + } + }, + + // getPath: + getPath: function (aFile) { + if (AppConstants.platform == "macosx") { + return aFile.leafName || aFile.path; + } + return aFile.path; + }, + + // initAppAndSaveToDiskValues: + initAppAndSaveToDiskValues: function() { + var modeGroup = this.dialogElement("mode"); + + // We don't let users open .exe files or random binary data directly + // from the browser at the moment because of security concerns. + var openWithDefaultOK = this.openWithDefaultOK(); + var mimeType = this.mLauncher.MIMEInfo.MIMEType; + if (this.mLauncher.targetFileIsExecutable || ( + (mimeType == "application/octet-stream" || + mimeType == "application/x-msdownload") && + !openWithDefaultOK)) { + this.dialogElement("open").disabled = true; + var openHandler = this.dialogElement("openHandler"); + openHandler.disabled = true; + openHandler.selectedItem = null; + modeGroup.selectedItem = this.dialogElement("save"); + return; + } + + // Fill in helper app info, if there is any. + try { + this.chosenApp = + this.mLauncher.MIMEInfo.preferredApplicationHandler + .QueryInterface(Components.interfaces.nsILocalHandlerApp); + } catch (e) { + this.chosenApp = null; + } + // Initialize "default application" field. + this.initDefaultApp(); + + var otherHandler = this.dialogElement("otherHandler"); + + // Fill application name textbox. + if (this.chosenApp && this.chosenApp.executable && + this.chosenApp.executable.path) { + otherHandler.setAttribute("path", + this.getPath(this.chosenApp.executable)); + + otherHandler.label = this.getFileDisplayName(this.chosenApp.executable); + otherHandler.hidden = false; + } + + var openHandler = this.dialogElement("openHandler"); + openHandler.selectedIndex = 0; + var defaultOpenHandler = this.dialogElement("defaultHandler"); + + if (this.mLauncher.MIMEInfo.preferredAction == this.nsIMIMEInfo.useSystemDefault) { + // Open (using system default). + modeGroup.selectedItem = this.dialogElement("open"); + } else if (this.mLauncher.MIMEInfo.preferredAction == this.nsIMIMEInfo.useHelperApp) { + // Open with given helper app. + modeGroup.selectedItem = this.dialogElement("open"); + openHandler.selectedItem = (otherHandler && !otherHandler.hidden) ? + otherHandler : defaultOpenHandler; + } else { + // Save to disk. + modeGroup.selectedItem = this.dialogElement("save"); + } + + // If we don't have a "default app" then disable that choice. + if (!openWithDefaultOK) { + var isSelected = defaultOpenHandler.selected; + + // Disable that choice. + defaultOpenHandler.hidden = true; + // If that's the default, then switch to "save to disk." + if (isSelected) { + openHandler.selectedIndex = 1; + modeGroup.selectedItem = this.dialogElement("save"); + } + } + + otherHandler.nextSibling.hidden = otherHandler.nextSibling.nextSibling.hidden = false; + this.updateOKButton(); + }, + + // Returns the user-selected application + helperAppChoice: function() { + return this.chosenApp; + }, + + get saveToDisk() { + return this.dialogElement("save").selected; + }, + + get useOtherHandler() { + return this.dialogElement("open").selected && this.dialogElement("openHandler").selectedIndex == 1; + }, + + get useSystemDefault() { + return this.dialogElement("open").selected && this.dialogElement("openHandler").selectedIndex == 0; + }, + + toggleRememberChoice: function (aCheckbox) { + this.dialogElement("settingsChange").hidden = !aCheckbox.checked; + this.mDialog.sizeToContent(); + }, + + openHandlerCommand: function () { + var openHandler = this.dialogElement("openHandler"); + if (openHandler.selectedItem.id == "choose") + this.chooseApp(); + else + openHandler.setAttribute("lastSelectedItemID", openHandler.selectedItem.id); + }, + + updateOKButton: function() { + var ok = false; + if (this.dialogElement("save").selected) { + // This is always OK. + ok = true; + } + else if (this.dialogElement("open").selected) { + switch (this.dialogElement("openHandler").selectedIndex) { + case 0: + // No app need be specified in this case. + ok = true; + break; + case 1: + // only enable the OK button if we have a default app to use or if + // the user chose an app.... + ok = this.chosenApp || /\S/.test(this.dialogElement("otherHandler").getAttribute("path")); + break; + } + } + + // Enable Ok button if ok to press. + this.mDialog.document.documentElement.getButton("accept").disabled = !ok; + }, + + // Returns true iff the user-specified helper app has been modified. + appChanged: function() { + return this.helperAppChoice() != this.mLauncher.MIMEInfo.preferredApplicationHandler; + }, + + updateMIMEInfo: function() { + // Don't update mime type preferences when the preferred action is set to + // the internal handler -- this dialog is the result of the handler fallback + // (e.g. Content-Disposition was set as attachment) + var discardUpdate = this.mLauncher.MIMEInfo.preferredAction == this.nsIMIMEInfo.handleInternally && + !this.dialogElement("rememberChoice").checked; + + var needUpdate = false; + // If current selection differs from what's in the mime info object, + // then we need to update. + if (this.saveToDisk) { + needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.saveToDisk; + if (needUpdate) + this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.saveToDisk; + } + else if (this.useSystemDefault) { + needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.useSystemDefault; + if (needUpdate) + this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.useSystemDefault; + } + else { + // For "open with", we need to check both preferred action and whether the user chose + // a new app. + needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.useHelperApp || this.appChanged(); + if (needUpdate) { + this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.useHelperApp; + // App may have changed - Update application + var app = this.helperAppChoice(); + this.mLauncher.MIMEInfo.preferredApplicationHandler = app; + } + } + // We will also need to update if the "always ask" flag has changed. + needUpdate = needUpdate || this.mLauncher.MIMEInfo.alwaysAskBeforeHandling != (!this.dialogElement("rememberChoice").checked); + + // One last special case: If the input "always ask" flag was false, then we always + // update. In that case we are displaying the helper app dialog for the first + // time for this mime type and we need to store the user's action in the mimeTypes.rdf + // data source (whether that action has changed or not; if it didn't change, then we need + // to store the "always ask" flag so the helper app dialog will or won't display + // next time, per the user's selection). + needUpdate = needUpdate || !this.mLauncher.MIMEInfo.alwaysAskBeforeHandling; + + // Make sure mime info has updated setting for the "always ask" flag. + this.mLauncher.MIMEInfo.alwaysAskBeforeHandling = !this.dialogElement("rememberChoice").checked; + + return needUpdate && !discardUpdate; + }, + + // See if the user changed things, and if so, update the + // mimeTypes.rdf entry for this mime type. + updateHelperAppPref: function() { + var handlerInfo = this.mLauncher.MIMEInfo; + var hs = Cc["@mozilla.org/uriloader/handler-service;1"].getService(Ci.nsIHandlerService); + hs.store(handlerInfo); + }, + + // onOK: + onOK: function() { + // Verify typed app path, if necessary. + if (this.useOtherHandler) { + var helperApp = this.helperAppChoice(); + if (!helperApp || !helperApp.executable || + !helperApp.executable.exists()) { + // Show alert and try again. + var bundle = this.dialogElement("strings"); + var msg = bundle.getFormattedString("badApp", [this.dialogElement("otherHandler").getAttribute("path")]); + Services.prompt.alert(this.mDialog, bundle.getString("badApp.title"), msg); + + // Disable the OK button. + this.mDialog.document.documentElement.getButton("accept").disabled = true; + this.dialogElement("mode").focus(); + + // Clear chosen application. + this.chosenApp = null; + + // Leave dialog up. + return false; + } + } + + // Remove our web progress listener (a progress dialog will be + // taking over). + this.mLauncher.setWebProgressListener(null); + + // saveToDisk and launchWithApplication can return errors in + // certain circumstances (e.g. The user clicks cancel in the + // "Save to Disk" dialog. In those cases, we don't want to + // update the helper application preferences in the RDF file. + try { + var needUpdate = this.updateMIMEInfo(); + + if (this.dialogElement("save").selected) { + // If we're using a default download location, create a path + // for the file to be saved to to pass to |saveToDisk| - otherwise + // we must ask the user to pick a save name. + + /* + var prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch); + var targetFile = null; + try { + targetFile = prefs.getComplexValue("browser.download.defaultFolder", + Components.interfaces.nsILocalFile); + var leafName = this.dialogElement("location").getAttribute("realname"); + // Ensure that we don't overwrite any existing files here. + targetFile = this.validateLeafName(targetFile, leafName, null); + } + catch(e) { } + + this.mLauncher.saveToDisk(targetFile, false); + */ + + // see @notify + // we cannot use opener's setTimeout, see bug 420405 + this._saveToDiskTimer = Components.classes["@mozilla.org/timer;1"] + .createInstance(nsITimer); + this._saveToDiskTimer.initWithCallback(this, 0, + nsITimer.TYPE_ONE_SHOT); + } + else + this.mLauncher.launchWithApplication(null, false); + + // Update user pref for this mime type (if necessary). We do not + // store anything in the mime type preferences for the ambiguous + // type application/octet-stream. We do NOT do this for + // application/x-msdownload since we want users to be able to + // autodownload these to disk. + if (needUpdate && this.mLauncher.MIMEInfo.MIMEType != "application/octet-stream") + this.updateHelperAppPref(); + } catch(e) { } + + // Unhook dialog from this object. + this.mDialog.dialog = null; + + // Close up dialog by returning true. + return true; + }, + + // onCancel: + onCancel: function() { + // Remove our web progress listener. + this.mLauncher.setWebProgressListener(null); + + // Cancel app launcher. + try { + this.mLauncher.cancel(Components.results.NS_BINDING_ABORTED); + } catch(exception) { + } + + // Unhook dialog from this object. + this.mDialog.dialog = null; + + // Close up dialog by returning true. + return true; + }, + + // dialogElement: Convenience. + dialogElement: function(id) { + return this.mDialog.document.getElementById(id); + }, + + // Retrieve the pretty description from the file + getFileDisplayName: function getFileDisplayName(file) + { + if (AppConstants.platform == "win") { + if (file instanceof Components.interfaces.nsILocalFileWin) { + try { + return file.getVersionInfoField("FileDescription"); + } catch (e) {} + } + } else if (AppConstants.platform == "macosx") { + if (file instanceof Components.interfaces.nsILocalFileMac) { + try { + return file.bundleDisplayName; + } catch (e) {} + } + } + return file.leafName; + }, + + finishChooseApp: function() { + if (this.chosenApp) { + // Show the "handler" menulist since we have a (user-specified) + // application now. + this.dialogElement("modeDeck").setAttribute("selectedIndex", "0"); + + // Update dialog. + var otherHandler = this.dialogElement("otherHandler"); + otherHandler.removeAttribute("hidden"); + otherHandler.setAttribute("path", this.getPath(this.chosenApp.executable)); + if (AppConstants.platform == "win") + otherHandler.label = this.getFileDisplayName(this.chosenApp.executable); + else + otherHandler.label = this.chosenApp.name; + this.dialogElement("openHandler").selectedIndex = 1; + this.dialogElement("openHandler").setAttribute("lastSelectedItemID", "otherHandler"); + + this.dialogElement("mode").selectedItem = this.dialogElement("open"); + } + else { + var openHandler = this.dialogElement("openHandler"); + var lastSelectedID = openHandler.getAttribute("lastSelectedItemID"); + if (!lastSelectedID) + lastSelectedID = "defaultHandler"; + openHandler.selectedItem = this.dialogElement(lastSelectedID); + } + }, + // chooseApp: Open file picker and prompt user for application. + chooseApp: function() { + if (AppConstants.platform == "win") { + // Protect against the lack of an extension + var fileExtension = ""; + try { + fileExtension = this.mLauncher.MIMEInfo.primaryExtension; + } catch(ex) { + } + + // Try to use the pretty description of the type, if one is available. + var typeString = this.mLauncher.MIMEInfo.description; + + if (!typeString) { + // If there is none, use the extension to + // identify the file, e.g. "ZIP file" + if (fileExtension) { + typeString = + this.dialogElement("strings"). + getFormattedString("fileType", [fileExtension.toUpperCase()]); + } else { + // If we can't even do that, just give up and show the MIME type. + typeString = this.mLauncher.MIMEInfo.MIMEType; + } + } + + var params = {}; + params.title = + this.dialogElement("strings").getString("chooseAppFilePickerTitle"); + params.description = typeString; + params.filename = this.mLauncher.suggestedFileName; + params.mimeInfo = this.mLauncher.MIMEInfo; + params.handlerApp = null; + + this.mDialog.openDialog("chrome://global/content/appPicker.xul", null, + "chrome,modal,centerscreen,titlebar,dialog=yes", + params); + + if (params.handlerApp && + params.handlerApp.executable && + params.handlerApp.executable.isFile()) { + // Remember the file they chose to run. + this.chosenApp = params.handlerApp; + } + } + else { +#if MOZ_WIDGET_GTK == 3 + var nsIApplicationChooser = Components.interfaces.nsIApplicationChooser; + var appChooser = Components.classes["@mozilla.org/applicationchooser;1"] + .createInstance(nsIApplicationChooser); + appChooser.init(this.mDialog, this.dialogElement("strings").getString("chooseAppFilePickerTitle")); + var contentTypeDialogObj = this; + let appChooserCallback = function appChooserCallback_done(aResult) { + if (aResult) { + contentTypeDialogObj.chosenApp = aResult.QueryInterface(Components.interfaces.nsILocalHandlerApp); + } + contentTypeDialogObj.finishChooseApp(); + }; + appChooser.open(this.mLauncher.MIMEInfo.MIMEType, appChooserCallback); + // The finishChooseApp is called from appChooserCallback + return; +#else + var nsIFilePicker = Components.interfaces.nsIFilePicker; + var fp = Components.classes["@mozilla.org/filepicker;1"] + .createInstance(nsIFilePicker); + fp.init(this.mDialog, + this.dialogElement("strings").getString("chooseAppFilePickerTitle"), + nsIFilePicker.modeOpen); + + fp.appendFilters(nsIFilePicker.filterApps); + + if (fp.show() == nsIFilePicker.returnOK && fp.file) { + // Remember the file they chose to run. + var localHandlerApp = + Components.classes["@mozilla.org/uriloader/local-handler-app;1"]. + createInstance(Components.interfaces.nsILocalHandlerApp); + localHandlerApp.executable = fp.file; + this.chosenApp = localHandlerApp; + } +#endif // MOZ_WIDGET_GTK == 3 + } + this.finishChooseApp(); + }, + + // Turn this on to get debugging messages. + debug: false, + + // Dump text (if debug is on). + dump: function( text ) { + if ( this.debug ) { + dump( text ); + } + }, + + // dumpObj: + dumpObj: function( spec ) { + var val = "<undefined>"; + try { + val = eval( "this."+spec ).toString(); + } catch( exception ) { + } + this.dump( spec + "=" + val + "\n" ); + }, + + // dumpObjectProperties + dumpObjectProperties: function( desc, obj ) { + for( prop in obj ) { + this.dump( desc + "." + prop + "=" ); + var val = "<undefined>"; + try { + val = obj[ prop ]; + } catch ( exception ) { + } + this.dump( val + "\n" ); + } + } +} + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([nsUnknownContentTypeDialog]); diff --git a/toolkit/mozapps/downloads/nsHelperAppDlg.manifest b/toolkit/mozapps/downloads/nsHelperAppDlg.manifest new file mode 100644 index 000000000..8824b45a2 --- /dev/null +++ b/toolkit/mozapps/downloads/nsHelperAppDlg.manifest @@ -0,0 +1,2 @@ +component {F68578EB-6EC2-4169-AE19-8C6243F0ABE1} nsHelperAppDlg.js +contract @mozilla.org/helperapplauncherdialog;1 {F68578EB-6EC2-4169-AE19-8C6243F0ABE1} diff --git a/toolkit/mozapps/downloads/tests/chrome/.eslintrc.js b/toolkit/mozapps/downloads/tests/chrome/.eslintrc.js new file mode 100644 index 000000000..8c0f4f574 --- /dev/null +++ b/toolkit/mozapps/downloads/tests/chrome/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/mochitest/chrome.eslintrc.js" + ] +}; diff --git a/toolkit/mozapps/downloads/tests/chrome/chrome.ini b/toolkit/mozapps/downloads/tests/chrome/chrome.ini new file mode 100644 index 000000000..b5a29cb45 --- /dev/null +++ b/toolkit/mozapps/downloads/tests/chrome/chrome.ini @@ -0,0 +1,10 @@ +[DEFAULT] +skip-if = os == 'android' +support-files = + unknownContentType_dialog_layout_data.pif + unknownContentType_dialog_layout_data.pif^headers^ + unknownContentType_dialog_layout_data.txt + unknownContentType_dialog_layout_data.txt^headers^ + +[test_unknownContentType_delayedbutton.xul] +[test_unknownContentType_dialog_layout.xul] diff --git a/toolkit/mozapps/downloads/tests/chrome/test_unknownContentType_delayedbutton.xul b/toolkit/mozapps/downloads/tests/chrome/test_unknownContentType_delayedbutton.xul new file mode 100644 index 000000000..9bbec0f92 --- /dev/null +++ b/toolkit/mozapps/downloads/tests/chrome/test_unknownContentType_delayedbutton.xul @@ -0,0 +1,117 @@ +<?xml version="1.0"?> +<!-- 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 unknownContentType popup can have two different layouts depending on + * whether a helper application can be selected or not. + * This tests that both layouts have correct collapsed elements. +--> + +<window title="Unknown Content Type Dialog Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="doTest()"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <script type="application/javascript"><![CDATA[ + const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + + Cu.import("resource://gre/modules/Services.jsm"); + Cu.import("resource://gre/modules/Task.jsm"); + Cu.import("resource://gre/modules/Promise.jsm"); + + const UCT_URI = "chrome://mozapps/content/downloads/unknownContentType.xul"; + const LOAD_URI = "http://mochi.test:8888/chrome/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.txt"; + + const DIALOG_DELAY = Services.prefs.getIntPref("security.dialog_enable_delay") + 200; + + let UCTObserver = { + opened: Promise.defer(), + closed: Promise.defer(), + + observe: function(aSubject, aTopic, aData) { + let win = aSubject.QueryInterface(Ci.nsIDOMEventTarget); + + switch (aTopic) { + case "domwindowopened": + win.addEventListener("load", function onLoad(event) { + win.removeEventListener("load", onLoad, false); + + // Let the dialog initialize + SimpleTest.executeSoon(function() { + UCTObserver.opened.resolve(win); + }); + }, false); + break; + + case "domwindowclosed": + if (win.location == UCT_URI) { + this.closed.resolve(); + } + break; + } + } + }; + + Services.ww.registerNotification(UCTObserver); + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestFlakyTimeout("This test is testing a timing-based feature, so it really needs to wait a certain amount of time to verify that the feature worked."); + + function waitDelay(delay) { + return new Promise((resolve, reject) => { + window.setTimeout(resolve, delay); + }); + } + + function doTest() { + Task.spawn(function test_aboutCrashed() { + let frame = document.getElementById("testframe"); + frame.setAttribute("src", LOAD_URI); + + let uctWindow = yield UCTObserver.opened.promise; + let ok = uctWindow.document.documentElement.getButton("accept"); + + SimpleTest.is(ok.disabled, true, "button started disabled"); + + yield waitDelay(DIALOG_DELAY); + + SimpleTest.is(ok.disabled, false, "button was enabled"); + + focusOutOfDialog = SimpleTest.promiseFocus(window); + window.focus(); + yield focusOutOfDialog; + + SimpleTest.is(ok.disabled, true, "button was disabled"); + + focusOnDialog = SimpleTest.promiseFocus(uctWindow); + uctWindow.focus(); + yield focusOnDialog; + + SimpleTest.is(ok.disabled, true, "button remained disabled"); + + yield waitDelay(DIALOG_DELAY); + SimpleTest.is(ok.disabled, false, "button re-enabled after delay"); + + uctWindow.document.documentElement.cancelDialog(); + yield UCTObserver.closed.promise; + + Services.ww.unregisterNotification(UCTObserver); + uctWindow = null; + UCTObserver = null; + SimpleTest.finish(); + }); + } + ]]></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + + <iframe xmlns="http://www.w3.org/1999/xhtml" + id="testframe"> + </iframe> +</window> 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 @@ +<?xml version="1.0"?> +<!-- 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 unknownContentType popup can have two different layouts depending on + * whether a helper application can be selected or not. + * This tests that both layouts have correct collapsed elements. +--> + +<window title="Unknown Content Type Dialog Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="init()"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <script type="application/javascript"> + <![CDATA[ +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +const UCT_URI = "chrome://mozapps/content/downloads/unknownContentType.xul"; + +let testIndex = -1; +let tests = [ + { // This URL will trigger the simple UI, where only the Save an Cancel buttons are available + url: "http://mochi.test:8888/chrome/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.pif", + elements: { + basicBox: { collapsed: false }, + normalBox: { collapsed: true } + } + }, + { // This URL will trigger the full UI + url: "http://mochi.test:8888/chrome/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.txt", + elements: { + basicBox: { collapsed: true }, + normalBox: { collapsed: false } + } + } +]; + +let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. + getService(Ci.nsIWindowWatcher); + +SimpleTest.waitForExplicitFinish(); + +let windowObserver = { + observe: function(aSubject, aTopic, aData) { + let win = aSubject.QueryInterface(Ci.nsIDOMEventTarget); + + if (aTopic == "domwindowclosed") { + if (win.location == UCT_URI) + loadNextTest(); + return; + } + + // domwindowopened + win.addEventListener("load", function onLoad(event) { + win.removeEventListener("load", onLoad, false); + + // Let the dialog initialize + SimpleTest.executeSoon(function() { + checkWindow(win); + }); + }, false); + } +}; + +function init() { + ww.registerNotification(windowObserver); + loadNextTest(); +} + +function loadNextTest() { + if (!tests[++testIndex]) { + ww.unregisterNotification(windowObserver); + SimpleTest.finish(); + return; + } + let frame = document.getElementById("testframe"); + frame.setAttribute("src", tests[testIndex].url); +} + +function checkWindow(win) { + for (let [id, props] of Object.entries(tests[testIndex].elements)) { + let elem = win.dialog.dialogElement(id); + for (let [prop, value] of Object.entries(props)) { + is(elem[prop], value, + "Element with id " + id + " has property " + + prop + " set to " + value); + } + } + win.document.documentElement.cancelDialog(); +} + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + + <iframe xmlns="http://www.w3.org/1999/xhtml" + id="testframe"> + </iframe> +</window> 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] |