diff options
Diffstat (limited to 'components/downloads/src')
-rw-r--r-- | components/downloads/src/DownloadLastDir.jsm | 195 | ||||
-rw-r--r-- | components/downloads/src/DownloadPaths.jsm | 88 | ||||
-rw-r--r-- | components/downloads/src/DownloadTaskbarProgress.jsm | 400 | ||||
-rw-r--r-- | components/downloads/src/DownloadUtils.jsm | 600 | ||||
-rw-r--r-- | components/downloads/src/SQLFunctions.cpp | 146 | ||||
-rw-r--r-- | components/downloads/src/SQLFunctions.h | 46 | ||||
-rw-r--r-- | components/downloads/src/nsDownloadManager.cpp | 3711 | ||||
-rw-r--r-- | components/downloads/src/nsDownloadManager.h | 454 | ||||
-rw-r--r-- | components/downloads/src/nsDownloadManagerUI.js | 107 | ||||
-rw-r--r-- | components/downloads/src/nsDownloadProxy.h | 179 | ||||
-rw-r--r-- | components/downloads/src/nsDownloadScanner.cpp | 728 | ||||
-rw-r--r-- | components/downloads/src/nsDownloadScanner.h | 121 | ||||
-rw-r--r-- | components/downloads/src/nsHelperAppDlg.js | 1138 |
13 files changed, 7913 insertions, 0 deletions
diff --git a/components/downloads/src/DownloadLastDir.jsm b/components/downloads/src/DownloadLastDir.jsm new file mode 100644 index 000000000..552fd3ef1 --- /dev/null +++ b/components/downloads/src/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/components/downloads/src/DownloadPaths.jsm b/components/downloads/src/DownloadPaths.jsm new file mode 100644 index 000000000..202e42487 --- /dev/null +++ b/components/downloads/src/DownloadPaths.jsm @@ -0,0 +1,88 @@ +/* -*- 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/. */ + +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/components/downloads/src/DownloadTaskbarProgress.jsm b/components/downloads/src/DownloadTaskbarProgress.jsm new file mode 100644 index 000000000..0264005e0 --- /dev/null +++ b/components/downloads/src/DownloadTaskbarProgress.jsm @@ -0,0 +1,400 @@ +/* -*- 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"); +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) + { + this.init(); + 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) + { +#ifdef XP_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; + } +#endif + }, + + // / Current state displayed on the active window's taskbar item + _taskbarState: null, + _totalSize: 0, + _totalTransferred: 0, + + _shouldSetState: function DTPU_shouldSetState() + { +#ifdef XP_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); +#else + return true; +#endif + }, + + /** + * 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/components/downloads/src/DownloadUtils.jsm b/components/downloads/src/DownloadUtils.jsm new file mode 100644 index 000000000..3ebdd605e --- /dev/null +++ b/components/downloads/src/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/components/downloads/src/SQLFunctions.cpp b/components/downloads/src/SQLFunctions.cpp new file mode 100644 index 000000000..8f2d3e77b --- /dev/null +++ b/components/downloads/src/SQLFunctions.cpp @@ -0,0 +1,146 @@ +/* vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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/. */ + +#include "mozilla/storage.h" +#include "mozilla/storage/Variant.h" +#include "mozilla/mozalloc.h" +#include "nsString.h" +#include "SQLFunctions.h" +#include "nsUTF8Utils.h" +#include "plbase64.h" +#include "prio.h" + +#ifdef XP_WIN +#include <windows.h> +#include <wincrypt.h> +#endif + +// The length of guids that are used by the download manager +#define GUID_LENGTH 12 + +namespace mozilla { +namespace downloads { + +// Keep this file in sync with the GUID-related code in toolkit/places/SQLFunctions.cpp +// and toolkit/places/Helpers.cpp! + +//////////////////////////////////////////////////////////////////////////////// +//// GUID Creation Function + +////////////////////////////////////////////////////////////////////////////// +//// GenerateGUIDFunction + +/* static */ +nsresult +GenerateGUIDFunction::create(mozIStorageConnection *aDBConn) +{ + RefPtr<GenerateGUIDFunction> function = new GenerateGUIDFunction(); + nsresult rv = aDBConn->CreateFunction( + NS_LITERAL_CSTRING("generate_guid"), 0, function + ); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMPL_ISUPPORTS( + GenerateGUIDFunction, + mozIStorageFunction +) + +static +nsresult +Base64urlEncode(const uint8_t* aBytes, + uint32_t aNumBytes, + nsCString& _result) +{ + // SetLength does not set aside space for null termination. PL_Base64Encode + // will not null terminate, however, nsCStrings must be null terminated. As a + // result, we set the capacity to be one greater than what we need, and the + // length to our desired length. + uint32_t length = (aNumBytes + 2) / 3 * 4; // +2 due to integer math. + NS_ENSURE_TRUE(_result.SetCapacity(length + 1, mozilla::fallible), + NS_ERROR_OUT_OF_MEMORY); + _result.SetLength(length); + (void)PL_Base64Encode(reinterpret_cast<const char*>(aBytes), aNumBytes, + _result.BeginWriting()); + + // base64url encoding is defined in RFC 4648. It replaces the last two + // alphabet characters of base64 encoding with '-' and '_' respectively. + _result.ReplaceChar('+', '-'); + _result.ReplaceChar('/', '_'); + return NS_OK; +} + +static +nsresult +GenerateRandomBytes(uint32_t aSize, + uint8_t* _buffer) +{ + // On Windows, we'll use its built-in cryptographic API. +#if defined(XP_WIN) + HCRYPTPROV cryptoProvider; + BOOL rc = CryptAcquireContext(&cryptoProvider, 0, 0, PROV_RSA_FULL, + CRYPT_VERIFYCONTEXT | CRYPT_SILENT); + if (rc) { + rc = CryptGenRandom(cryptoProvider, aSize, _buffer); + (void)CryptReleaseContext(cryptoProvider, 0); + } + return rc ? NS_OK : NS_ERROR_FAILURE; + + // On Unix, we'll just read in from /dev/urandom. +#elif defined(XP_UNIX) + NS_ENSURE_ARG_MAX(aSize, INT32_MAX); + PRFileDesc* urandom = PR_Open("/dev/urandom", PR_RDONLY, 0); + nsresult rv = NS_ERROR_FAILURE; + if (urandom) { + int32_t bytesRead = PR_Read(urandom, _buffer, aSize); + if (bytesRead == static_cast<int32_t>(aSize)) { + rv = NS_OK; + } + (void)PR_Close(urandom); + } + return rv; +#endif +} + +nsresult +GenerateGUID(nsCString& _guid) +{ + _guid.Truncate(); + + // Request raw random bytes and base64url encode them. For each set of three + // bytes, we get one character. + const uint32_t kRequiredBytesLength = + static_cast<uint32_t>(GUID_LENGTH / 4 * 3); + + uint8_t buffer[kRequiredBytesLength]; + nsresult rv = GenerateRandomBytes(kRequiredBytesLength, buffer); + NS_ENSURE_SUCCESS(rv, rv); + + rv = Base64urlEncode(buffer, kRequiredBytesLength, _guid); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(_guid.Length() == GUID_LENGTH, "GUID is not the right size!"); + return NS_OK; +} + +////////////////////////////////////////////////////////////////////////////// +//// mozIStorageFunction + +NS_IMETHODIMP +GenerateGUIDFunction::OnFunctionCall(mozIStorageValueArray *aArguments, + nsIVariant **_result) +{ + nsAutoCString guid; + nsresult rv = GenerateGUID(guid); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ADDREF(*_result = new mozilla::storage::UTF8TextVariant(guid)); + return NS_OK; +} + +} // namespace downloads +} // namespace mozilla diff --git a/components/downloads/src/SQLFunctions.h b/components/downloads/src/SQLFunctions.h new file mode 100644 index 000000000..ae207788c --- /dev/null +++ b/components/downloads/src/SQLFunctions.h @@ -0,0 +1,46 @@ +/* vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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/. */ + +#ifndef mozilla_downloads_SQLFunctions_h +#define mozilla_downloads_SQLFunctions_h + +#include "mozIStorageFunction.h" +#include "mozilla/Attributes.h" + +class nsCString; +class mozIStorageConnection; + +namespace mozilla { +namespace downloads { + +/** + * SQL function to generate a GUID for a place or bookmark item. This is just + * a wrapper around GenerateGUID in SQLFunctions.cpp. + * + * @return a guid for the item. + * @see toolkit/components/places/SQLFunctions.h - keep this in sync + */ +class GenerateGUIDFunction final : public mozIStorageFunction +{ + ~GenerateGUIDFunction() {} +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_MOZISTORAGEFUNCTION + + /** + * Registers the function with the specified database connection. + * + * @param aDBConn + * The database connection to register with. + */ + static nsresult create(mozIStorageConnection *aDBConn); +}; + +nsresult GenerateGUID(nsCString& _guid); + +} // namespace downloads +} // namespace mozilla + +#endif diff --git a/components/downloads/src/nsDownloadManager.cpp b/components/downloads/src/nsDownloadManager.cpp new file mode 100644 index 000000000..bc01b9ae5 --- /dev/null +++ b/components/downloads/src/nsDownloadManager.cpp @@ -0,0 +1,3711 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "mozilla/DebugOnly.h" +#include "mozilla/Unused.h" + +#include "mozIStorageService.h" +#include "nsIAlertsService.h" +#include "nsIArray.h" +#include "nsIClassInfoImpl.h" +#include "nsIDOMWindow.h" +#include "nsIDownloadHistory.h" +#include "nsIDownloadManagerUI.h" +#include "nsIFileURL.h" +#include "nsIMIMEService.h" +#include "nsIParentalControlsService.h" +#include "nsIPrefService.h" +#include "nsIPrivateBrowsingChannel.h" +#include "nsIPromptService.h" +#include "nsIPropertyBag2.h" +#include "nsIResumableChannel.h" +#include "nsIWebBrowserPersist.h" +#include "nsIWindowMediator.h" +#include "nsILocalFileWin.h" +#include "nsILoadContext.h" +#include "nsIXULAppInfo.h" +#include "nsContentUtils.h" + +#include "nsAppDirectoryServiceDefs.h" +#include "nsArrayEnumerator.h" +#include "nsCExternalHandlerService.h" +#include "nsCRTGlue.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDownloadManager.h" +#include "nsNetUtil.h" +#include "nsThreadUtils.h" +#include "prtime.h" + +#include "mozStorageCID.h" +#include "nsDocShellCID.h" +#include "nsEmbedCID.h" +#include "nsToolkitCompsCID.h" + +#include "mozilla/net/ReferrerPolicy.h" + +#include "SQLFunctions.h" + +#include "mozilla/Preferences.h" + +#ifdef XP_WIN +#include <shlobj.h> +#include "nsWindowsHelpers.h" +#ifdef DOWNLOAD_SCANNER +#include "nsDownloadScanner.h" +#endif +#endif + +#ifdef MOZ_WIDGET_GTK +#include <gtk/gtk.h> +#endif + +using namespace mozilla; +using mozilla::downloads::GenerateGUID; + +#define DOWNLOAD_MANAGER_BUNDLE "chrome://mozapps/locale/downloads/downloads.properties" +#define DOWNLOAD_MANAGER_ALERT_ICON "chrome://mozapps/skin/downloads/downloadIcon.png" +#define PREF_BD_USEJSTRANSFER "browser.download.useJSTransfer" +#define PREF_BDM_SHOWALERTONCOMPLETE "browser.download.manager.showAlertOnComplete" +#define PREF_BDM_SHOWALERTINTERVAL "browser.download.manager.showAlertInterval" +#define PREF_BDM_RETENTION "browser.download.manager.retention" +#define PREF_BDM_QUITBEHAVIOR "browser.download.manager.quitBehavior" +#define PREF_BDM_ADDTORECENTDOCS "browser.download.manager.addToRecentDocs" +#define PREF_BDM_SCANWHENDONE "browser.download.manager.scanWhenDone" +#define PREF_BDM_RESUMEONWAKEDELAY "browser.download.manager.resumeOnWakeDelay" +#define PREF_BH_DELETETEMPFILEONEXIT "browser.helperApps.deleteTempFileOnExit" + +static const int64_t gUpdateInterval = 400 * PR_USEC_PER_MSEC; + +#define DM_SCHEMA_VERSION 9 +#define DM_DB_NAME NS_LITERAL_STRING("downloads.sqlite") +#define DM_DB_CORRUPT_FILENAME NS_LITERAL_STRING("downloads.sqlite.corrupt") + +#define NS_SYSTEMINFO_CONTRACTID "@mozilla.org/system-info;1" + +//////////////////////////////////////////////////////////////////////////////// +//// nsDownloadManager + +NS_IMPL_ISUPPORTS( + nsDownloadManager +, nsIDownloadManager +, nsINavHistoryObserver +, nsIObserver +, nsISupportsWeakReference +) + +nsDownloadManager *nsDownloadManager::gDownloadManagerService = nullptr; + +nsDownloadManager * +nsDownloadManager::GetSingleton() +{ + if (gDownloadManagerService) { + NS_ADDREF(gDownloadManagerService); + return gDownloadManagerService; + } + + gDownloadManagerService = new nsDownloadManager(); + if (gDownloadManagerService) { +#if defined(MOZ_WIDGET_GTK) + g_type_init(); +#endif + NS_ADDREF(gDownloadManagerService); + if (NS_FAILED(gDownloadManagerService->Init())) + NS_RELEASE(gDownloadManagerService); + } + + return gDownloadManagerService; +} + +nsDownloadManager::~nsDownloadManager() +{ +#ifdef DOWNLOAD_SCANNER + if (mScanner) { + delete mScanner; + mScanner = nullptr; + } +#endif + gDownloadManagerService = nullptr; +} + +nsresult +nsDownloadManager::ResumeRetry(nsDownload *aDl) +{ + // Keep a reference in case we need to cancel the download + RefPtr<nsDownload> dl = aDl; + + // Try to resume the active download + nsresult rv = dl->Resume(); + + // If not, try to retry the download + if (NS_FAILED(rv)) { + // First cancel the download so it's no longer active + rv = dl->Cancel(); + + // Then retry it + if (NS_SUCCEEDED(rv)) + rv = dl->Retry(); + } + + return rv; +} + +nsresult +nsDownloadManager::PauseAllDownloads(bool aSetResume) +{ + nsresult rv = PauseAllDownloads(mCurrentDownloads, aSetResume); + nsresult rv2 = PauseAllDownloads(mCurrentPrivateDownloads, aSetResume); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_SUCCESS(rv2, rv2); + return NS_OK; +} + +nsresult +nsDownloadManager::PauseAllDownloads(nsCOMArray<nsDownload>& aDownloads, bool aSetResume) +{ + nsresult retVal = NS_OK; + for (int32_t i = aDownloads.Count() - 1; i >= 0; --i) { + RefPtr<nsDownload> dl = aDownloads[i]; + + // Only pause things that need to be paused + if (!dl->IsPaused()) { + // Set auto-resume before pausing so that it gets into the DB + dl->mAutoResume = aSetResume ? nsDownload::AUTO_RESUME : + nsDownload::DONT_RESUME; + + // Try to pause the download but don't bail now if we fail + nsresult rv = dl->Pause(); + if (NS_FAILED(rv)) + retVal = rv; + } + } + + return retVal; +} + +nsresult +nsDownloadManager::ResumeAllDownloads(bool aResumeAll) +{ + nsresult rv = ResumeAllDownloads(mCurrentDownloads, aResumeAll); + nsresult rv2 = ResumeAllDownloads(mCurrentPrivateDownloads, aResumeAll); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_SUCCESS(rv2, rv2); + return NS_OK; +} + +nsresult +nsDownloadManager::ResumeAllDownloads(nsCOMArray<nsDownload>& aDownloads, bool aResumeAll) +{ + nsresult retVal = NS_OK; + for (int32_t i = aDownloads.Count() - 1; i >= 0; --i) { + RefPtr<nsDownload> dl = aDownloads[i]; + + // If aResumeAll is true, then resume everything; otherwise, check if the + // download should auto-resume + if (aResumeAll || dl->ShouldAutoResume()) { + // Reset auto-resume before retrying so that it gets into the DB through + // ResumeRetry's eventual call to SetState. We clear the value now so we + // don't accidentally query completed downloads that were previously + // auto-resumed (and try to resume them). + dl->mAutoResume = nsDownload::DONT_RESUME; + + // Try to resume/retry the download but don't bail now if we fail + nsresult rv = ResumeRetry(dl); + if (NS_FAILED(rv)) + retVal = rv; + } + } + + return retVal; +} + +nsresult +nsDownloadManager::RemoveAllDownloads() +{ + nsresult rv = RemoveAllDownloads(mCurrentDownloads); + nsresult rv2 = RemoveAllDownloads(mCurrentPrivateDownloads); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_SUCCESS(rv2, rv2); + return NS_OK; +} + +nsresult +nsDownloadManager::RemoveAllDownloads(nsCOMArray<nsDownload>& aDownloads) +{ + nsresult rv = NS_OK; + for (int32_t i = aDownloads.Count() - 1; i >= 0; --i) { + RefPtr<nsDownload> dl = aDownloads[0]; + + nsresult result = NS_OK; + if (!dl->mPrivate && dl->IsPaused() && GetQuitBehavior() != QUIT_AND_CANCEL) + aDownloads.RemoveObject(dl); + else + result = dl->Cancel(); + + // Track the failure, but don't miss out on other downloads + if (NS_FAILED(result)) + rv = result; + } + + return rv; +} + +nsresult +nsDownloadManager::RemoveDownloadsForURI(mozIStorageStatement* aStatement, nsIURI *aURI) +{ + mozStorageStatementScoper scope(aStatement); + + nsAutoCString source; + nsresult rv = aURI->GetSpec(source); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aStatement->BindUTF8StringByName( + NS_LITERAL_CSTRING("source"), source); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasMore = false; + AutoTArray<nsCString, 4> downloads; + // Get all the downloads that match the provided URI + while (NS_SUCCEEDED(aStatement->ExecuteStep(&hasMore)) && + hasMore) { + nsAutoCString downloadGuid; + rv = aStatement->GetUTF8String(0, downloadGuid); + NS_ENSURE_SUCCESS(rv, rv); + + downloads.AppendElement(downloadGuid); + } + + // Remove each download ignoring any failure so we reach other downloads + for (int32_t i = downloads.Length(); --i >= 0; ) + (void)RemoveDownload(downloads[i]); + + return NS_OK; +} + +void // static +nsDownloadManager::ResumeOnWakeCallback(nsITimer *aTimer, void *aClosure) +{ + // Resume the downloads that were set to autoResume + nsDownloadManager *dlMgr = static_cast<nsDownloadManager *>(aClosure); + (void)dlMgr->ResumeAllDownloads(false); +} + +already_AddRefed<mozIStorageConnection> +nsDownloadManager::GetFileDBConnection(nsIFile *dbFile) const +{ + NS_ASSERTION(dbFile, "GetFileDBConnection called with an invalid nsIFile"); + + nsCOMPtr<mozIStorageService> storage = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID); + NS_ENSURE_TRUE(storage, nullptr); + + nsCOMPtr<mozIStorageConnection> conn; + nsresult rv = storage->OpenDatabase(dbFile, getter_AddRefs(conn)); + if (rv == NS_ERROR_FILE_CORRUPTED) { + // delete and try again, since we don't care so much about losing a user's + // download history + rv = dbFile->Remove(false); + NS_ENSURE_SUCCESS(rv, nullptr); + rv = storage->OpenDatabase(dbFile, getter_AddRefs(conn)); + } + NS_ENSURE_SUCCESS(rv, nullptr); + + return conn.forget(); +} + +already_AddRefed<mozIStorageConnection> +nsDownloadManager::GetPrivateDBConnection() const +{ + nsCOMPtr<mozIStorageService> storage = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID); + NS_ENSURE_TRUE(storage, nullptr); + + nsCOMPtr<mozIStorageConnection> conn; + nsresult rv = storage->OpenSpecialDatabase("memory", getter_AddRefs(conn)); + NS_ENSURE_SUCCESS(rv, nullptr); + + return conn.forget(); +} + +void +nsDownloadManager::CloseAllDBs() +{ + CloseDB(mDBConn, mUpdateDownloadStatement, mGetIdsForURIStatement); + CloseDB(mPrivateDBConn, mUpdatePrivateDownloadStatement, mGetPrivateIdsForURIStatement); +} + +void +nsDownloadManager::CloseDB(mozIStorageConnection* aDBConn, + mozIStorageStatement* aUpdateStmt, + mozIStorageStatement* aGetIdsStmt) +{ + DebugOnly<nsresult> rv = aGetIdsStmt->Finalize(); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = aUpdateStmt->Finalize(); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = aDBConn->AsyncClose(nullptr); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +static nsresult +InitSQLFunctions(mozIStorageConnection* aDBConn) +{ + nsresult rv = mozilla::downloads::GenerateGUIDFunction::create(aDBConn); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +nsresult +nsDownloadManager::InitPrivateDB() +{ + bool ready = false; + if (mPrivateDBConn && NS_SUCCEEDED(mPrivateDBConn->GetConnectionReady(&ready)) && ready) + CloseDB(mPrivateDBConn, mUpdatePrivateDownloadStatement, mGetPrivateIdsForURIStatement); + mPrivateDBConn = GetPrivateDBConnection(); + if (!mPrivateDBConn) + return NS_ERROR_NOT_AVAILABLE; + + nsresult rv = InitSQLFunctions(mPrivateDBConn); + NS_ENSURE_SUCCESS(rv, rv); + + rv = CreateTable(mPrivateDBConn); + NS_ENSURE_SUCCESS(rv, rv); + + rv = InitStatements(mPrivateDBConn, getter_AddRefs(mUpdatePrivateDownloadStatement), + getter_AddRefs(mGetPrivateIdsForURIStatement)); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +nsresult +nsDownloadManager::InitFileDB() +{ + nsresult rv; + + nsCOMPtr<nsIFile> dbFile; + rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(dbFile)); + NS_ENSURE_SUCCESS(rv, rv); + rv = dbFile->Append(DM_DB_NAME); + NS_ENSURE_SUCCESS(rv, rv); + + bool ready = false; + if (mDBConn && NS_SUCCEEDED(mDBConn->GetConnectionReady(&ready)) && ready) + CloseDB(mDBConn, mUpdateDownloadStatement, mGetIdsForURIStatement); + mDBConn = GetFileDBConnection(dbFile); + NS_ENSURE_TRUE(mDBConn, NS_ERROR_NOT_AVAILABLE); + + rv = InitSQLFunctions(mDBConn); + NS_ENSURE_SUCCESS(rv, rv); + + bool tableExists; + rv = mDBConn->TableExists(NS_LITERAL_CSTRING("moz_downloads"), &tableExists); + NS_ENSURE_SUCCESS(rv, rv); + + if (!tableExists) { + rv = CreateTable(mDBConn); + NS_ENSURE_SUCCESS(rv, rv); + + // We're done with the initialization now and can skip the remaining + // upgrading logic. + return NS_OK; + } + + // Checking the database schema now + int32_t schemaVersion; + rv = mDBConn->GetSchemaVersion(&schemaVersion); + NS_ENSURE_SUCCESS(rv, rv); + + // Changing the database? Be sure to do these two things! + // 1) Increment DM_SCHEMA_VERSION + // 2) Implement the proper downgrade/upgrade code for the current version + + switch (schemaVersion) { + // Upgrading + // Every time you increment the database schema, you need to implement + // the upgrading code from the previous version to the new one. + // Also, don't forget to make a unit test to test your upgrading code! + case 1: // Drop a column (iconURL) from the database (bug 385875) + { + // Safely wrap this in a transaction so we don't hose the whole DB + mozStorageTransaction safeTransaction(mDBConn, true); + + // Create a temporary table that will store the existing records + rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TEMPORARY TABLE moz_downloads_backup (" + "id INTEGER PRIMARY KEY, " + "name TEXT, " + "source TEXT, " + "target TEXT, " + "startTime INTEGER, " + "endTime INTEGER, " + "state INTEGER" + ")")); + NS_ENSURE_SUCCESS(rv, rv); + + // Insert into a temporary table + rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO moz_downloads_backup " + "SELECT id, name, source, target, startTime, endTime, state " + "FROM moz_downloads")); + NS_ENSURE_SUCCESS(rv, rv); + + // Drop the old table + rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE moz_downloads")); + NS_ENSURE_SUCCESS(rv, rv); + + // Now recreate it with this schema version + rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE moz_downloads (" + "id INTEGER PRIMARY KEY, " + "name TEXT, " + "source TEXT, " + "target TEXT, " + "startTime INTEGER, " + "endTime INTEGER, " + "state INTEGER" + ")")); + NS_ENSURE_SUCCESS(rv, rv); + + // Insert the data back into it + rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO moz_downloads " + "SELECT id, name, source, target, startTime, endTime, state " + "FROM moz_downloads_backup")); + NS_ENSURE_SUCCESS(rv, rv); + + // And drop our temporary table + rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE moz_downloads_backup")); + NS_ENSURE_SUCCESS(rv, rv); + + // Finally, update the schemaVersion variable and the database schema + schemaVersion = 2; + rv = mDBConn->SetSchemaVersion(schemaVersion); + NS_ENSURE_SUCCESS(rv, rv); + } + // Fallthrough to the next upgrade + MOZ_FALLTHROUGH; + + case 2: // Add referrer column to the database + { + rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE moz_downloads " + "ADD COLUMN referrer TEXT")); + NS_ENSURE_SUCCESS(rv, rv); + + // Finally, update the schemaVersion variable and the database schema + schemaVersion = 3; + rv = mDBConn->SetSchemaVersion(schemaVersion); + NS_ENSURE_SUCCESS(rv, rv); + } + // Fallthrough to the next upgrade + MOZ_FALLTHROUGH; + + case 3: // This version adds a column to the database (entityID) + { + rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE moz_downloads " + "ADD COLUMN entityID TEXT")); + NS_ENSURE_SUCCESS(rv, rv); + + // Finally, update the schemaVersion variable and the database schema + schemaVersion = 4; + rv = mDBConn->SetSchemaVersion(schemaVersion); + NS_ENSURE_SUCCESS(rv, rv); + } + // Fallthrough to the next upgrade + MOZ_FALLTHROUGH; + + case 4: // This version adds a column to the database (tempPath) + { + rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE moz_downloads " + "ADD COLUMN tempPath TEXT")); + NS_ENSURE_SUCCESS(rv, rv); + + // Finally, update the schemaVersion variable and the database schema + schemaVersion = 5; + rv = mDBConn->SetSchemaVersion(schemaVersion); + NS_ENSURE_SUCCESS(rv, rv); + } + // Fallthrough to the next upgrade + MOZ_FALLTHROUGH; + + case 5: // This version adds two columns for tracking transfer progress + { + rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE moz_downloads " + "ADD COLUMN currBytes INTEGER NOT NULL DEFAULT 0")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE moz_downloads " + "ADD COLUMN maxBytes INTEGER NOT NULL DEFAULT -1")); + NS_ENSURE_SUCCESS(rv, rv); + + // Finally, update the schemaVersion variable and the database schema + schemaVersion = 6; + rv = mDBConn->SetSchemaVersion(schemaVersion); + NS_ENSURE_SUCCESS(rv, rv); + } + // Fallthrough to the next upgrade + MOZ_FALLTHROUGH; + + case 6: // This version adds three columns to DB (MIME type related info) + { + rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE moz_downloads " + "ADD COLUMN mimeType TEXT")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE moz_downloads " + "ADD COLUMN preferredApplication TEXT")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE moz_downloads " + "ADD COLUMN preferredAction INTEGER NOT NULL DEFAULT 0")); + NS_ENSURE_SUCCESS(rv, rv); + + // Finally, update the schemaVersion variable and the database schema + schemaVersion = 7; + rv = mDBConn->SetSchemaVersion(schemaVersion); + NS_ENSURE_SUCCESS(rv, rv); + } + // Fallthrough to next upgrade + MOZ_FALLTHROUGH; + + case 7: // This version adds a column to remember to auto-resume downloads + { + rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE moz_downloads " + "ADD COLUMN autoResume INTEGER NOT NULL DEFAULT 0")); + NS_ENSURE_SUCCESS(rv, rv); + + // Finally, update the schemaVersion variable and the database schema + schemaVersion = 8; + rv = mDBConn->SetSchemaVersion(schemaVersion); + NS_ENSURE_SUCCESS(rv, rv); + } + // Fallthrough to the next upgrade + MOZ_FALLTHROUGH; + + // Warning: schema versions >=8 must take into account that they can + // be operating on schemas from unknown, future versions that have + // been downgraded. Operations such as adding columns may fail, + // since the column may already exist. + + case 8: // This version adds a column for GUIDs + { + bool exists; + rv = mDBConn->IndexExists(NS_LITERAL_CSTRING("moz_downloads_guid_uniqueindex"), + &exists); + NS_ENSURE_SUCCESS(rv, rv); + if (!exists) { + rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE moz_downloads ADD COLUMN guid TEXT")); + NS_ENSURE_SUCCESS(rv, rv); + rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE UNIQUE INDEX moz_downloads_guid_uniqueindex ON moz_downloads (guid)")); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "UPDATE moz_downloads SET guid = GENERATE_GUID() WHERE guid ISNULL")); + NS_ENSURE_SUCCESS(rv, rv); + + // Finally, update the database schema + schemaVersion = 9; + rv = mDBConn->SetSchemaVersion(schemaVersion); + NS_ENSURE_SUCCESS(rv, rv); + } + // Fallthrough to the next upgrade + + // Extra sanity checking for developers +#ifndef DEBUG + MOZ_FALLTHROUGH; + case DM_SCHEMA_VERSION: +#endif + break; + + case 0: + { + NS_WARNING("Could not get download database's schema version!"); + + // The table may still be usable - someone may have just messed with the + // schema version, so let's just treat this like a downgrade and verify + // that the needed columns are there. If they aren't there, we'll drop + // the table anyway. + rv = mDBConn->SetSchemaVersion(DM_SCHEMA_VERSION); + NS_ENSURE_SUCCESS(rv, rv); + } + // Fallthrough to downgrade check + MOZ_FALLTHROUGH; + + // Downgrading + // If columns have been added to the table, we can still use the ones we + // understand safely. If columns have been deleted or alterd, we just + // drop the table and start from scratch. If you change how a column + // should be interpreted, make sure you also change its name so this + // check will catch it. + default: + { + nsCOMPtr<mozIStorageStatement> stmt; + rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT id, name, source, target, tempPath, startTime, endTime, state, " + "referrer, entityID, currBytes, maxBytes, mimeType, " + "preferredApplication, preferredAction, autoResume, guid " + "FROM moz_downloads"), getter_AddRefs(stmt)); + if (NS_SUCCEEDED(rv)) { + // We have a database that contains all of the elements that make up + // the latest known schema. Reset the version to force an upgrade + // path if this downgraded database is used in a later version. + mDBConn->SetSchemaVersion(DM_SCHEMA_VERSION); + break; + } + + // if the statement fails, that means all the columns were not there. + // First we backup the database + nsCOMPtr<mozIStorageService> storage = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID); + NS_ENSURE_TRUE(storage, NS_ERROR_NOT_AVAILABLE); + nsCOMPtr<nsIFile> backup; + rv = storage->BackupDatabaseFile(dbFile, DM_DB_CORRUPT_FILENAME, nullptr, + getter_AddRefs(backup)); + NS_ENSURE_SUCCESS(rv, rv); + + // Then we dump it + rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE moz_downloads")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = CreateTable(mDBConn); + NS_ENSURE_SUCCESS(rv, rv); + } + break; + } + + return NS_OK; +} + +nsresult +nsDownloadManager::CreateTable(mozIStorageConnection* aDBConn) +{ + nsresult rv = aDBConn->SetSchemaVersion(DM_SCHEMA_VERSION); + if (NS_FAILED(rv)) return rv; + + rv = aDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE moz_downloads (" + "id INTEGER PRIMARY KEY, " + "name TEXT, " + "source TEXT, " + "target TEXT, " + "tempPath TEXT, " + "startTime INTEGER, " + "endTime INTEGER, " + "state INTEGER, " + "referrer TEXT, " + "entityID TEXT, " + "currBytes INTEGER NOT NULL DEFAULT 0, " + "maxBytes INTEGER NOT NULL DEFAULT -1, " + "mimeType TEXT, " + "preferredApplication TEXT, " + "preferredAction INTEGER NOT NULL DEFAULT 0, " + "autoResume INTEGER NOT NULL DEFAULT 0, " + "guid TEXT" + ")")); + if (NS_FAILED(rv)) return rv; + + rv = aDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE UNIQUE INDEX moz_downloads_guid_uniqueindex " + "ON moz_downloads(guid)")); + return rv; +} + +nsresult +nsDownloadManager::RestoreDatabaseState() +{ + // Restore downloads that were in a scanning state. We can assume that they + // have been dealt with by the virus scanner + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( + "UPDATE moz_downloads " + "SET state = :state " + "WHERE state = :state_cond"), getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("state"), nsIDownloadManager::DOWNLOAD_FINISHED); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("state_cond"), nsIDownloadManager::DOWNLOAD_SCANNING); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + // Convert supposedly-active downloads into downloads that should auto-resume + rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( + "UPDATE moz_downloads " + "SET autoResume = :autoResume " + "WHERE state = :notStarted " + "OR state = :queued " + "OR state = :downloading"), getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("autoResume"), nsDownload::AUTO_RESUME); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("notStarted"), nsIDownloadManager::DOWNLOAD_NOTSTARTED); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("queued"), nsIDownloadManager::DOWNLOAD_QUEUED); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("downloading"), nsIDownloadManager::DOWNLOAD_DOWNLOADING); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + // Switch any download that is supposed to automatically resume and is in a + // finished state to *not* automatically resume. See Bug 409179 for details. + rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( + "UPDATE moz_downloads " + "SET autoResume = :autoResume " + "WHERE state = :state " + "AND autoResume = :autoResume_cond"), + getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("autoResume"), nsDownload::DONT_RESUME); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("state"), nsIDownloadManager::DOWNLOAD_FINISHED); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("autoResume_cond"), nsDownload::AUTO_RESUME); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +nsDownloadManager::RestoreActiveDownloads() +{ + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT id " + "FROM moz_downloads " + "WHERE (state = :state AND LENGTH(entityID) > 0) " + "OR autoResume != :autoResume"), getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("state"), nsIDownloadManager::DOWNLOAD_PAUSED); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("autoResume"), nsDownload::DONT_RESUME); + NS_ENSURE_SUCCESS(rv, rv); + + nsresult retVal = NS_OK; + bool hasResults; + while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResults)) && hasResults) { + RefPtr<nsDownload> dl; + // Keep trying to add even if we fail one, but make sure to return failure. + // Additionally, be careful to not call anything that tries to change the + // database because we're iterating over a live statement. + if (NS_FAILED(GetDownloadFromDB(stmt->AsInt32(0), getter_AddRefs(dl))) || + NS_FAILED(AddToCurrentDownloads(dl))) + retVal = NS_ERROR_FAILURE; + } + + // Try to resume only the downloads that should auto-resume + rv = ResumeAllDownloads(false); + NS_ENSURE_SUCCESS(rv, rv); + + return retVal; +} + +int64_t +nsDownloadManager::AddDownloadToDB(const nsAString &aName, + const nsACString &aSource, + const nsACString &aTarget, + const nsAString &aTempPath, + int64_t aStartTime, + int64_t aEndTime, + const nsACString &aMimeType, + const nsACString &aPreferredApp, + nsHandlerInfoAction aPreferredAction, + bool aPrivate, + nsACString& aNewGUID) +{ + mozIStorageConnection* dbConn = aPrivate ? mPrivateDBConn : mDBConn; + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = dbConn->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO moz_downloads " + "(name, source, target, tempPath, startTime, endTime, state, " + "mimeType, preferredApplication, preferredAction, guid) VALUES " + "(:name, :source, :target, :tempPath, :startTime, :endTime, :state, " + ":mimeType, :preferredApplication, :preferredAction, :guid)"), + getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, 0); + + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("name"), aName); + NS_ENSURE_SUCCESS(rv, 0); + + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("source"), aSource); + NS_ENSURE_SUCCESS(rv, 0); + + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("target"), aTarget); + NS_ENSURE_SUCCESS(rv, 0); + + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("tempPath"), aTempPath); + NS_ENSURE_SUCCESS(rv, 0); + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("startTime"), aStartTime); + NS_ENSURE_SUCCESS(rv, 0); + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("endTime"), aEndTime); + NS_ENSURE_SUCCESS(rv, 0); + + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("state"), nsIDownloadManager::DOWNLOAD_NOTSTARTED); + NS_ENSURE_SUCCESS(rv, 0); + + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("mimeType"), aMimeType); + NS_ENSURE_SUCCESS(rv, 0); + + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("preferredApplication"), aPreferredApp); + NS_ENSURE_SUCCESS(rv, 0); + + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("preferredAction"), aPreferredAction); + NS_ENSURE_SUCCESS(rv, 0); + + nsAutoCString guid; + rv = GenerateGUID(guid); + NS_ENSURE_SUCCESS(rv, 0); + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), guid); + NS_ENSURE_SUCCESS(rv, 0); + + bool hasMore; + rv = stmt->ExecuteStep(&hasMore); // we want to keep our lock + NS_ENSURE_SUCCESS(rv, 0); + + int64_t id = 0; + rv = dbConn->GetLastInsertRowID(&id); + NS_ENSURE_SUCCESS(rv, 0); + + aNewGUID = guid; + + // lock on DB from statement will be released once we return + return id; +} + +nsresult +nsDownloadManager::InitDB() +{ + nsresult rv = InitPrivateDB(); + NS_ENSURE_SUCCESS(rv, rv); + + rv = InitFileDB(); + NS_ENSURE_SUCCESS(rv, rv); + + rv = InitStatements(mDBConn, getter_AddRefs(mUpdateDownloadStatement), + getter_AddRefs(mGetIdsForURIStatement)); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +nsresult +nsDownloadManager::InitStatements(mozIStorageConnection* aDBConn, + mozIStorageStatement** aUpdateStatement, + mozIStorageStatement** aGetIdsStatement) +{ + nsresult rv = aDBConn->CreateStatement(NS_LITERAL_CSTRING( + "UPDATE moz_downloads " + "SET tempPath = :tempPath, startTime = :startTime, endTime = :endTime, " + "state = :state, referrer = :referrer, entityID = :entityID, " + "currBytes = :currBytes, maxBytes = :maxBytes, autoResume = :autoResume " + "WHERE id = :id"), aUpdateStatement); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aDBConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT guid " + "FROM moz_downloads " + "WHERE source = :source"), aGetIdsStatement); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +nsDownloadManager::Init() +{ + nsresult rv; + + nsCOMPtr<nsIStringBundleService> bundleService = + mozilla::services::GetStringBundleService(); + if (!bundleService) + return NS_ERROR_FAILURE; + + rv = bundleService->CreateBundle(DOWNLOAD_MANAGER_BUNDLE, + getter_AddRefs(mBundle)); + NS_ENSURE_SUCCESS(rv, rv); + +#if !defined(MOZ_JSDOWNLOADS) + // When MOZ_JSDOWNLOADS is undefined, we still check the preference that can + // be used to enable the JavaScript API during the migration process. + mUseJSTransfer = Preferences::GetBool(PREF_BD_USEJSTRANSFER, false); +#else + mUseJSTransfer = true; +#endif + + if (mUseJSTransfer) + return NS_OK; + + // Clean up any old downloads.rdf files from before Firefox 3 + { + nsCOMPtr<nsIFile> oldDownloadsFile; + bool fileExists; + if (NS_SUCCEEDED(NS_GetSpecialDirectory(NS_APP_DOWNLOADS_50_FILE, + getter_AddRefs(oldDownloadsFile))) && + NS_SUCCEEDED(oldDownloadsFile->Exists(&fileExists)) && + fileExists) { + (void)oldDownloadsFile->Remove(false); + } + } + + mObserverService = mozilla::services::GetObserverService(); + if (!mObserverService) + return NS_ERROR_FAILURE; + + rv = InitDB(); + NS_ENSURE_SUCCESS(rv, rv); + +#ifdef DOWNLOAD_SCANNER + mScanner = new nsDownloadScanner(); + if (!mScanner) + return NS_ERROR_OUT_OF_MEMORY; + rv = mScanner->Init(); + if (NS_FAILED(rv)) { + delete mScanner; + mScanner = nullptr; + } +#endif + + // Do things *after* initializing various download manager properties such as + // restoring downloads to a consistent state + rv = RestoreDatabaseState(); + NS_ENSURE_SUCCESS(rv, rv); + + rv = RestoreActiveDownloads(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "Failed to restore all active downloads"); + + nsCOMPtr<nsINavHistoryService> history = + do_GetService(NS_NAVHISTORYSERVICE_CONTRACTID); + + (void)mObserverService->NotifyObservers( + static_cast<nsIDownloadManager *>(this), + "download-manager-initialized", + nullptr); + + // The following AddObserver calls must be the last lines in this function, + // because otherwise, this function may fail (and thus, this object would be not + // completely initialized), but the observerservice would still keep a reference + // to us and notify us about shutdown, which may cause crashes. + // failure to add an observer is not critical + (void)mObserverService->AddObserver(this, "quit-application", true); + (void)mObserverService->AddObserver(this, "quit-application-requested", true); + (void)mObserverService->AddObserver(this, "offline-requested", true); + (void)mObserverService->AddObserver(this, "sleep_notification", true); + (void)mObserverService->AddObserver(this, "wake_notification", true); + (void)mObserverService->AddObserver(this, "suspend_process_notification", true); + (void)mObserverService->AddObserver(this, "resume_process_notification", true); + (void)mObserverService->AddObserver(this, "profile-before-change", true); + (void)mObserverService->AddObserver(this, NS_IOSERVICE_GOING_OFFLINE_TOPIC, true); + (void)mObserverService->AddObserver(this, NS_IOSERVICE_OFFLINE_STATUS_TOPIC, true); + (void)mObserverService->AddObserver(this, "last-pb-context-exited", true); + (void)mObserverService->AddObserver(this, "last-pb-context-exiting", true); + + if (history) + (void)history->AddObserver(this, true); + + return NS_OK; +} + +int32_t +nsDownloadManager::GetRetentionBehavior() +{ + // We use 0 as the default, which is "remove when done" + nsresult rv; + nsCOMPtr<nsIPrefBranch> pref = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, 0); + + int32_t val; + rv = pref->GetIntPref(PREF_BDM_RETENTION, &val); + NS_ENSURE_SUCCESS(rv, 0); + + // Allow the Downloads Panel to change the retention behavior. We do this to + // allow proper migration to the new feature when using the same profile on + // multiple versions of the product (bug 697678). Implementation note: in + // order to allow observers to change the retention value, we have to pass an + // object in the aSubject parameter, we cannot use aData for that. + nsCOMPtr<nsISupportsPRInt32> retentionBehavior = + do_CreateInstance(NS_SUPPORTS_PRINT32_CONTRACTID); + retentionBehavior->SetData(val); + (void)mObserverService->NotifyObservers(retentionBehavior, + "download-manager-change-retention", + nullptr); + retentionBehavior->GetData(&val); + + return val; +} + +enum nsDownloadManager::QuitBehavior +nsDownloadManager::GetQuitBehavior() +{ + // We use 0 as the default, which is "remember and resume the download" + nsresult rv; + nsCOMPtr<nsIPrefBranch> pref = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, QUIT_AND_RESUME); + + int32_t val; + rv = pref->GetIntPref(PREF_BDM_QUITBEHAVIOR, &val); + NS_ENSURE_SUCCESS(rv, QUIT_AND_RESUME); + + switch (val) { + case 1: + return QUIT_AND_PAUSE; + case 2: + return QUIT_AND_CANCEL; + default: + return QUIT_AND_RESUME; + } +} + +// Using a globally-unique GUID, search all databases (both private and public). +// A return value of NS_ERROR_NOT_AVAILABLE means no download with the given GUID +// could be found, either private or public. + +nsresult +nsDownloadManager::GetDownloadFromDB(const nsACString& aGUID, nsDownload **retVal) +{ + MOZ_ASSERT(!FindDownload(aGUID), + "If it is a current download, you should not call this method!"); + + NS_NAMED_LITERAL_CSTRING(query, + "SELECT id, state, startTime, source, target, tempPath, name, referrer, " + "entityID, currBytes, maxBytes, mimeType, preferredAction, " + "preferredApplication, autoResume, guid " + "FROM moz_downloads " + "WHERE guid = :guid"); + // First, let's query the database and see if it even exists + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = mDBConn->CreateStatement(query, getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), aGUID); + NS_ENSURE_SUCCESS(rv, rv); + + rv = GetDownloadFromDB(mDBConn, stmt, retVal); + + // If the download cannot be found in the public database, try again + // in the private one. Otherwise, return whatever successful result + // or failure obtained from the public database. + if (rv == NS_ERROR_NOT_AVAILABLE) { + rv = mPrivateDBConn->CreateStatement(query, getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), aGUID); + NS_ENSURE_SUCCESS(rv, rv); + + rv = GetDownloadFromDB(mPrivateDBConn, stmt, retVal); + + // Only if it still cannot be found do we report the failure. + if (rv == NS_ERROR_NOT_AVAILABLE) { + *retVal = nullptr; + } + } + return rv; +} + +nsresult +nsDownloadManager::GetDownloadFromDB(uint32_t aID, nsDownload **retVal) +{ + NS_WARNING("Using integer IDs without compat mode enabled"); + + MOZ_ASSERT(!FindDownload(aID), + "If it is a current download, you should not call this method!"); + + // First, let's query the database and see if it even exists + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT id, state, startTime, source, target, tempPath, name, referrer, " + "entityID, currBytes, maxBytes, mimeType, preferredAction, " + "preferredApplication, autoResume, guid " + "FROM moz_downloads " + "WHERE id = :id"), getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), aID); + NS_ENSURE_SUCCESS(rv, rv); + + return GetDownloadFromDB(mDBConn, stmt, retVal); +} + +nsresult +nsDownloadManager::GetDownloadFromDB(mozIStorageConnection* aDBConn, + mozIStorageStatement* stmt, + nsDownload **retVal) +{ + bool hasResults = false; + nsresult rv = stmt->ExecuteStep(&hasResults); + if (NS_FAILED(rv) || !hasResults) + return NS_ERROR_NOT_AVAILABLE; + + // We have a download, so lets create it + RefPtr<nsDownload> dl = new nsDownload(); + if (!dl) + return NS_ERROR_OUT_OF_MEMORY; + dl->mPrivate = aDBConn == mPrivateDBConn; + + dl->mDownloadManager = this; + + int32_t i = 0; + // Setting all properties of the download now + dl->mCancelable = nullptr; + dl->mID = stmt->AsInt64(i++); + dl->mDownloadState = stmt->AsInt32(i++); + dl->mStartTime = stmt->AsInt64(i++); + + nsCString source; + stmt->GetUTF8String(i++, source); + rv = NS_NewURI(getter_AddRefs(dl->mSource), source); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString target; + stmt->GetUTF8String(i++, target); + rv = NS_NewURI(getter_AddRefs(dl->mTarget), target); + NS_ENSURE_SUCCESS(rv, rv); + + nsString tempPath; + stmt->GetString(i++, tempPath); + if (!tempPath.IsEmpty()) { + rv = NS_NewLocalFile(tempPath, true, getter_AddRefs(dl->mTempFile)); + NS_ENSURE_SUCCESS(rv, rv); + } + + stmt->GetString(i++, dl->mDisplayName); + + nsCString referrer; + rv = stmt->GetUTF8String(i++, referrer); + if (NS_SUCCEEDED(rv) && !referrer.IsEmpty()) { + rv = NS_NewURI(getter_AddRefs(dl->mReferrer), referrer); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = stmt->GetUTF8String(i++, dl->mEntityID); + NS_ENSURE_SUCCESS(rv, rv); + + int64_t currBytes = stmt->AsInt64(i++); + int64_t maxBytes = stmt->AsInt64(i++); + dl->SetProgressBytes(currBytes, maxBytes); + + // Build mMIMEInfo only if the mimeType in DB is not empty + nsAutoCString mimeType; + rv = stmt->GetUTF8String(i++, mimeType); + NS_ENSURE_SUCCESS(rv, rv); + + if (!mimeType.IsEmpty()) { + nsCOMPtr<nsIMIMEService> mimeService = + do_GetService(NS_MIMESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mimeService->GetFromTypeAndExtension(mimeType, EmptyCString(), + getter_AddRefs(dl->mMIMEInfo)); + NS_ENSURE_SUCCESS(rv, rv); + + nsHandlerInfoAction action = stmt->AsInt32(i++); + rv = dl->mMIMEInfo->SetPreferredAction(action); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString persistentDescriptor; + rv = stmt->GetUTF8String(i++, persistentDescriptor); + NS_ENSURE_SUCCESS(rv, rv); + + if (!persistentDescriptor.IsEmpty()) { + nsCOMPtr<nsILocalHandlerApp> handler = + do_CreateInstance(NS_LOCALHANDLERAPP_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> localExecutable; + rv = NS_NewNativeLocalFile(EmptyCString(), false, + getter_AddRefs(localExecutable)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = localExecutable->SetPersistentDescriptor(persistentDescriptor); + NS_ENSURE_SUCCESS(rv, rv); + + rv = handler->SetExecutable(localExecutable); + NS_ENSURE_SUCCESS(rv, rv); + + rv = dl->mMIMEInfo->SetPreferredApplicationHandler(handler); + NS_ENSURE_SUCCESS(rv, rv); + } + } else { + // Compensate for the i++s skipped in the true block + i += 2; + } + + dl->mAutoResume = + static_cast<enum nsDownload::AutoResume>(stmt->AsInt32(i++)); + + rv = stmt->GetUTF8String(i++, dl->mGUID); + NS_ENSURE_SUCCESS(rv, rv); + + // Handle situations where we load a download from a database that has been + // used in an older version and not gone through the upgrade path (ie. it + // contains empty GUID entries). + if (dl->mGUID.IsEmpty()) { + rv = GenerateGUID(dl->mGUID); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<mozIStorageStatement> updateStmt; + rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( + "UPDATE moz_downloads SET guid = :guid " + "WHERE id = :id"), + getter_AddRefs(updateStmt)); + NS_ENSURE_SUCCESS(rv, rv); + rv = updateStmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), dl->mGUID); + NS_ENSURE_SUCCESS(rv, rv); + rv = updateStmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), dl->mID); + NS_ENSURE_SUCCESS(rv, rv); + rv = updateStmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Addrefing and returning + dl.forget(retVal); + return NS_OK; +} + +nsresult +nsDownloadManager::AddToCurrentDownloads(nsDownload *aDl) +{ + nsCOMArray<nsDownload>& currentDownloads = + aDl->mPrivate ? mCurrentPrivateDownloads : mCurrentDownloads; + if (!currentDownloads.AppendObject(aDl)) + return NS_ERROR_OUT_OF_MEMORY; + + aDl->mDownloadManager = this; + return NS_OK; +} + +void +nsDownloadManager::SendEvent(nsDownload *aDownload, const char *aTopic) +{ + (void)mObserverService->NotifyObservers(aDownload, aTopic, nullptr); +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsIDownloadManager + +NS_IMETHODIMP +nsDownloadManager::GetActivePrivateDownloadCount(int32_t* aResult) +{ + NS_ENSURE_STATE(!mUseJSTransfer); + + *aResult = mCurrentPrivateDownloads.Count(); + return NS_OK; +} + +NS_IMETHODIMP +nsDownloadManager::GetActiveDownloadCount(int32_t *aResult) +{ + NS_ENSURE_STATE(!mUseJSTransfer); + + *aResult = mCurrentDownloads.Count(); + + return NS_OK; +} + +NS_IMETHODIMP +nsDownloadManager::GetActiveDownloads(nsISimpleEnumerator **aResult) +{ + NS_ENSURE_STATE(!mUseJSTransfer); + + return NS_NewArrayEnumerator(aResult, mCurrentDownloads); +} + +NS_IMETHODIMP +nsDownloadManager::GetActivePrivateDownloads(nsISimpleEnumerator **aResult) +{ + NS_ENSURE_STATE(!mUseJSTransfer); + + return NS_NewArrayEnumerator(aResult, mCurrentPrivateDownloads); +} + +/** + * For platforms where helper apps use the downloads directory (i.e. mobile), + * this should be kept in sync with nsExternalHelperAppService.cpp + */ +NS_IMETHODIMP +nsDownloadManager::GetDefaultDownloadsDirectory(nsIFile **aResult) +{ + nsCOMPtr<nsIFile> downloadDir; + + nsresult rv; + nsCOMPtr<nsIProperties> dirService = + do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // OSX 10.4: + // Desktop + // OSX 10.5: + // User download directory + // Vista: + // Downloads + // XP/2K: + // My Documents/Downloads + // Linux: + // XDG user dir spec, with a fallback to Home/Downloads + + nsXPIDLString folderName; + mBundle->GetStringFromName(u"downloadsFolder", + getter_Copies(folderName)); + +#if defined(XP_WIN) + rv = dirService->Get(NS_WIN_DEFAULT_DOWNLOAD_DIR, + NS_GET_IID(nsIFile), + getter_AddRefs(downloadDir)); + NS_ENSURE_SUCCESS(rv, rv); + + // Check the os version + nsCOMPtr<nsIPropertyBag2> infoService = + do_GetService(NS_SYSTEMINFO_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t version; + NS_NAMED_LITERAL_STRING(osVersion, "version"); + rv = infoService->GetPropertyAsInt32(osVersion, &version); + NS_ENSURE_SUCCESS(rv, rv); + if (version < 6) { // XP/2K + // First get "My Documents" + rv = dirService->Get(NS_WIN_PERSONAL_DIR, + NS_GET_IID(nsIFile), + getter_AddRefs(downloadDir)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = downloadDir->Append(folderName); + NS_ENSURE_SUCCESS(rv, rv); + + // This could be the first time we are creating the downloads folder in My + // Documents, so make sure it exists. + bool exists; + rv = downloadDir->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + if (!exists) { + rv = downloadDir->Create(nsIFile::DIRECTORY_TYPE, 0755); + NS_ENSURE_SUCCESS(rv, rv); + } + } +#elif defined(XP_UNIX) + rv = dirService->Get(NS_UNIX_DEFAULT_DOWNLOAD_DIR, + NS_GET_IID(nsIFile), + getter_AddRefs(downloadDir)); + // fallback to Home/Downloads + if (NS_FAILED(rv)) { + rv = dirService->Get(NS_UNIX_HOME_DIR, + NS_GET_IID(nsIFile), + getter_AddRefs(downloadDir)); + NS_ENSURE_SUCCESS(rv, rv); + rv = downloadDir->Append(folderName); + NS_ENSURE_SUCCESS(rv, rv); + } +#else + rv = dirService->Get(NS_OS_HOME_DIR, + NS_GET_IID(nsIFile), + getter_AddRefs(downloadDir)); + NS_ENSURE_SUCCESS(rv, rv); + rv = downloadDir->Append(folderName); + NS_ENSURE_SUCCESS(rv, rv); +#endif + + downloadDir.forget(aResult); + + return NS_OK; +} + +#define NS_BRANCH_DOWNLOAD "browser.download." +#define NS_PREF_FOLDERLIST "folderList" +#define NS_PREF_DIR "dir" + +NS_IMETHODIMP +nsDownloadManager::GetUserDownloadsDirectory(nsIFile **aResult) +{ + nsresult rv; + nsCOMPtr<nsIProperties> dirService = + do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrefService> prefService = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrefBranch> prefBranch; + rv = prefService->GetBranch(NS_BRANCH_DOWNLOAD, + getter_AddRefs(prefBranch)); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t val; + rv = prefBranch->GetIntPref(NS_PREF_FOLDERLIST, + &val); + NS_ENSURE_SUCCESS(rv, rv); + + switch(val) { + case 0: // Desktop + { + nsCOMPtr<nsIFile> downloadDir; + rv = dirService->Get(NS_OS_DESKTOP_DIR, + NS_GET_IID(nsIFile), + getter_AddRefs(downloadDir)); + NS_ENSURE_SUCCESS(rv, rv); + downloadDir.forget(aResult); + return NS_OK; + } + break; + case 1: // Downloads + return GetDefaultDownloadsDirectory(aResult); + case 2: // Custom + { + nsCOMPtr<nsIFile> customDirectory; + prefBranch->GetComplexValue(NS_PREF_DIR, + NS_GET_IID(nsIFile), + getter_AddRefs(customDirectory)); + if (customDirectory) { + bool exists = false; + (void)customDirectory->Exists(&exists); + + if (!exists) { + rv = customDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755); + if (NS_SUCCEEDED(rv)) { + customDirectory.forget(aResult); + return NS_OK; + } + + // Create failed, so it still doesn't exist. Fall out and get the + // default downloads directory. + } + + bool writable = false; + bool directory = false; + (void)customDirectory->IsWritable(&writable); + (void)customDirectory->IsDirectory(&directory); + + if (exists && writable && directory) { + customDirectory.forget(aResult); + return NS_OK; + } + } + rv = GetDefaultDownloadsDirectory(aResult); + if (NS_SUCCEEDED(rv)) { + (void)prefBranch->SetComplexValue(NS_PREF_DIR, + NS_GET_IID(nsIFile), + *aResult); + } + return rv; + } + break; + } + return NS_ERROR_INVALID_ARG; +} + +NS_IMETHODIMP +nsDownloadManager::AddDownload(DownloadType aDownloadType, + nsIURI *aSource, + nsIURI *aTarget, + const nsAString& aDisplayName, + nsIMIMEInfo *aMIMEInfo, + PRTime aStartTime, + nsIFile *aTempFile, + nsICancelable *aCancelable, + bool aIsPrivate, + nsIDownload **aDownload) +{ + NS_ENSURE_STATE(!mUseJSTransfer); + + NS_ENSURE_ARG_POINTER(aSource); + NS_ENSURE_ARG_POINTER(aTarget); + NS_ENSURE_ARG_POINTER(aDownload); + + nsresult rv; + + // target must be on the local filesystem + nsCOMPtr<nsIFileURL> targetFileURL = do_QueryInterface(aTarget, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> targetFile; + rv = targetFileURL->GetFile(getter_AddRefs(targetFile)); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<nsDownload> dl = new nsDownload(); + if (!dl) + return NS_ERROR_OUT_OF_MEMORY; + + // give our new nsIDownload some info so it's ready to go off into the world + dl->mTarget = aTarget; + dl->mSource = aSource; + dl->mTempFile = aTempFile; + dl->mPrivate = aIsPrivate; + + dl->mDisplayName = aDisplayName; + if (dl->mDisplayName.IsEmpty()) + targetFile->GetLeafName(dl->mDisplayName); + + dl->mMIMEInfo = aMIMEInfo; + dl->SetStartTime(aStartTime == 0 ? PR_Now() : aStartTime); + + // Creates a cycle that will be broken when the download finishes + dl->mCancelable = aCancelable; + + // Adding to the DB + nsAutoCString source, target; + rv = aSource->GetSpec(source); + NS_ENSURE_SUCCESS(rv, rv); + rv = aTarget->GetSpec(target); + NS_ENSURE_SUCCESS(rv, rv); + + // Track the temp file for exthandler downloads + nsAutoString tempPath; + if (aTempFile) + aTempFile->GetPath(tempPath); + + // Break down MIMEInfo but don't panic if we can't get all the pieces - we + // can still download the file + nsAutoCString persistentDescriptor, mimeType; + nsHandlerInfoAction action = nsIMIMEInfo::saveToDisk; + if (aMIMEInfo) { + (void)aMIMEInfo->GetType(mimeType); + + nsCOMPtr<nsIHandlerApp> handlerApp; + (void)aMIMEInfo->GetPreferredApplicationHandler(getter_AddRefs(handlerApp)); + nsCOMPtr<nsILocalHandlerApp> locHandlerApp = do_QueryInterface(handlerApp); + + if (locHandlerApp) { + nsCOMPtr<nsIFile> executable; + (void)locHandlerApp->GetExecutable(getter_AddRefs(executable)); + Unused << executable->GetPersistentDescriptor(persistentDescriptor); + } + + (void)aMIMEInfo->GetPreferredAction(&action); + } + + int64_t id = AddDownloadToDB(dl->mDisplayName, source, target, tempPath, + dl->mStartTime, dl->mLastUpdate, + mimeType, persistentDescriptor, action, + dl->mPrivate, dl->mGUID /* outparam */); + NS_ENSURE_TRUE(id, NS_ERROR_FAILURE); + dl->mID = id; + + rv = AddToCurrentDownloads(dl); + (void)dl->SetState(nsIDownloadManager::DOWNLOAD_QUEUED); + NS_ENSURE_SUCCESS(rv, rv); + +#ifdef DOWNLOAD_SCANNER + if (mScanner) { + bool scan = true; + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (prefs) { + (void)prefs->GetBoolPref(PREF_BDM_SCANWHENDONE, &scan); + } + // We currently apply local security policy to downloads when we scan + // via windows all-in-one download security api. The CheckPolicy call + // below is a pre-emptive part of that process. So tie applying security + // zone policy settings when downloads are intiated to the same pref + // that triggers applying security zone policy settings after a download + // completes. (bug 504804) + if (scan) { + AVCheckPolicyState res = mScanner->CheckPolicy(aSource, aTarget); + if (res == AVPOLICY_BLOCKED) { + // This download will get deleted during a call to IAE's Save, + // so go ahead and mark it as blocked and avoid the download. + (void)CancelDownload(id); + (void)dl->SetState(nsIDownloadManager::DOWNLOAD_BLOCKED_POLICY); + } + } + } +#endif + + // Check with parental controls to see if file downloads + // are allowed for this user. If not allowed, cancel the + // download and mark its state as being blocked. + nsCOMPtr<nsIParentalControlsService> pc = + do_CreateInstance(NS_PARENTALCONTROLSSERVICE_CONTRACTID); + if (pc) { + bool enabled = false; + (void)pc->GetBlockFileDownloadsEnabled(&enabled); + if (enabled) { + (void)CancelDownload(id); + (void)dl->SetState(nsIDownloadManager::DOWNLOAD_BLOCKED_PARENTAL); + } + + // Log the event if required by pc settings. + bool logEnabled = false; + (void)pc->GetLoggingEnabled(&logEnabled); + if (logEnabled) { + (void)pc->Log(nsIParentalControlsService::ePCLog_FileDownload, + enabled, + aSource, + nullptr); + } + } + + dl.forget(aDownload); + + return NS_OK; +} + +NS_IMETHODIMP +nsDownloadManager::GetDownload(uint32_t aID, nsIDownload **aDownloadItem) +{ + NS_ENSURE_STATE(!mUseJSTransfer); + + NS_WARNING("Using integer IDs without compat mode enabled"); + + nsDownload *itm = FindDownload(aID); + + RefPtr<nsDownload> dl; + if (!itm) { + nsresult rv = GetDownloadFromDB(aID, getter_AddRefs(dl)); + NS_ENSURE_SUCCESS(rv, rv); + + itm = dl.get(); + } + + NS_ADDREF(*aDownloadItem = itm); + + return NS_OK; +} + +namespace { +class AsyncResult : public Runnable +{ +public: + AsyncResult(nsresult aStatus, nsIDownload* aResult, + nsIDownloadManagerResult* aCallback) + : mStatus(aStatus), mResult(aResult), mCallback(aCallback) + { + } + + NS_IMETHOD Run() override + { + mCallback->HandleResult(mStatus, mResult); + return NS_OK; + } + +private: + nsresult mStatus; + nsCOMPtr<nsIDownload> mResult; + nsCOMPtr<nsIDownloadManagerResult> mCallback; +}; +} // namespace + +NS_IMETHODIMP +nsDownloadManager::GetDownloadByGUID(const nsACString& aGUID, + nsIDownloadManagerResult* aCallback) +{ + NS_ENSURE_STATE(!mUseJSTransfer); + + nsDownload *itm = FindDownload(aGUID); + + nsresult rv = NS_OK; + RefPtr<nsDownload> dl; + if (!itm) { + rv = GetDownloadFromDB(aGUID, getter_AddRefs(dl)); + itm = dl.get(); + } + + RefPtr<AsyncResult> runnable = new AsyncResult(rv, itm, aCallback); + NS_DispatchToMainThread(runnable); + return NS_OK; +} + +nsDownload * +nsDownloadManager::FindDownload(uint32_t aID) +{ + // we shouldn't ever have many downloads, so we can loop over them + for (int32_t i = mCurrentDownloads.Count() - 1; i >= 0; --i) { + nsDownload *dl = mCurrentDownloads[i]; + if (dl->mID == aID) + return dl; + } + + return nullptr; +} + +nsDownload * +nsDownloadManager::FindDownload(const nsACString& aGUID) +{ + // we shouldn't ever have many downloads, so we can loop over them + for (int32_t i = mCurrentDownloads.Count() - 1; i >= 0; --i) { + nsDownload *dl = mCurrentDownloads[i]; + if (dl->mGUID == aGUID) + return dl; + } + + for (int32_t i = mCurrentPrivateDownloads.Count() - 1; i >= 0; --i) { + nsDownload *dl = mCurrentPrivateDownloads[i]; + if (dl->mGUID == aGUID) + return dl; + } + + return nullptr; +} + +NS_IMETHODIMP +nsDownloadManager::CancelDownload(uint32_t aID) +{ + NS_ENSURE_STATE(!mUseJSTransfer); + + NS_WARNING("Using integer IDs without compat mode enabled"); + + // We AddRef here so we don't lose access to member variables when we remove + RefPtr<nsDownload> dl = FindDownload(aID); + + // if it's null, someone passed us a bad id. + if (!dl) + return NS_ERROR_FAILURE; + + return dl->Cancel(); +} + +nsresult +nsDownloadManager::RetryDownload(const nsACString& aGUID) +{ + RefPtr<nsDownload> dl; + nsresult rv = GetDownloadFromDB(aGUID, getter_AddRefs(dl)); + NS_ENSURE_SUCCESS(rv, rv); + + return RetryDownload(dl); +} + +NS_IMETHODIMP +nsDownloadManager::RetryDownload(uint32_t aID) +{ + NS_ENSURE_STATE(!mUseJSTransfer); + + NS_WARNING("Using integer IDs without compat mode enabled"); + + RefPtr<nsDownload> dl; + nsresult rv = GetDownloadFromDB(aID, getter_AddRefs(dl)); + NS_ENSURE_SUCCESS(rv, rv); + + return RetryDownload(dl); +} + +nsresult +nsDownloadManager::RetryDownload(nsDownload* dl) +{ + // if our download is not canceled or failed, we should fail + if (dl->mDownloadState != nsIDownloadManager::DOWNLOAD_FAILED && + dl->mDownloadState != nsIDownloadManager::DOWNLOAD_BLOCKED_PARENTAL && + dl->mDownloadState != nsIDownloadManager::DOWNLOAD_BLOCKED_POLICY && + dl->mDownloadState != nsIDownloadManager::DOWNLOAD_DIRTY && + dl->mDownloadState != nsIDownloadManager::DOWNLOAD_CANCELED) + return NS_ERROR_FAILURE; + + // If the download has failed and is resumable then we first try resuming it + nsresult rv; + if (dl->mDownloadState == nsIDownloadManager::DOWNLOAD_FAILED && dl->IsResumable()) { + rv = dl->Resume(); + if (NS_SUCCEEDED(rv)) + return rv; + } + + rv = NotifyDownloadRemoval(dl); + NS_ENSURE_SUCCESS(rv, rv); + + // reset time and download progress + dl->SetStartTime(PR_Now()); + dl->SetProgressBytes(0, -1); + + nsCOMPtr<nsIWebBrowserPersist> wbp = + do_CreateInstance("@mozilla.org/embedding/browser/nsWebBrowserPersist;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = wbp->SetPersistFlags(nsIWebBrowserPersist::PERSIST_FLAGS_REPLACE_EXISTING_FILES | + nsIWebBrowserPersist::PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION); + NS_ENSURE_SUCCESS(rv, rv); + + rv = AddToCurrentDownloads(dl); + NS_ENSURE_SUCCESS(rv, rv); + + rv = dl->SetState(nsIDownloadManager::DOWNLOAD_QUEUED); + NS_ENSURE_SUCCESS(rv, rv); + + // Creates a cycle that will be broken when the download finishes + dl->mCancelable = wbp; + (void)wbp->SetProgressListener(dl); + + // referrer policy can be anything since referrer is nullptr + rv = wbp->SavePrivacyAwareURI(dl->mSource, nullptr, + nullptr, mozilla::net::RP_Default, + nullptr, nullptr, + dl->mTarget, dl->mPrivate); + if (NS_FAILED(rv)) { + dl->mCancelable = nullptr; + (void)wbp->SetProgressListener(nullptr); + return rv; + } + + return NS_OK; +} + +static nsresult +RemoveDownloadByGUID(const nsACString& aGUID, mozIStorageConnection* aDBConn) +{ + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = aDBConn->CreateStatement(NS_LITERAL_CSTRING( + "DELETE FROM moz_downloads " + "WHERE guid = :guid"), getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), aGUID); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +nsDownloadManager::RemoveDownload(const nsACString& aGUID) +{ + RefPtr<nsDownload> dl = FindDownload(aGUID); + MOZ_ASSERT(!dl, "Can't call RemoveDownload on a download in progress!"); + if (dl) + return NS_ERROR_FAILURE; + + nsresult rv = GetDownloadFromDB(aGUID, getter_AddRefs(dl)); + NS_ENSURE_SUCCESS(rv, rv); + + if (dl->mPrivate) { + RemoveDownloadByGUID(aGUID, mPrivateDBConn); + } else { + RemoveDownloadByGUID(aGUID, mDBConn); + } + + return NotifyDownloadRemoval(dl); +} + +NS_IMETHODIMP +nsDownloadManager::RemoveDownload(uint32_t aID) +{ + NS_ENSURE_STATE(!mUseJSTransfer); + + NS_WARNING("Using integer IDs without compat mode enabled"); + + RefPtr<nsDownload> dl = FindDownload(aID); + MOZ_ASSERT(!dl, "Can't call RemoveDownload on a download in progress!"); + if (dl) + return NS_ERROR_FAILURE; + + nsresult rv = GetDownloadFromDB(aID, getter_AddRefs(dl)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<mozIStorageStatement> stmt; + rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( + "DELETE FROM moz_downloads " + "WHERE id = :id"), getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), aID); // unsigned; 64-bit to prevent overflow + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + // Notify the UI with the topic and download id + return NotifyDownloadRemoval(dl); +} + +nsresult +nsDownloadManager::NotifyDownloadRemoval(nsDownload* aRemoved) +{ + nsCOMPtr<nsISupportsPRUint32> id; + nsCOMPtr<nsISupportsCString> guid; + nsresult rv; + + // Only send an integer ID notification if the download is public. + bool sendDeprecatedNotification = !(aRemoved && aRemoved->mPrivate); + + if (sendDeprecatedNotification && aRemoved) { + id = do_CreateInstance(NS_SUPPORTS_PRUINT32_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t dlID; + rv = aRemoved->GetId(&dlID); + NS_ENSURE_SUCCESS(rv, rv); + rv = id->SetData(dlID); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (sendDeprecatedNotification) { + mObserverService->NotifyObservers(id, + "download-manager-remove-download", + nullptr); + } + + if (aRemoved) { + guid = do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoCString guidStr; + rv = aRemoved->GetGuid(guidStr); + NS_ENSURE_SUCCESS(rv, rv); + rv = guid->SetData(guidStr); + NS_ENSURE_SUCCESS(rv, rv); + } + + mObserverService->NotifyObservers(guid, + "download-manager-remove-download-guid", + nullptr); + return NS_OK; +} + +static nsresult +DoRemoveDownloadsByTimeframe(mozIStorageConnection* aDBConn, + int64_t aStartTime, + int64_t aEndTime) +{ + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = aDBConn->CreateStatement(NS_LITERAL_CSTRING( + "DELETE FROM moz_downloads " + "WHERE startTime >= :startTime " + "AND startTime <= :endTime " + "AND state NOT IN (:downloading, :paused, :queued)"), getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + + // Bind the times + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("startTime"), aStartTime); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("endTime"), aEndTime); + NS_ENSURE_SUCCESS(rv, rv); + + // Bind the active states + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("downloading"), nsIDownloadManager::DOWNLOAD_DOWNLOADING); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("paused"), nsIDownloadManager::DOWNLOAD_PAUSED); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("queued"), nsIDownloadManager::DOWNLOAD_QUEUED); + NS_ENSURE_SUCCESS(rv, rv); + + // Execute + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +nsDownloadManager::RemoveDownloadsByTimeframe(int64_t aStartTime, + int64_t aEndTime) +{ + NS_ENSURE_STATE(!mUseJSTransfer); + + nsresult rv = DoRemoveDownloadsByTimeframe(mDBConn, aStartTime, aEndTime); + nsresult rv2 = DoRemoveDownloadsByTimeframe(mPrivateDBConn, aStartTime, aEndTime); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_SUCCESS(rv2, rv2); + + // Notify the UI with the topic and null subject to indicate "remove multiple" + return NotifyDownloadRemoval(nullptr); +} + +NS_IMETHODIMP +nsDownloadManager::CleanUp() +{ + NS_ENSURE_STATE(!mUseJSTransfer); + + return CleanUp(mDBConn); +} + +NS_IMETHODIMP +nsDownloadManager::CleanUpPrivate() +{ + NS_ENSURE_STATE(!mUseJSTransfer); + + return CleanUp(mPrivateDBConn); +} + +nsresult +nsDownloadManager::CleanUp(mozIStorageConnection* aDBConn) +{ + DownloadState states[] = { nsIDownloadManager::DOWNLOAD_FINISHED, + nsIDownloadManager::DOWNLOAD_FAILED, + nsIDownloadManager::DOWNLOAD_CANCELED, + nsIDownloadManager::DOWNLOAD_BLOCKED_PARENTAL, + nsIDownloadManager::DOWNLOAD_BLOCKED_POLICY, + nsIDownloadManager::DOWNLOAD_DIRTY }; + + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = aDBConn->CreateStatement(NS_LITERAL_CSTRING( + "DELETE FROM moz_downloads " + "WHERE state = ? " + "OR state = ? " + "OR state = ? " + "OR state = ? " + "OR state = ? " + "OR state = ?"), getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + for (uint32_t i = 0; i < ArrayLength(states); ++i) { + rv = stmt->BindInt32ByIndex(i, states[i]); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + // Notify the UI with the topic and null subject to indicate "remove multiple" + return NotifyDownloadRemoval(nullptr); +} + +static nsresult +DoGetCanCleanUp(mozIStorageConnection* aDBConn, bool *aResult) +{ + // This method should never return anything but NS_OK for the benefit of + // unwitting consumers. + + *aResult = false; + + DownloadState states[] = { nsIDownloadManager::DOWNLOAD_FINISHED, + nsIDownloadManager::DOWNLOAD_FAILED, + nsIDownloadManager::DOWNLOAD_CANCELED, + nsIDownloadManager::DOWNLOAD_BLOCKED_PARENTAL, + nsIDownloadManager::DOWNLOAD_BLOCKED_POLICY, + nsIDownloadManager::DOWNLOAD_DIRTY }; + + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = aDBConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT COUNT(*) " + "FROM moz_downloads " + "WHERE state = ? " + "OR state = ? " + "OR state = ? " + "OR state = ? " + "OR state = ? " + "OR state = ?"), getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, NS_OK); + for (uint32_t i = 0; i < ArrayLength(states); ++i) { + rv = stmt->BindInt32ByIndex(i, states[i]); + NS_ENSURE_SUCCESS(rv, NS_OK); + } + + bool moreResults; // We don't really care... + rv = stmt->ExecuteStep(&moreResults); + NS_ENSURE_SUCCESS(rv, NS_OK); + + int32_t count; + rv = stmt->GetInt32(0, &count); + NS_ENSURE_SUCCESS(rv, NS_OK); + + if (count > 0) + *aResult = true; + + return NS_OK; +} + +NS_IMETHODIMP +nsDownloadManager::GetCanCleanUp(bool *aResult) +{ + NS_ENSURE_STATE(!mUseJSTransfer); + + return DoGetCanCleanUp(mDBConn, aResult); +} + +NS_IMETHODIMP +nsDownloadManager::GetCanCleanUpPrivate(bool *aResult) +{ + NS_ENSURE_STATE(!mUseJSTransfer); + + return DoGetCanCleanUp(mPrivateDBConn, aResult); +} + +NS_IMETHODIMP +nsDownloadManager::PauseDownload(uint32_t aID) +{ + NS_ENSURE_STATE(!mUseJSTransfer); + + NS_WARNING("Using integer IDs without compat mode enabled"); + + nsDownload *dl = FindDownload(aID); + if (!dl) + return NS_ERROR_FAILURE; + + return dl->Pause(); +} + +NS_IMETHODIMP +nsDownloadManager::ResumeDownload(uint32_t aID) +{ + NS_ENSURE_STATE(!mUseJSTransfer); + + NS_WARNING("Using integer IDs without compat mode enabled"); + + nsDownload *dl = FindDownload(aID); + if (!dl) + return NS_ERROR_FAILURE; + + return dl->Resume(); +} + +NS_IMETHODIMP +nsDownloadManager::GetDBConnection(mozIStorageConnection **aDBConn) +{ + NS_ENSURE_STATE(!mUseJSTransfer); + + NS_ADDREF(*aDBConn = mDBConn); + + return NS_OK; +} + +NS_IMETHODIMP +nsDownloadManager::GetPrivateDBConnection(mozIStorageConnection **aDBConn) +{ + NS_ENSURE_STATE(!mUseJSTransfer); + + NS_ADDREF(*aDBConn = mPrivateDBConn); + + return NS_OK; + } + +NS_IMETHODIMP +nsDownloadManager::AddListener(nsIDownloadProgressListener *aListener) +{ + NS_ENSURE_STATE(!mUseJSTransfer); + + mListeners.AppendObject(aListener); + return NS_OK; +} + +NS_IMETHODIMP +nsDownloadManager::AddPrivacyAwareListener(nsIDownloadProgressListener *aListener) +{ + NS_ENSURE_STATE(!mUseJSTransfer); + + mPrivacyAwareListeners.AppendObject(aListener); + return NS_OK; +} + +NS_IMETHODIMP +nsDownloadManager::RemoveListener(nsIDownloadProgressListener *aListener) +{ + NS_ENSURE_STATE(!mUseJSTransfer); + + mListeners.RemoveObject(aListener); + mPrivacyAwareListeners.RemoveObject(aListener); + return NS_OK; +} + +void +nsDownloadManager::NotifyListenersOnDownloadStateChange(int16_t aOldState, + nsDownload *aDownload) +{ + for (int32_t i = mPrivacyAwareListeners.Count() - 1; i >= 0; --i) { + mPrivacyAwareListeners[i]->OnDownloadStateChange(aOldState, aDownload); + } + + // Only privacy-aware listeners should receive notifications about private + // downloads, while non-privacy-aware listeners receive no sign they exist. + if (aDownload->mPrivate) { + return; + } + + for (int32_t i = mListeners.Count() - 1; i >= 0; --i) { + mListeners[i]->OnDownloadStateChange(aOldState, aDownload); + } +} + +void +nsDownloadManager::NotifyListenersOnProgressChange(nsIWebProgress *aProgress, + nsIRequest *aRequest, + int64_t aCurSelfProgress, + int64_t aMaxSelfProgress, + int64_t aCurTotalProgress, + int64_t aMaxTotalProgress, + nsDownload *aDownload) +{ + for (int32_t i = mPrivacyAwareListeners.Count() - 1; i >= 0; --i) { + mPrivacyAwareListeners[i]->OnProgressChange(aProgress, aRequest, aCurSelfProgress, + aMaxSelfProgress, aCurTotalProgress, + aMaxTotalProgress, aDownload); + } + + // Only privacy-aware listeners should receive notifications about private + // downloads, while non-privacy-aware listeners receive no sign they exist. + if (aDownload->mPrivate) { + return; + } + + for (int32_t i = mListeners.Count() - 1; i >= 0; --i) { + mListeners[i]->OnProgressChange(aProgress, aRequest, aCurSelfProgress, + aMaxSelfProgress, aCurTotalProgress, + aMaxTotalProgress, aDownload); + } +} + +void +nsDownloadManager::NotifyListenersOnStateChange(nsIWebProgress *aProgress, + nsIRequest *aRequest, + uint32_t aStateFlags, + nsresult aStatus, + nsDownload *aDownload) +{ + for (int32_t i = mPrivacyAwareListeners.Count() - 1; i >= 0; --i) { + mPrivacyAwareListeners[i]->OnStateChange(aProgress, aRequest, aStateFlags, aStatus, + aDownload); + } + + // Only privacy-aware listeners should receive notifications about private + // downloads, while non-privacy-aware listeners receive no sign they exist. + if (aDownload->mPrivate) { + return; + } + + for (int32_t i = mListeners.Count() - 1; i >= 0; --i) { + mListeners[i]->OnStateChange(aProgress, aRequest, aStateFlags, aStatus, + aDownload); + } +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsINavHistoryObserver + +NS_IMETHODIMP +nsDownloadManager::OnBeginUpdateBatch() +{ + // This method in not normally invoked when mUseJSTransfer is enabled, however + // we provide an extra check in case it is called manually by add-ons. + NS_ENSURE_STATE(!mUseJSTransfer); + + // We already have a transaction, so don't make another + if (mHistoryTransaction) + return NS_OK; + + // Start a transaction that commits when deleted + mHistoryTransaction = new mozStorageTransaction(mDBConn, true); + + return NS_OK; +} + +NS_IMETHODIMP +nsDownloadManager::OnEndUpdateBatch() +{ + // Get rid of the transaction and cause it to commit + mHistoryTransaction = nullptr; + + return NS_OK; +} + +NS_IMETHODIMP +nsDownloadManager::OnVisit(nsIURI *aURI, int64_t aVisitID, PRTime aTime, + int64_t aSessionID, int64_t aReferringID, + uint32_t aTransitionType, const nsACString& aGUID, + bool aHidden, uint32_t aVisitCount, uint32_t aTyped) +{ + return NS_OK; +} + +NS_IMETHODIMP +nsDownloadManager::OnTitleChanged(nsIURI *aURI, + const nsAString &aPageTitle, + const nsACString &aGUID) +{ + return NS_OK; +} + +NS_IMETHODIMP +nsDownloadManager::OnFrecencyChanged(nsIURI* aURI, + int32_t aNewFrecency, + const nsACString& aGUID, + bool aHidden, + PRTime aLastVisitDate) +{ + return NS_OK; +} + +NS_IMETHODIMP +nsDownloadManager::OnManyFrecenciesChanged() +{ + return NS_OK; +} + +NS_IMETHODIMP +nsDownloadManager::OnDeleteURI(nsIURI *aURI, + const nsACString& aGUID, + uint16_t aReason) +{ + // This method in not normally invoked when mUseJSTransfer is enabled, however + // we provide an extra check in case it is called manually by add-ons. + NS_ENSURE_STATE(!mUseJSTransfer); + + nsresult rv = RemoveDownloadsForURI(mGetIdsForURIStatement, aURI); + nsresult rv2 = RemoveDownloadsForURI(mGetPrivateIdsForURIStatement, aURI); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_SUCCESS(rv2, rv2); + return NS_OK; +} + +NS_IMETHODIMP +nsDownloadManager::OnClearHistory() +{ + return CleanUp(); +} + +NS_IMETHODIMP +nsDownloadManager::OnPageChanged(nsIURI *aURI, + uint32_t aChangedAttribute, + const nsAString& aNewValue, + const nsACString &aGUID) +{ + return NS_OK; +} + +NS_IMETHODIMP +nsDownloadManager::OnDeleteVisits(nsIURI *aURI, PRTime aVisitTime, + const nsACString& aGUID, + uint16_t aReason, uint32_t aTransitionType) +{ + // Don't bother removing downloads until the page is removed. + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsIObserver + +NS_IMETHODIMP +nsDownloadManager::Observe(nsISupports *aSubject, + const char *aTopic, + const char16_t *aData) +{ + // This method in not normally invoked when mUseJSTransfer is enabled, however + // we provide an extra check in case it is called manually by add-ons. + NS_ENSURE_STATE(!mUseJSTransfer); + + // We need to count the active public downloads that could be lost + // by quitting, and add any active private ones as well, since per-window + // private browsing may be active. + int32_t currDownloadCount = mCurrentDownloads.Count(); + + // If we don't need to cancel all the downloads on quit, only count the ones + // that aren't resumable. + if (GetQuitBehavior() != QUIT_AND_CANCEL) { + for (int32_t i = currDownloadCount - 1; i >= 0; --i) { + if (mCurrentDownloads[i]->IsResumable()) { + currDownloadCount--; + } + } + + // We have a count of the public, non-resumable downloads. Now we need + // to add the total number of private downloads, since they are in danger + // of being lost. + currDownloadCount += mCurrentPrivateDownloads.Count(); + } + + nsresult rv; + if (strcmp(aTopic, "oncancel") == 0) { + nsCOMPtr<nsIDownload> dl = do_QueryInterface(aSubject, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + dl->Cancel(); + } else if (strcmp(aTopic, "profile-before-change") == 0) { + CloseAllDBs(); + } else if (strcmp(aTopic, "quit-application") == 0) { + // Try to pause all downloads and, if appropriate, mark them as auto-resume + // unless user has specified that downloads should be canceled + enum QuitBehavior behavior = GetQuitBehavior(); + if (behavior != QUIT_AND_CANCEL) + (void)PauseAllDownloads(bool(behavior != QUIT_AND_PAUSE)); + + // Remove downloads to break cycles and cancel downloads + (void)RemoveAllDownloads(); + + // Now that active downloads have been canceled, remove all completed or + // aborted downloads if the user's retention policy specifies it. + if (GetRetentionBehavior() == 1) + CleanUp(); + } else if (strcmp(aTopic, "quit-application-requested") == 0 && + currDownloadCount) { + nsCOMPtr<nsISupportsPRBool> cancelDownloads = + do_QueryInterface(aSubject, &rv); + NS_ENSURE_SUCCESS(rv, rv); + ConfirmCancelDownloads(currDownloadCount, cancelDownloads, + u"quitCancelDownloadsAlertTitle", + u"quitCancelDownloadsAlertMsgMultiple", + u"quitCancelDownloadsAlertMsg", + u"dontQuitButtonWin"); + } else if (strcmp(aTopic, "offline-requested") == 0 && currDownloadCount) { + nsCOMPtr<nsISupportsPRBool> cancelDownloads = + do_QueryInterface(aSubject, &rv); + NS_ENSURE_SUCCESS(rv, rv); + ConfirmCancelDownloads(currDownloadCount, cancelDownloads, + u"offlineCancelDownloadsAlertTitle", + u"offlineCancelDownloadsAlertMsgMultiple", + u"offlineCancelDownloadsAlertMsg", + u"dontGoOfflineButton"); + } + else if (strcmp(aTopic, NS_IOSERVICE_GOING_OFFLINE_TOPIC) == 0) { + // Pause all downloads, and mark them to auto-resume. + (void)PauseAllDownloads(true); + } + else if (strcmp(aTopic, NS_IOSERVICE_OFFLINE_STATUS_TOPIC) == 0 && + nsDependentString(aData).EqualsLiteral(NS_IOSERVICE_ONLINE)) { + // We can now resume all downloads that are supposed to auto-resume. + (void)ResumeAllDownloads(false); + } + else if (strcmp(aTopic, "alertclickcallback") == 0) { + nsCOMPtr<nsIDownloadManagerUI> dmui = + do_GetService("@mozilla.org/download-manager-ui;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + return dmui->Show(nullptr, nullptr, nsIDownloadManagerUI::REASON_USER_INTERACTED, + aData && NS_strcmp(aData, u"private") == 0); + } else if (strcmp(aTopic, "sleep_notification") == 0 || + strcmp(aTopic, "suspend_process_notification") == 0) { + // Pause downloads if we're sleeping, and mark the downloads as auto-resume + (void)PauseAllDownloads(true); + } else if (strcmp(aTopic, "wake_notification") == 0 || + strcmp(aTopic, "resume_process_notification") == 0) { + int32_t resumeOnWakeDelay = 10000; + nsCOMPtr<nsIPrefBranch> pref = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (pref) + (void)pref->GetIntPref(PREF_BDM_RESUMEONWAKEDELAY, &resumeOnWakeDelay); + + // Wait a little bit before trying to resume to avoid resuming when network + // connections haven't restarted yet + mResumeOnWakeTimer = do_CreateInstance("@mozilla.org/timer;1"); + if (resumeOnWakeDelay >= 0 && mResumeOnWakeTimer) { + (void)mResumeOnWakeTimer->InitWithFuncCallback(ResumeOnWakeCallback, + this, resumeOnWakeDelay, nsITimer::TYPE_ONE_SHOT); + } + } else if (strcmp(aTopic, "last-pb-context-exited") == 0) { + // Upon leaving private browsing mode, cancel all private downloads, + // remove all trace of them, and then blow away the private database + // and recreate a blank one. + RemoveAllDownloads(mCurrentPrivateDownloads); + InitPrivateDB(); + } else if (strcmp(aTopic, "last-pb-context-exiting") == 0) { + // If there are active private downloads, prompt the user to confirm leaving + // private browsing mode (thereby cancelling them). Otherwise, silently proceed. + if (!mCurrentPrivateDownloads.Count()) + return NS_OK; + + nsCOMPtr<nsISupportsPRBool> cancelDownloads = do_QueryInterface(aSubject, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + ConfirmCancelDownloads(mCurrentPrivateDownloads.Count(), cancelDownloads, + u"leavePrivateBrowsingCancelDownloadsAlertTitle", + u"leavePrivateBrowsingWindowsCancelDownloadsAlertMsgMultiple2", + u"leavePrivateBrowsingWindowsCancelDownloadsAlertMsg2", + u"dontLeavePrivateBrowsingButton2"); + } + + return NS_OK; +} + +void +nsDownloadManager::ConfirmCancelDownloads(int32_t aCount, + nsISupportsPRBool *aCancelDownloads, + const char16_t *aTitle, + const char16_t *aCancelMessageMultiple, + const char16_t *aCancelMessageSingle, + const char16_t *aDontCancelButton) +{ + // If user has already dismissed quit request, then do nothing + bool quitRequestCancelled = false; + aCancelDownloads->GetData(&quitRequestCancelled); + if (quitRequestCancelled) + return; + + nsXPIDLString title, message, quitButton, dontQuitButton; + + mBundle->GetStringFromName(aTitle, getter_Copies(title)); + + nsAutoString countString; + countString.AppendInt(aCount); + const char16_t *strings[1] = { countString.get() }; + if (aCount > 1) { + mBundle->FormatStringFromName(aCancelMessageMultiple, strings, 1, + getter_Copies(message)); + mBundle->FormatStringFromName(u"cancelDownloadsOKTextMultiple", + strings, 1, getter_Copies(quitButton)); + } else { + mBundle->GetStringFromName(aCancelMessageSingle, getter_Copies(message)); + mBundle->GetStringFromName(u"cancelDownloadsOKText", + getter_Copies(quitButton)); + } + + mBundle->GetStringFromName(aDontCancelButton, getter_Copies(dontQuitButton)); + + // Get Download Manager window, to be parent of alert. + nsCOMPtr<nsIWindowMediator> wm = do_GetService(NS_WINDOWMEDIATOR_CONTRACTID); + nsCOMPtr<mozIDOMWindowProxy> dmWindow; + if (wm) { + wm->GetMostRecentWindow(u"Download:Manager", + getter_AddRefs(dmWindow)); + } + + // Show alert. + nsCOMPtr<nsIPromptService> prompter(do_GetService(NS_PROMPTSERVICE_CONTRACTID)); + if (prompter) { + int32_t flags = (nsIPromptService::BUTTON_TITLE_IS_STRING * nsIPromptService::BUTTON_POS_0) + (nsIPromptService::BUTTON_TITLE_IS_STRING * nsIPromptService::BUTTON_POS_1); + bool nothing = false; + int32_t button; + prompter->ConfirmEx(dmWindow, title, message, flags, quitButton.get(), dontQuitButton.get(), nullptr, nullptr, ¬hing, &button); + + aCancelDownloads->SetData(button == 1); + } +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsDownload + +NS_IMPL_CLASSINFO(nsDownload, nullptr, 0, NS_DOWNLOAD_CID) +NS_IMPL_ISUPPORTS_CI( + nsDownload + , nsIDownload + , nsITransfer + , nsIWebProgressListener + , nsIWebProgressListener2 +) + +nsDownload::nsDownload() : mDownloadState(nsIDownloadManager::DOWNLOAD_NOTSTARTED), + mID(0), + mPercentComplete(0), + mCurrBytes(0), + mMaxBytes(-1), + mStartTime(0), + mLastUpdate(PR_Now() - (uint32_t)gUpdateInterval), + mResumedAt(-1), + mSpeed(0), + mHasMultipleFiles(false), + mPrivate(false), + mAutoResume(DONT_RESUME) +{ +} + +nsDownload::~nsDownload() +{ +} + +NS_IMETHODIMP nsDownload::SetSha256Hash(const nsACString& aHash) { + MOZ_ASSERT(NS_IsMainThread(), "Must call SetSha256Hash on main thread"); + // This will be used later to query the application reputation service. + mHash = aHash; + return NS_OK; +} + +NS_IMETHODIMP nsDownload::SetSignatureInfo(nsIArray* aSignatureInfo) { + MOZ_ASSERT(NS_IsMainThread(), "Must call SetSignatureInfo on main thread"); + // This will be used later to query the application reputation service. + mSignatureInfo = aSignatureInfo; + return NS_OK; +} + +NS_IMETHODIMP nsDownload::SetRedirects(nsIArray* aRedirects) { + MOZ_ASSERT(NS_IsMainThread(), "Must call SetRedirects on main thread"); + // This will be used later to query the application reputation service. + mRedirects = aRedirects; + return NS_OK; +} + +#ifdef MOZ_ENABLE_GIO +static void gio_set_metadata_done(GObject *source_obj, GAsyncResult *res, gpointer user_data) +{ + GError *err = nullptr; + g_file_set_attributes_finish(G_FILE(source_obj), res, nullptr, &err); + if (err) { +#ifdef DEBUG + NS_DebugBreak(NS_DEBUG_WARNING, "Set file metadata failed: ", err->message, __FILE__, __LINE__); +#endif + g_error_free(err); + } +} +#endif + +nsresult +nsDownload::SetState(DownloadState aState) +{ + NS_ASSERTION(mDownloadState != aState, + "Trying to set the download state to what it already is set to!"); + + int16_t oldState = mDownloadState; + mDownloadState = aState; + + // We don't want to lose access to our member variables + RefPtr<nsDownload> kungFuDeathGrip = this; + + // When the state changed listener is dispatched, queries to the database and + // the download manager api should reflect what the nsIDownload object would + // return. So, if a download is done (finished, canceled, etc.), it should + // first be removed from the current downloads. We will also have to update + // the database *before* notifying listeners. At this point, you can safely + // dispatch to the observers as well. + switch (aState) { + case nsIDownloadManager::DOWNLOAD_BLOCKED_PARENTAL: + case nsIDownloadManager::DOWNLOAD_BLOCKED_POLICY: + case nsIDownloadManager::DOWNLOAD_DIRTY: + case nsIDownloadManager::DOWNLOAD_CANCELED: + case nsIDownloadManager::DOWNLOAD_FAILED: + + // Transfers are finished, so break the reference cycle + Finalize(); + break; +#ifdef DOWNLOAD_SCANNER + case nsIDownloadManager::DOWNLOAD_SCANNING: + { + nsresult rv = mDownloadManager->mScanner ? mDownloadManager->mScanner->ScanDownload(this) : NS_ERROR_NOT_INITIALIZED; + // If we failed, then fall through to 'download finished' + if (NS_SUCCEEDED(rv)) + break; + mDownloadState = aState = nsIDownloadManager::DOWNLOAD_FINISHED; + } +#endif + case nsIDownloadManager::DOWNLOAD_FINISHED: + { + nsresult rv = ExecuteDesiredAction(); + if (NS_FAILED(rv)) { + // We've failed to execute the desired action. As a result, we should + // fail the download so the user can try again. + (void)FailDownload(rv, nullptr); + return rv; + } + + // Now that we're done with handling the download, clean it up + Finalize(); + + nsCOMPtr<nsIPrefBranch> pref(do_GetService(NS_PREFSERVICE_CONTRACTID)); + + // Master pref to control this function. + bool showTaskbarAlert = true; + if (pref) + pref->GetBoolPref(PREF_BDM_SHOWALERTONCOMPLETE, &showTaskbarAlert); + + if (showTaskbarAlert) { + int32_t alertInterval = 2000; + if (pref) + pref->GetIntPref(PREF_BDM_SHOWALERTINTERVAL, &alertInterval); + + int64_t alertIntervalUSec = alertInterval * PR_USEC_PER_MSEC; + int64_t goat = PR_Now() - mStartTime; + showTaskbarAlert = goat > alertIntervalUSec; + + int32_t size = mPrivate ? + mDownloadManager->mCurrentPrivateDownloads.Count() : + mDownloadManager->mCurrentDownloads.Count(); + if (showTaskbarAlert && size == 0) { + nsCOMPtr<nsIAlertsService> alerts = + do_GetService("@mozilla.org/alerts-service;1"); + if (alerts) { + nsXPIDLString title, message; + + mDownloadManager->mBundle->GetStringFromName( + u"downloadsCompleteTitle", + getter_Copies(title)); + mDownloadManager->mBundle->GetStringFromName( + u"downloadsCompleteMsg", + getter_Copies(message)); + + bool removeWhenDone = + mDownloadManager->GetRetentionBehavior() == 0; + + // If downloads are automatically removed per the user's + // retention policy, there's no reason to make the text clickable + // because if it is, they'll click open the download manager and + // the items they downloaded will have been removed. + alerts->ShowAlertNotification( + NS_LITERAL_STRING(DOWNLOAD_MANAGER_ALERT_ICON), title, + message, !removeWhenDone, + mPrivate ? NS_LITERAL_STRING("private") : NS_LITERAL_STRING("non-private"), + mDownloadManager, EmptyString(), NS_LITERAL_STRING("auto"), + EmptyString(), EmptyString(), nullptr, mPrivate, + false /* requireInteraction */); + } + } + } + +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) + nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(mTarget); + nsCOMPtr<nsIFile> file; + nsAutoString path; + + if (fileURL && + NS_SUCCEEDED(fileURL->GetFile(getter_AddRefs(file))) && + file && + NS_SUCCEEDED(file->GetPath(path))) { + + // On Windows and Gtk, add the download to the system's "recent documents" + // list, with a pref to disable. + { + bool addToRecentDocs = true; + if (pref) + pref->GetBoolPref(PREF_BDM_ADDTORECENTDOCS, &addToRecentDocs); + if (addToRecentDocs && !mPrivate) { +#ifdef XP_WIN + ::SHAddToRecentDocs(SHARD_PATHW, path.get()); +#elif defined(MOZ_WIDGET_GTK) + GtkRecentManager* manager = gtk_recent_manager_get_default(); + + gchar* uri = g_filename_to_uri(NS_ConvertUTF16toUTF8(path).get(), + nullptr, nullptr); + if (uri) { + gtk_recent_manager_add_item(manager, uri); + g_free(uri); + } +#endif + } +#ifdef MOZ_ENABLE_GIO + // Use GIO to store the source URI for later display in the file manager. + GFile* gio_file = g_file_new_for_path(NS_ConvertUTF16toUTF8(path).get()); + nsCString source_uri; + rv = mSource->GetSpec(source_uri); + NS_ENSURE_SUCCESS(rv, rv); + GFileInfo *file_info = g_file_info_new(); + g_file_info_set_attribute_string(file_info, "metadata::download-uri", source_uri.get()); + g_file_set_attributes_async(gio_file, + file_info, + G_FILE_QUERY_INFO_NONE, + G_PRIORITY_DEFAULT, + nullptr, gio_set_metadata_done, nullptr); + g_object_unref(file_info); + g_object_unref(gio_file); +#endif + } + } + +#ifdef XP_WIN + // Adjust file attributes so that by default, new files are indexed + // by desktop search services. Skip off those that land in the temp + // folder. + nsCOMPtr<nsIFile> tempDir, fileDir; + rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(tempDir)); + NS_ENSURE_SUCCESS(rv, rv); + (void)file->GetParent(getter_AddRefs(fileDir)); + + bool isTemp = false; + if (fileDir) + (void)fileDir->Equals(tempDir, &isTemp); + + nsCOMPtr<nsILocalFileWin> localFileWin(do_QueryInterface(file)); + if (!isTemp && localFileWin) + (void)localFileWin->SetFileAttributesWin(nsILocalFileWin::WFA_SEARCH_INDEXED); +#endif + +#endif + // Now remove the download if the user's retention policy is "Remove when Done" + if (mDownloadManager->GetRetentionBehavior() == 0) + mDownloadManager->RemoveDownload(mGUID); + } + break; + default: + break; + } + + // Before notifying the listener, we must update the database so that calls + // to it work out properly. + nsresult rv = UpdateDB(); + NS_ENSURE_SUCCESS(rv, rv); + + mDownloadManager->NotifyListenersOnDownloadStateChange(oldState, this); + + switch (mDownloadState) { + case nsIDownloadManager::DOWNLOAD_DOWNLOADING: + // Only send the dl-start event to downloads that are actually starting. + if (oldState == nsIDownloadManager::DOWNLOAD_QUEUED) { + if (!mPrivate) + mDownloadManager->SendEvent(this, "dl-start"); + } + break; + case nsIDownloadManager::DOWNLOAD_FAILED: + if (!mPrivate) + mDownloadManager->SendEvent(this, "dl-failed"); + break; + case nsIDownloadManager::DOWNLOAD_SCANNING: + if (!mPrivate) + mDownloadManager->SendEvent(this, "dl-scanning"); + break; + case nsIDownloadManager::DOWNLOAD_FINISHED: + if (!mPrivate) + mDownloadManager->SendEvent(this, "dl-done"); + break; + case nsIDownloadManager::DOWNLOAD_BLOCKED_PARENTAL: + case nsIDownloadManager::DOWNLOAD_BLOCKED_POLICY: + if (!mPrivate) + mDownloadManager->SendEvent(this, "dl-blocked"); + break; + case nsIDownloadManager::DOWNLOAD_DIRTY: + if (!mPrivate) + mDownloadManager->SendEvent(this, "dl-dirty"); + break; + case nsIDownloadManager::DOWNLOAD_CANCELED: + if (!mPrivate) + mDownloadManager->SendEvent(this, "dl-cancel"); + break; + default: + break; + } + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsIWebProgressListener2 + +NS_IMETHODIMP +nsDownload::OnProgressChange64(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, + int64_t aCurSelfProgress, + int64_t aMaxSelfProgress, + int64_t aCurTotalProgress, + int64_t aMaxTotalProgress) +{ + if (!mRequest) + mRequest = aRequest; // used for pause/resume + + if (mDownloadState == nsIDownloadManager::DOWNLOAD_QUEUED) { + // Obtain the referrer + nsresult rv; + nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest)); + nsCOMPtr<nsIURI> referrer = mReferrer; + if (channel) + (void)NS_GetReferrerFromChannel(channel, getter_AddRefs(mReferrer)); + + // Restore the original referrer if the new one isn't useful + if (!mReferrer) + mReferrer = referrer; + + // If we have a MIME info, we know that exthandler has already added this to + // the history, but if we do not, we'll have to add it ourselves. + if (!mMIMEInfo && !mPrivate) { + nsCOMPtr<nsIDownloadHistory> dh = + do_GetService(NS_DOWNLOADHISTORY_CONTRACTID); + if (dh) + (void)dh->AddDownload(mSource, mReferrer, mStartTime, mTarget); + } + + // Fetch the entityID, but if we can't get it, don't panic (non-resumable) + nsCOMPtr<nsIResumableChannel> resumableChannel(do_QueryInterface(aRequest)); + if (resumableChannel) + (void)resumableChannel->GetEntityID(mEntityID); + + // Before we update the state and dispatch state notifications, we want to + // ensure that we have the correct state for this download with regards to + // its percent completion and size. + SetProgressBytes(0, aMaxTotalProgress); + + // Update the state and the database + rv = SetState(nsIDownloadManager::DOWNLOAD_DOWNLOADING); + NS_ENSURE_SUCCESS(rv, rv); + } + + // filter notifications since they come in so frequently + PRTime now = PR_Now(); + PRIntervalTime delta = now - mLastUpdate; + if (delta < gUpdateInterval) + return NS_OK; + + mLastUpdate = now; + + // Calculate the speed using the elapsed delta time and bytes downloaded + // during that time for more accuracy. + double elapsedSecs = double(delta) / PR_USEC_PER_SEC; + if (elapsedSecs > 0) { + double speed = double(aCurTotalProgress - mCurrBytes) / elapsedSecs; + if (mCurrBytes == 0) { + mSpeed = speed; + } else { + // Calculate 'smoothed average' of 10 readings. + mSpeed = mSpeed * 0.9 + speed * 0.1; + } + } + + SetProgressBytes(aCurTotalProgress, aMaxTotalProgress); + + // Report to the listener our real sizes + int64_t currBytes, maxBytes; + (void)GetAmountTransferred(&currBytes); + (void)GetSize(&maxBytes); + mDownloadManager->NotifyListenersOnProgressChange( + aWebProgress, aRequest, currBytes, maxBytes, currBytes, maxBytes, this); + + // If the maximums are different, then there must be more than one file + if (aMaxSelfProgress != aMaxTotalProgress) + mHasMultipleFiles = true; + + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::OnRefreshAttempted(nsIWebProgress *aWebProgress, + nsIURI *aUri, + int32_t aDelay, + bool aSameUri, + bool *allowRefresh) +{ + *allowRefresh = true; + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsIWebProgressListener + +NS_IMETHODIMP +nsDownload::OnProgressChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, + int32_t aCurSelfProgress, + int32_t aMaxSelfProgress, + int32_t aCurTotalProgress, + int32_t aMaxTotalProgress) +{ + return OnProgressChange64(aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress); +} + +NS_IMETHODIMP +nsDownload::OnLocationChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, nsIURI *aLocation, + uint32_t aFlags) +{ + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::OnStatusChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, nsresult aStatus, + const char16_t *aMessage) +{ + if (NS_FAILED(aStatus)) + return FailDownload(aStatus, aMessage); + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::OnStateChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, uint32_t aStateFlags, + nsresult aStatus) +{ + MOZ_ASSERT(NS_IsMainThread(), "Must call OnStateChange in main thread"); + + // We don't want to lose access to our member variables + RefPtr<nsDownload> kungFuDeathGrip = this; + + // Check if we're starting a request; the NETWORK flag is necessary to not + // pick up the START of *each* file but only for the whole request + if ((aStateFlags & STATE_START) && (aStateFlags & STATE_IS_NETWORK)) { + nsresult rv; + nsCOMPtr<nsIHttpChannel> channel = do_QueryInterface(aRequest, &rv); + if (NS_SUCCEEDED(rv)) { + uint32_t status; + rv = channel->GetResponseStatus(&status); + // HTTP 450 - Blocked by parental control proxies + if (NS_SUCCEEDED(rv) && status == 450) { + // Cancel using the provided object + (void)Cancel(); + + // Fail the download + (void)SetState(nsIDownloadManager::DOWNLOAD_BLOCKED_PARENTAL); + } + } + } else if ((aStateFlags & STATE_STOP) && (aStateFlags & STATE_IS_NETWORK) && + IsFinishable()) { + // We got both STOP and NETWORK so that means the whole request is done + // (and not just a single file if there are multiple files) + if (NS_SUCCEEDED(aStatus)) { + // We can't completely trust the bytes we've added up because we might be + // missing on some/all of the progress updates (especially from cache). + // Our best bet is the file itself, but if for some reason it's gone or + // if we have multiple files, the next best is what we've calculated. + int64_t fileSize; + nsCOMPtr<nsIFile> file; + // We need a nsIFile clone to deal with file size caching issues. :( + nsCOMPtr<nsIFile> clone; + if (!mHasMultipleFiles && + NS_SUCCEEDED(GetTargetFile(getter_AddRefs(file))) && + NS_SUCCEEDED(file->Clone(getter_AddRefs(clone))) && + NS_SUCCEEDED(clone->GetFileSize(&fileSize)) && fileSize > 0) { + mCurrBytes = mMaxBytes = fileSize; + + // If we resumed, keep the fact that we did and fix size calculations + if (WasResumed()) + mResumedAt = 0; + } else if (mMaxBytes == -1) { + mMaxBytes = mCurrBytes; + } else { + mCurrBytes = mMaxBytes; + } + + mPercentComplete = 100; + mLastUpdate = PR_Now(); + +#ifdef DOWNLOAD_SCANNER + bool scan = true; + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (prefs) + (void)prefs->GetBoolPref(PREF_BDM_SCANWHENDONE, &scan); + + if (scan) + (void)SetState(nsIDownloadManager::DOWNLOAD_SCANNING); + else + (void)SetState(nsIDownloadManager::DOWNLOAD_FINISHED); +#else + (void)SetState(nsIDownloadManager::DOWNLOAD_FINISHED); +#endif + } else if (aStatus != NS_BINDING_ABORTED) { + // We failed for some unknown reason -- fail with a generic message + (void)FailDownload(aStatus, nullptr); + } + } + + mDownloadManager->NotifyListenersOnStateChange(aWebProgress, aRequest, + aStateFlags, aStatus, this); + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::OnSecurityChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, uint32_t aState) +{ + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsIDownload + +NS_IMETHODIMP +nsDownload::Init(nsIURI *aSource, + nsIURI *aTarget, + const nsAString& aDisplayName, + nsIMIMEInfo *aMIMEInfo, + PRTime aStartTime, + nsIFile *aTempFile, + nsICancelable *aCancelable, + bool aIsPrivate) +{ + NS_WARNING("Huh...how did we get here?!"); + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetState(int16_t *aState) +{ + *aState = mDownloadState; + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetDisplayName(nsAString &aDisplayName) +{ + aDisplayName = mDisplayName; + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetCancelable(nsICancelable **aCancelable) +{ + *aCancelable = mCancelable; + NS_IF_ADDREF(*aCancelable); + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetTarget(nsIURI **aTarget) +{ + *aTarget = mTarget; + NS_IF_ADDREF(*aTarget); + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetSource(nsIURI **aSource) +{ + *aSource = mSource; + NS_IF_ADDREF(*aSource); + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetStartTime(int64_t *aStartTime) +{ + *aStartTime = mStartTime; + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetPercentComplete(int32_t *aPercentComplete) +{ + *aPercentComplete = mPercentComplete; + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetAmountTransferred(int64_t *aAmountTransferred) +{ + *aAmountTransferred = mCurrBytes + (WasResumed() ? mResumedAt : 0); + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetSize(int64_t *aSize) +{ + *aSize = mMaxBytes + (WasResumed() && mMaxBytes != -1 ? mResumedAt : 0); + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetMIMEInfo(nsIMIMEInfo **aMIMEInfo) +{ + *aMIMEInfo = mMIMEInfo; + NS_IF_ADDREF(*aMIMEInfo); + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetTargetFile(nsIFile **aTargetFile) +{ + nsresult rv; + + nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(mTarget, &rv); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIFile> file; + rv = fileURL->GetFile(getter_AddRefs(file)); + if (NS_FAILED(rv)) { + return rv; + } + + file.forget(aTargetFile); + return rv; +} + +NS_IMETHODIMP +nsDownload::GetSpeed(double *aSpeed) +{ + *aSpeed = mSpeed; + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetId(uint32_t *aId) +{ + if (mPrivate) { + return NS_ERROR_NOT_AVAILABLE; + } + *aId = mID; + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetGuid(nsACString &aGUID) +{ + aGUID = mGUID; + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetReferrer(nsIURI **referrer) +{ + NS_IF_ADDREF(*referrer = mReferrer); + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetResumable(bool *resumable) +{ + *resumable = IsResumable(); + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetIsPrivate(bool *isPrivate) +{ + *isPrivate = mPrivate; + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsDownload Helper Functions + +void +nsDownload::Finalize() +{ + // We're stopping, so break the cycle we created at download start + mCancelable = nullptr; + + // Reset values that aren't needed anymore, so the DB can be updated as well + mEntityID.Truncate(); + mTempFile = nullptr; + + // Remove ourself from the active downloads + nsCOMArray<nsDownload>& currentDownloads = mPrivate ? + mDownloadManager->mCurrentPrivateDownloads : + mDownloadManager->mCurrentDownloads; + (void)currentDownloads.RemoveObject(this); + + // Make sure we do not automatically resume + mAutoResume = DONT_RESUME; +} + +nsresult +nsDownload::ExecuteDesiredAction() +{ + // nsExternalHelperAppHandler is the only caller of AddDownload that sets a + // tempfile parameter. In this case, execute the desired action according to + // the saved mime info. + if (!mTempFile) { + return NS_OK; + } + + // We need to bail if for some reason the temp file got removed + bool fileExists; + if (NS_FAILED(mTempFile->Exists(&fileExists)) || !fileExists) + return NS_ERROR_FILE_NOT_FOUND; + + // Assume an unknown action is save to disk + nsHandlerInfoAction action = nsIMIMEInfo::saveToDisk; + if (mMIMEInfo) { + nsresult rv = mMIMEInfo->GetPreferredAction(&action); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsresult rv = NS_OK; + switch (action) { + case nsIMIMEInfo::saveToDisk: + // Move the file to the proper location + rv = MoveTempToTarget(); + if (NS_SUCCEEDED(rv)) { + rv = FixTargetPermissions(); + } + break; + case nsIMIMEInfo::useHelperApp: + case nsIMIMEInfo::useSystemDefault: + // For these cases we have to move the file to the target location and + // open with the appropriate application + rv = OpenWithApplication(); + break; + default: + break; + } + + return rv; +} + +nsresult +nsDownload::FixTargetPermissions() +{ + nsCOMPtr<nsIFile> target; + nsresult rv = GetTargetFile(getter_AddRefs(target)); + NS_ENSURE_SUCCESS(rv, rv); + + // Set perms according to umask. + nsCOMPtr<nsIPropertyBag2> infoService = + do_GetService("@mozilla.org/system-info;1"); + uint32_t gUserUmask = 0; + rv = infoService->GetPropertyAsUint32(NS_LITERAL_STRING("umask"), + &gUserUmask); + if (NS_SUCCEEDED(rv)) { + (void)target->SetPermissions(0666 & ~gUserUmask); + } + return NS_OK; +} + +nsresult +nsDownload::MoveTempToTarget() +{ + nsCOMPtr<nsIFile> target; + nsresult rv = GetTargetFile(getter_AddRefs(target)); + NS_ENSURE_SUCCESS(rv, rv); + + // MoveTo will fail if the file already exists, but we've already obtained + // confirmation from the user that this is OK, so remove it if it exists. + bool fileExists; + if (NS_SUCCEEDED(target->Exists(&fileExists)) && fileExists) { + rv = target->Remove(false); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Extract the new leaf name from the file location + nsAutoString fileName; + rv = target->GetLeafName(fileName); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIFile> dir; + rv = target->GetParent(getter_AddRefs(dir)); + NS_ENSURE_SUCCESS(rv, rv); + rv = mTempFile->MoveTo(dir, fileName); + return rv; +} + +nsresult +nsDownload::OpenWithApplication() +{ + // First move the temporary file to the target location + nsCOMPtr<nsIFile> target; + nsresult rv = GetTargetFile(getter_AddRefs(target)); + NS_ENSURE_SUCCESS(rv, rv); + + // Move the temporary file to the target location + rv = MoveTempToTarget(); + NS_ENSURE_SUCCESS(rv, rv); + + bool deleteTempFileOnExit; + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (!prefs || NS_FAILED(prefs->GetBoolPref(PREF_BH_DELETETEMPFILEONEXIT, + &deleteTempFileOnExit))) { + // No prefservice or no pref set; use default value + // Some users have been very verbal about temp files being deleted on + // app exit - they don't like it - but we'll continue to do this on + // all platforms for now. + deleteTempFileOnExit = true; + } + + // Always schedule files to be deleted at the end of the private browsing + // mode, regardless of the value of the pref. + if (deleteTempFileOnExit || mPrivate) { + + // Make the tmp file readonly so users won't lose changes. + target->SetPermissions(0400); + + // Use the ExternalHelperAppService to push the temporary file to the list + // of files to be deleted on exit. + nsCOMPtr<nsPIExternalAppLauncher> appLauncher(do_GetService + (NS_EXTERNALHELPERAPPSERVICE_CONTRACTID)); + + // Even if we are unable to get this service we return the result + // of LaunchWithFile() which makes more sense. + if (appLauncher) { + if (mPrivate) { + (void)appLauncher->DeleteTemporaryPrivateFileWhenPossible(target); + } else { + (void)appLauncher->DeleteTemporaryFileOnExit(target); + } + } + } + + return mMIMEInfo->LaunchWithFile(target); +} + +void +nsDownload::SetStartTime(int64_t aStartTime) +{ + mStartTime = aStartTime; + mLastUpdate = aStartTime; +} + +void +nsDownload::SetProgressBytes(int64_t aCurrBytes, int64_t aMaxBytes) +{ + mCurrBytes = aCurrBytes; + mMaxBytes = aMaxBytes; + + // Get the real bytes that include resume position + int64_t currBytes, maxBytes; + (void)GetAmountTransferred(&currBytes); + (void)GetSize(&maxBytes); + + if (currBytes == maxBytes) + mPercentComplete = 100; + else if (maxBytes <= 0) + mPercentComplete = -1; + else + mPercentComplete = (int32_t)((double)currBytes / maxBytes * 100 + .5); +} + +NS_IMETHODIMP +nsDownload::Pause() +{ + if (!IsResumable()) + return NS_ERROR_UNEXPECTED; + + nsresult rv = CancelTransfer(); + NS_ENSURE_SUCCESS(rv, rv); + + return SetState(nsIDownloadManager::DOWNLOAD_PAUSED); +} + +nsresult +nsDownload::CancelTransfer() +{ + nsresult rv = NS_OK; + if (mCancelable) { + rv = mCancelable->Cancel(NS_BINDING_ABORTED); + // we're done with this, so break the cycle + mCancelable = nullptr; + } + + return rv; +} + +NS_IMETHODIMP +nsDownload::Cancel() +{ + // Don't cancel if download is already finished + if (IsFinished()) + return NS_OK; + + // Have the download cancel its connection + (void)CancelTransfer(); + + // Dump the temp file because we know we don't need the file anymore. The + // underlying transfer creating the file doesn't delete the file because it + // can't distinguish between a pause that cancels the transfer or a real + // cancel. + if (mTempFile) { + bool exists; + mTempFile->Exists(&exists); + if (exists) + mTempFile->Remove(false); + } + + nsCOMPtr<nsIFile> file; + if (NS_SUCCEEDED(GetTargetFile(getter_AddRefs(file)))) + { + bool exists; + file->Exists(&exists); + if (exists) + file->Remove(false); + } + + nsresult rv = SetState(nsIDownloadManager::DOWNLOAD_CANCELED); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::Resume() +{ + if (!IsPaused() || !IsResumable()) + return NS_ERROR_UNEXPECTED; + + nsresult rv; + nsCOMPtr<nsIWebBrowserPersist> wbp = + do_CreateInstance("@mozilla.org/embedding/browser/nsWebBrowserPersist;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = wbp->SetPersistFlags(nsIWebBrowserPersist::PERSIST_FLAGS_APPEND_TO_FILE | + nsIWebBrowserPersist::PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION); + NS_ENSURE_SUCCESS(rv, rv); + + // Create a new channel for the source URI + nsCOMPtr<nsIChannel> channel; + nsCOMPtr<nsIInterfaceRequestor> ir(do_QueryInterface(wbp)); + rv = NS_NewChannel(getter_AddRefs(channel), + mSource, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + nsIContentPolicy::TYPE_OTHER, + nullptr, // aLoadGroup + ir); + + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrivateBrowsingChannel> pbChannel = do_QueryInterface(channel); + if (pbChannel) { + pbChannel->SetPrivate(mPrivate); + } + + // Make sure we can get a file, either the temporary or the real target, for + // both purposes of file size and a target to write to + nsCOMPtr<nsIFile> targetLocalFile(mTempFile); + if (!targetLocalFile) { + rv = GetTargetFile(getter_AddRefs(targetLocalFile)); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Get the file size to be used as an offset, but if anything goes wrong + // along the way, we'll silently restart at 0. + int64_t fileSize; + // We need a nsIFile clone to deal with file size caching issues. :( + nsCOMPtr<nsIFile> clone; + if (NS_FAILED(targetLocalFile->Clone(getter_AddRefs(clone))) || + NS_FAILED(clone->GetFileSize(&fileSize))) + fileSize = 0; + + // Set the channel to resume at the right position along with the entityID + nsCOMPtr<nsIResumableChannel> resumableChannel(do_QueryInterface(channel)); + if (!resumableChannel) + return NS_ERROR_UNEXPECTED; + rv = resumableChannel->ResumeAt(fileSize, mEntityID); + NS_ENSURE_SUCCESS(rv, rv); + + // If we know the max size, we know what it should be when resuming + int64_t maxBytes; + GetSize(&maxBytes); + SetProgressBytes(0, maxBytes != -1 ? maxBytes - fileSize : -1); + // Track where we resumed because progress notifications restart at 0 + mResumedAt = fileSize; + + // Set the referrer + if (mReferrer) { + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(channel)); + if (httpChannel) { + rv = httpChannel->SetReferrer(mReferrer); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // Creates a cycle that will be broken when the download finishes + mCancelable = wbp; + (void)wbp->SetProgressListener(this); + + // Save the channel using nsIWBP + rv = wbp->SaveChannel(channel, targetLocalFile); + if (NS_FAILED(rv)) { + mCancelable = nullptr; + (void)wbp->SetProgressListener(nullptr); + return rv; + } + + return SetState(nsIDownloadManager::DOWNLOAD_DOWNLOADING); +} + +NS_IMETHODIMP +nsDownload::Remove() +{ + return mDownloadManager->RemoveDownload(mGUID); +} + +NS_IMETHODIMP +nsDownload::Retry() +{ + return mDownloadManager->RetryDownload(mGUID); +} + +bool +nsDownload::IsPaused() +{ + return mDownloadState == nsIDownloadManager::DOWNLOAD_PAUSED; +} + +bool +nsDownload::IsResumable() +{ + return !mEntityID.IsEmpty(); +} + +bool +nsDownload::WasResumed() +{ + return mResumedAt != -1; +} + +bool +nsDownload::ShouldAutoResume() +{ + return mAutoResume == AUTO_RESUME; +} + +bool +nsDownload::IsFinishable() +{ + return mDownloadState == nsIDownloadManager::DOWNLOAD_NOTSTARTED || + mDownloadState == nsIDownloadManager::DOWNLOAD_QUEUED || + mDownloadState == nsIDownloadManager::DOWNLOAD_DOWNLOADING; +} + +bool +nsDownload::IsFinished() +{ + return mDownloadState == nsIDownloadManager::DOWNLOAD_FINISHED; +} + +nsresult +nsDownload::UpdateDB() +{ + NS_ASSERTION(mID, "Download ID is stored as zero. This is bad!"); + NS_ASSERTION(mDownloadManager, "Egads! We have no download manager!"); + + mozIStorageStatement *stmt = mPrivate ? + mDownloadManager->mUpdatePrivateDownloadStatement : mDownloadManager->mUpdateDownloadStatement; + + nsAutoString tempPath; + if (mTempFile) + (void)mTempFile->GetPath(tempPath); + nsresult rv = stmt->BindStringByName(NS_LITERAL_CSTRING("tempPath"), tempPath); + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("startTime"), mStartTime); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("endTime"), mLastUpdate); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("state"), mDownloadState); + NS_ENSURE_SUCCESS(rv, rv); + + if (mReferrer) { + nsAutoCString referrer; + rv = mReferrer->GetSpec(referrer); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("referrer"), referrer); + } else { + rv = stmt->BindNullByName(NS_LITERAL_CSTRING("referrer")); + } + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("entityID"), mEntityID); + NS_ENSURE_SUCCESS(rv, rv); + + int64_t currBytes; + (void)GetAmountTransferred(&currBytes); + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("currBytes"), currBytes); + NS_ENSURE_SUCCESS(rv, rv); + + int64_t maxBytes; + (void)GetSize(&maxBytes); + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("maxBytes"), maxBytes); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("autoResume"), mAutoResume); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), mID); + NS_ENSURE_SUCCESS(rv, rv); + + return stmt->Execute(); +} + +nsresult +nsDownload::FailDownload(nsresult aStatus, const char16_t *aMessage) +{ + // Grab the bundle before potentially losing our member variables + nsCOMPtr<nsIStringBundle> bundle = mDownloadManager->mBundle; + + (void)SetState(nsIDownloadManager::DOWNLOAD_FAILED); + + // Get title for alert. + nsXPIDLString title; + nsresult rv = bundle->GetStringFromName( + u"downloadErrorAlertTitle", getter_Copies(title)); + NS_ENSURE_SUCCESS(rv, rv); + + // Get a generic message if we weren't supplied one + nsXPIDLString message; + message = aMessage; + if (message.IsEmpty()) { + rv = bundle->GetStringFromName( + u"downloadErrorGeneric", getter_Copies(message)); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Get Download Manager window to be parent of alert + nsCOMPtr<nsIWindowMediator> wm = + do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<mozIDOMWindowProxy> dmWindow; + rv = wm->GetMostRecentWindow(u"Download:Manager", + getter_AddRefs(dmWindow)); + NS_ENSURE_SUCCESS(rv, rv); + + // Show alert + nsCOMPtr<nsIPromptService> prompter = + do_GetService("@mozilla.org/embedcomp/prompt-service;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + return prompter->Alert(dmWindow, title, message); +} diff --git a/components/downloads/src/nsDownloadManager.h b/components/downloads/src/nsDownloadManager.h new file mode 100644 index 000000000..566e3560a --- /dev/null +++ b/components/downloads/src/nsDownloadManager.h @@ -0,0 +1,454 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#ifndef downloadmanager___h___ +#define downloadmanager___h___ + +#if defined(XP_WIN) +#define DOWNLOAD_SCANNER +#endif + +#include "nsIDownload.h" +#include "nsIDownloadManager.h" +#include "nsIDownloadProgressListener.h" +#include "nsIFile.h" +#include "nsIMIMEInfo.h" +#include "nsINavHistoryService.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIStringBundle.h" +#include "nsISupportsPrimitives.h" +#include "nsWeakReference.h" +#include "nsITimer.h" +#include "nsString.h" + +#include "mozIDOMWindow.h" +#include "mozStorageHelper.h" +#include "nsAutoPtr.h" +#include "nsCOMArray.h" + +typedef int16_t DownloadState; +typedef int16_t DownloadType; + +class nsIArray; +class nsDownload; + +#ifdef DOWNLOAD_SCANNER +#include "nsDownloadScanner.h" +#endif + +class nsDownloadManager final : public nsIDownloadManager, + public nsINavHistoryObserver, + public nsIObserver, + public nsSupportsWeakReference +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIDOWNLOADMANAGER + NS_DECL_NSINAVHISTORYOBSERVER + NS_DECL_NSIOBSERVER + + nsresult Init(); + + static nsDownloadManager *GetSingleton(); + + nsDownloadManager() +#ifdef DOWNLOAD_SCANNER + : mScanner(nullptr) +#endif + { + } + +protected: + virtual ~nsDownloadManager(); + + nsresult InitDB(); + nsresult InitFileDB(); + void CloseAllDBs(); + void CloseDB(mozIStorageConnection* aDBConn, + mozIStorageStatement* aUpdateStmt, + mozIStorageStatement* aGetIdsStmt); + nsresult InitPrivateDB(); + already_AddRefed<mozIStorageConnection> GetFileDBConnection(nsIFile *dbFile) const; + already_AddRefed<mozIStorageConnection> GetPrivateDBConnection() const; + nsresult CreateTable(mozIStorageConnection* aDBConn); + + /** + * Fix up the database after a crash such as dealing with previously-active + * downloads. Call this before RestoreActiveDownloads to get the downloads + * fixed here to be auto-resumed. + */ + nsresult RestoreDatabaseState(); + + /** + * Paused downloads that survive across sessions are considered active, so + * rebuild the list of these downloads. + */ + nsresult RestoreActiveDownloads(); + + nsresult GetDownloadFromDB(const nsACString& aGUID, nsDownload **retVal); + nsresult GetDownloadFromDB(uint32_t aID, nsDownload **retVal); + nsresult GetDownloadFromDB(mozIStorageConnection* aDBConn, + mozIStorageStatement* stmt, + nsDownload **retVal); + + /** + * Specially track the active downloads so that we don't need to check + * every download to see if they're in progress. + */ + nsresult AddToCurrentDownloads(nsDownload *aDl); + + void SendEvent(nsDownload *aDownload, const char *aTopic); + + /** + * Adds a download with the specified information to the DB. + * + * @return The id of the download, or 0 if there was an error. + */ + int64_t AddDownloadToDB(const nsAString &aName, + const nsACString &aSource, + const nsACString &aTarget, + const nsAString &aTempPath, + int64_t aStartTime, + int64_t aEndTime, + const nsACString &aMimeType, + const nsACString &aPreferredApp, + nsHandlerInfoAction aPreferredAction, + bool aPrivate, + nsACString &aNewGUID); + + void NotifyListenersOnDownloadStateChange(int16_t aOldState, + nsDownload *aDownload); + void NotifyListenersOnProgressChange(nsIWebProgress *aProgress, + nsIRequest *aRequest, + int64_t aCurSelfProgress, + int64_t aMaxSelfProgress, + int64_t aCurTotalProgress, + int64_t aMaxTotalProgress, + nsDownload *aDownload); + void NotifyListenersOnStateChange(nsIWebProgress *aProgress, + nsIRequest *aRequest, + uint32_t aStateFlags, + nsresult aStatus, + nsDownload *aDownload); + + nsDownload *FindDownload(const nsACString& aGUID); + nsDownload *FindDownload(uint32_t aID); + + /** + * First try to resume the download, and if that fails, retry it. + * + * @param aDl The download to resume and/or retry. + */ + nsresult ResumeRetry(nsDownload *aDl); + + /** + * Pause all active downloads and remember if they should try to auto-resume + * when the download manager starts again. + * + * @param aSetResume Indicate if the downloads that get paused should be set + * as auto-resume. + */ + nsresult PauseAllDownloads(bool aSetResume); + + /** + * Resume all paused downloads unless we're only supposed to do the automatic + * ones; in that case, try to retry them as well if resuming doesn't work. + * + * @param aResumeAll If true, all downloads will be resumed; otherwise, only + * those that are marked as auto-resume will resume. + */ + nsresult ResumeAllDownloads(bool aResumeAll); + + /** + * Stop tracking the active downloads. Only use this when we're about to quit + * the download manager because we destroy our list of active downloads to + * break the dlmgr<->dl cycle. Active downloads that aren't real-paused will + * be canceled. + */ + nsresult RemoveAllDownloads(); + + /** + * Find all downloads from a source URI and delete them. + * + * @param aURI + * The source URI to remove downloads + */ + nsresult RemoveDownloadsForURI(nsIURI *aURI); + + /** + * Callback used for resuming downloads after getting a wake notification. + * + * @param aTimer + * Timer object fired after some delay after a wake notification + * @param aClosure + * nsDownloadManager object used to resume downloads + */ + static void ResumeOnWakeCallback(nsITimer *aTimer, void *aClosure); + nsCOMPtr<nsITimer> mResumeOnWakeTimer; + + void ConfirmCancelDownloads(int32_t aCount, + nsISupportsPRBool *aCancelDownloads, + const char16_t *aTitle, + const char16_t *aCancelMessageMultiple, + const char16_t *aCancelMessageSingle, + const char16_t *aDontCancelButton); + + int32_t GetRetentionBehavior(); + + /** + * Type to indicate possible behaviors for active downloads across sessions. + * + * Possible values are: + * QUIT_AND_RESUME - downloads should be auto-resumed + * QUIT_AND_PAUSE - downloads should be paused + * QUIT_AND_CANCEL - downloads should be cancelled + */ + enum QuitBehavior { + QUIT_AND_RESUME = 0, + QUIT_AND_PAUSE = 1, + QUIT_AND_CANCEL = 2 + }; + + /** + * Indicates user-set behavior for active downloads across sessions, + * + * @return value of user-set pref for active download behavior + */ + enum QuitBehavior GetQuitBehavior(); + + void OnEnterPrivateBrowsingMode(); + void OnLeavePrivateBrowsingMode(); + + nsresult RetryDownload(const nsACString& aGUID); + nsresult RetryDownload(nsDownload* dl); + + nsresult RemoveDownload(const nsACString& aGUID); + + nsresult NotifyDownloadRemoval(nsDownload* aRemoved); + + // Virus scanner for windows +#ifdef DOWNLOAD_SCANNER +private: + nsDownloadScanner* mScanner; +#endif + +private: + nsresult CleanUp(mozIStorageConnection* aDBConn); + nsresult InitStatements(mozIStorageConnection* aDBConn, + mozIStorageStatement** aUpdateStatement, + mozIStorageStatement** aGetIdsStatement); + nsresult RemoveAllDownloads(nsCOMArray<nsDownload>& aDownloads); + nsresult PauseAllDownloads(nsCOMArray<nsDownload>& aDownloads, bool aSetResume); + nsresult ResumeAllDownloads(nsCOMArray<nsDownload>& aDownloads, bool aResumeAll); + nsresult RemoveDownloadsForURI(mozIStorageStatement* aStatement, nsIURI *aURI); + + bool mUseJSTransfer; + nsCOMArray<nsIDownloadProgressListener> mListeners; + nsCOMArray<nsIDownloadProgressListener> mPrivacyAwareListeners; + nsCOMPtr<nsIStringBundle> mBundle; + nsCOMPtr<mozIStorageConnection> mDBConn; + nsCOMPtr<mozIStorageConnection> mPrivateDBConn; + nsCOMArray<nsDownload> mCurrentDownloads; + nsCOMArray<nsDownload> mCurrentPrivateDownloads; + nsCOMPtr<nsIObserverService> mObserverService; + nsCOMPtr<mozIStorageStatement> mUpdateDownloadStatement; + nsCOMPtr<mozIStorageStatement> mUpdatePrivateDownloadStatement; + nsCOMPtr<mozIStorageStatement> mGetIdsForURIStatement; + nsCOMPtr<mozIStorageStatement> mGetPrivateIdsForURIStatement; + nsAutoPtr<mozStorageTransaction> mHistoryTransaction; + + static nsDownloadManager *gDownloadManagerService; + + friend class nsDownload; +}; + +class nsDownload final : public nsIDownload +{ +public: + NS_DECL_NSIWEBPROGRESSLISTENER + NS_DECL_NSIWEBPROGRESSLISTENER2 + NS_DECL_NSITRANSFER + NS_DECL_NSIDOWNLOAD + NS_DECL_ISUPPORTS + + nsDownload(); + + /** + * This method MUST be called when changing states on a download. It will + * notify the download listener when a change happens. This also updates the + * database, by calling UpdateDB(). + */ + nsresult SetState(DownloadState aState); + +protected: + virtual ~nsDownload(); + + /** + * Finish up the download by breaking reference cycles and clearing unneeded + * data. Additionally, the download removes itself from the download + * manager's list of current downloads. + * + * NOTE: This method removes the cycle created when starting the download, so + * make sure to use kungFuDeathGrip if you want to access member variables. + */ + void Finalize(); + + /** + * For finished resumed downloads that came in from exthandler, perform the + * action that would have been done if the download wasn't resumed. + */ + nsresult ExecuteDesiredAction(); + + /** + * Move the temporary file to the final destination by removing the existing + * dummy target and renaming the temporary. + */ + nsresult MoveTempToTarget(); + + /** + * Set the target file permissions to be appropriate. + */ + nsresult FixTargetPermissions(); + + /** + * Update the start time which also implies the last update time is the same. + */ + void SetStartTime(int64_t aStartTime); + + /** + * Update the amount of bytes transferred and max bytes; and recalculate the + * download percent. + */ + void SetProgressBytes(int64_t aCurrBytes, int64_t aMaxBytes); + + /** + * All this does is cancel the connection that the download is using. It does + * not remove it from the download manager. + */ + nsresult CancelTransfer(); + + /** + * Download is not transferring? + */ + bool IsPaused(); + + /** + * Download can continue from the middle of a transfer? + */ + bool IsResumable(); + + /** + * Download was resumed? + */ + bool WasResumed(); + + /** + * Indicates if the download should try to automatically resume or not. + */ + bool ShouldAutoResume(); + + /** + * Download is in a state to stop and complete the download? + */ + bool IsFinishable(); + + /** + * Download is totally done transferring and all? + */ + bool IsFinished(); + + /** + * Update the DB with the current state of the download including time, + * download state and other values not known when first creating the + * download DB entry. + */ + nsresult UpdateDB(); + + /** + * Fail a download because of a failure status and prompt the provided + * message or use a generic download failure message if nullptr. + */ + nsresult FailDownload(nsresult aStatus, const char16_t *aMessage); + + /** + * Opens the downloaded file with the appropriate application, which is + * either the OS default, MIME type default, or the one selected by the user. + * + * This also adds the temporary file to the "To be deleted on Exit" list, if + * the corresponding user preference is set (except on OS X). + * + * This function was adopted from nsExternalAppHandler::OpenWithApplication + * (uriloader/exthandler/nsExternalHelperAppService.cpp). + */ + nsresult OpenWithApplication(); + + nsDownloadManager *mDownloadManager; + nsCOMPtr<nsIURI> mTarget; + +private: + nsString mDisplayName; + nsCString mEntityID; + nsCString mGUID; + + nsCOMPtr<nsIURI> mSource; + nsCOMPtr<nsIURI> mReferrer; + nsCOMPtr<nsICancelable> mCancelable; + nsCOMPtr<nsIRequest> mRequest; + nsCOMPtr<nsIFile> mTempFile; + nsCOMPtr<nsIMIMEInfo> mMIMEInfo; + + DownloadState mDownloadState; + + uint32_t mID; + int32_t mPercentComplete; + + /** + * These bytes are based on the position of where the request started, so 0 + * doesn't necessarily mean we have nothing. Use GetAmountTransferred and + * GetSize for the real transferred amount and size. + */ + int64_t mCurrBytes; + int64_t mMaxBytes; + + PRTime mStartTime; + PRTime mLastUpdate; + int64_t mResumedAt; + double mSpeed; + + bool mHasMultipleFiles; + bool mPrivate; + + /** + * Track various states of the download trying to auto-resume when starting + * the download manager or restoring from a crash. + * + * DONT_RESUME: Don't automatically resume the download + * AUTO_RESUME: Automaically resume the download + */ + enum AutoResume { DONT_RESUME, AUTO_RESUME }; + AutoResume mAutoResume; + + /** + * Stores the SHA-256 hash associated with the downloaded file. + */ + nsCString mHash; + + /** + * Stores the certificate chains in an nsIArray of nsIX509CertList of + * nsIX509Cert, if this binary is signed. + */ + nsCOMPtr<nsIArray> mSignatureInfo; + + /** + * Stores the redirects that led to this download in an nsIArray of + * nsIPrincipal. + */ + nsCOMPtr<nsIArray> mRedirects; + + friend class nsDownloadManager; +}; + +#endif diff --git a/components/downloads/src/nsDownloadManagerUI.js b/components/downloads/src/nsDownloadManagerUI.js new file mode 100644 index 000000000..11e241403 --- /dev/null +++ b/components/downloads/src/nsDownloadManagerUI.js @@ -0,0 +1,107 @@ +/* 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/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +// Constants + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const DOWNLOAD_MANAGER_URL = "chrome://mozapps/content/downloads/downloads.xul"; +const PREF_FLASH_COUNT = "browser.download.manager.flashCount"; + +// nsDownloadManagerUI class + +function nsDownloadManagerUI() {} + +nsDownloadManagerUI.prototype = { + classID: Components.ID("7dfdf0d1-aff6-4a34-bad1-d0fe74601642"), + + // nsIDownloadManagerUI + + show: function show(aWindowContext, aDownload, aReason, aUsePrivateUI) + { + if (!aReason) + aReason = Ci.nsIDownloadManagerUI.REASON_USER_INTERACTED; + + // First we see if it is already visible + let window = this.recentWindow; + if (window) { + window.focus(); + + // If we are being asked to show again, with a user interaction reason, + // set the appropriate variable. + if (aReason == Ci.nsIDownloadManagerUI.REASON_USER_INTERACTED) + window.gUserInteracted = true; + return; + } + + let parent = null; + // We try to get a window to use as the parent here. If we don't have one, + // the download manager will close immediately after opening if the pref + // browser.download.manager.closeWhenDone is set to true. + try { + if (aWindowContext) + parent = aWindowContext.getInterface(Ci.nsIDOMWindow); + } catch (e) { /* it's OK to not have a parent window */ } + + // We pass the download manager and the nsIDownload we want selected (if any) + var params = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); + params.appendElement(aDownload, false); + + // Pass in the reason as well + let reason = Cc["@mozilla.org/supports-PRInt16;1"]. + createInstance(Ci.nsISupportsPRInt16); + reason.data = aReason; + params.appendElement(reason, false); + + var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. + getService(Ci.nsIWindowWatcher); + ww.openWindow(parent, + DOWNLOAD_MANAGER_URL, + "Download:Manager", + "chrome,dialog=no,resizable", + params); + }, + + get visible() { + return (null != this.recentWindow); + }, + + getAttention: function getAttention() + { + if (!this.visible) + throw Cr.NS_ERROR_UNEXPECTED; + + var prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + // This preference may not be set, so defaulting to two. + let flashCount = 2; + try { + flashCount = prefs.getIntPref(PREF_FLASH_COUNT); + } catch (e) { } + + var win = this.recentWindow.QueryInterface(Ci.nsIDOMChromeWindow); + win.getAttentionWithCycleCount(flashCount); + }, + + // nsDownloadManagerUI + + get recentWindow() { + var wm = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); + return wm.getMostRecentWindow("Download:Manager"); + }, + + // nsISupports + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDownloadManagerUI]) +}; + +// Module + +var components = [nsDownloadManagerUI]; +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); + diff --git a/components/downloads/src/nsDownloadProxy.h b/components/downloads/src/nsDownloadProxy.h new file mode 100644 index 000000000..ca48c9dad --- /dev/null +++ b/components/downloads/src/nsDownloadProxy.h @@ -0,0 +1,179 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#ifndef downloadproxy___h___ +#define downloadproxy___h___ + +#include "nsIDownloadManager.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsIMIMEInfo.h" +#include "nsIFileURL.h" +#include "nsIDownloadManagerUI.h" + +#define PREF_BDM_SHOWWHENSTARTING "browser.download.manager.showWhenStarting" +#define PREF_BDM_FOCUSWHENSTARTING "browser.download.manager.focusWhenStarting" + +// This class only exists because nsDownload cannot inherit from nsITransfer +// directly. The reason for this is that nsDownloadManager (incorrectly) keeps +// an nsCOMArray of nsDownloads, and nsCOMArray is only intended for use with +// abstract classes. Using a concrete class that multiply inherits from classes +// deriving from nsISupports will throw ambiguous base class errors. +class nsDownloadProxy : public nsITransfer +{ +protected: + + virtual ~nsDownloadProxy() { } + +public: + + nsDownloadProxy() { } + + NS_DECL_ISUPPORTS + + NS_IMETHOD Init(nsIURI* aSource, + nsIURI* aTarget, + const nsAString& aDisplayName, + nsIMIMEInfo *aMIMEInfo, + PRTime aStartTime, + nsIFile* aTempFile, + nsICancelable* aCancelable, + bool aIsPrivate) override { + nsresult rv; + nsCOMPtr<nsIDownloadManager> dm = do_GetService("@mozilla.org/download-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = dm->AddDownload(nsIDownloadManager::DOWNLOAD_TYPE_DOWNLOAD, aSource, + aTarget, aDisplayName, aMIMEInfo, aStartTime, + aTempFile, aCancelable, aIsPrivate, + getter_AddRefs(mInner)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrefService> prefs = do_GetService("@mozilla.org/preferences-service;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIPrefBranch> branch = do_QueryInterface(prefs); + + bool showDM = true; + if (branch) + branch->GetBoolPref(PREF_BDM_SHOWWHENSTARTING, &showDM); + + if (showDM) { + nsCOMPtr<nsIDownloadManagerUI> dmui = + do_GetService("@mozilla.org/download-manager-ui;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + bool visible; + rv = dmui->GetVisible(&visible); + NS_ENSURE_SUCCESS(rv, rv); + + bool focusWhenStarting = true; + if (branch) + (void)branch->GetBoolPref(PREF_BDM_FOCUSWHENSTARTING, &focusWhenStarting); + + if (visible && !focusWhenStarting) + return NS_OK; + + return dmui->Show(nullptr, mInner, nsIDownloadManagerUI::REASON_NEW_DOWNLOAD, aIsPrivate); + } + return rv; + } + + NS_IMETHOD OnStateChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aStateFlags, + nsresult aStatus) override + { + NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED); + return mInner->OnStateChange(aWebProgress, aRequest, aStateFlags, aStatus); + } + + NS_IMETHOD OnStatusChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, nsresult aStatus, + const char16_t *aMessage) override + { + NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED); + return mInner->OnStatusChange(aWebProgress, aRequest, aStatus, aMessage); + } + + NS_IMETHOD OnLocationChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, nsIURI *aLocation, + uint32_t aFlags) override + { + NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED); + return mInner->OnLocationChange(aWebProgress, aRequest, aLocation, aFlags); + } + + NS_IMETHOD OnProgressChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, + int32_t aCurSelfProgress, + int32_t aMaxSelfProgress, + int32_t aCurTotalProgress, + int32_t aMaxTotalProgress) override + { + NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED); + return mInner->OnProgressChange(aWebProgress, aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress); + } + + NS_IMETHOD OnProgressChange64(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, + int64_t aCurSelfProgress, + int64_t aMaxSelfProgress, + int64_t aCurTotalProgress, + int64_t aMaxTotalProgress) override + { + NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED); + return mInner->OnProgressChange64(aWebProgress, aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress); + } + + NS_IMETHOD OnRefreshAttempted(nsIWebProgress *aWebProgress, + nsIURI *aUri, + int32_t aDelay, + bool aSameUri, + bool *allowRefresh) override + { + *allowRefresh = true; + return NS_OK; + } + + NS_IMETHOD OnSecurityChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, uint32_t aState) override + { + NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED); + return mInner->OnSecurityChange(aWebProgress, aRequest, aState); + } + + NS_IMETHOD SetSha256Hash(const nsACString& aHash) override + { + NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED); + return mInner->SetSha256Hash(aHash); + } + + NS_IMETHOD SetSignatureInfo(nsIArray* aSignatureInfo) override + { + NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED); + return mInner->SetSignatureInfo(aSignatureInfo); + } + + NS_IMETHOD SetRedirects(nsIArray* aRedirects) override + { + NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED); + return mInner->SetRedirects(aRedirects); + } + +private: + nsCOMPtr<nsIDownload> mInner; +}; + +NS_IMPL_ISUPPORTS(nsDownloadProxy, nsITransfer, + nsIWebProgressListener, nsIWebProgressListener2) + +#endif diff --git a/components/downloads/src/nsDownloadScanner.cpp b/components/downloads/src/nsDownloadScanner.cpp new file mode 100644 index 000000000..1ef5b3660 --- /dev/null +++ b/components/downloads/src/nsDownloadScanner.cpp @@ -0,0 +1,728 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: se cin sw=2 ts=2 et : */ +/* 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/. */ + +#include "nsDownloadScanner.h" +#include <comcat.h> +#include <process.h> +#include "nsDownloadManager.h" +#include "nsIXULAppInfo.h" +#include "nsXULAppAPI.h" +#include "nsIPrefService.h" +#include "nsNetUtil.h" +#include "prtime.h" +#include "nsDeque.h" +#include "nsIFileURL.h" +#include "nsIPrefBranch.h" +#include "nsXPCOMCIDInternal.h" + +/** + * Code overview + * + * Download scanner attempts to make use of one of two different virus + * scanning interfaces available on Windows - IOfficeAntiVirus (Windows + * 95/NT 4 and IE 5) and IAttachmentExecute (XPSP2 and up). The latter + * interface supports calling IOfficeAntiVirus internally, while also + * adding support for XPSP2+ ADS forks which define security related + * prompting on downloaded content. + * + * Both interfaces are synchronous and can take a while, so it is not a + * good idea to call either from the main thread. Some antivirus scanners can + * take a long time to scan or the call might block while the scanner shows + * its UI so if the user were to download many files that finished around the + * same time, they would have to wait a while if the scanning were done on + * exactly one other thread. Since the overhead of creating a thread is + * relatively small compared to the time it takes to download a file and scan + * it, a new thread is spawned for each download that is to be scanned. Since + * most of the mozilla codebase is not threadsafe, all the information needed + * for the scanner is gathered in the main thread in nsDownloadScanner::Scan::Start. + * The only function of nsDownloadScanner::Scan which is invoked on another + * thread is DoScan. + * + * Watchdog overview + * + * The watchdog is used internally by the scanner. It maintains a queue of + * current download scans. In a separate thread, it dequeues the oldest scan + * and waits on that scan's thread with a timeout given by WATCHDOG_TIMEOUT + * (default is 30 seconds). If the wait times out, then the watchdog notifies + * the Scan that it has timed out. If the scan really has timed out, then the + * Scan object will dispatch its run method to the main thread; this will + * release the watchdog thread's addref on the Scan. If it has not timed out + * (i.e. the Scan just finished in time), then the watchdog dispatches a + * ReleaseDispatcher to release its ref of the Scan on the main thread. + * + * In order to minimize execution time, there are two events used to notify the + * watchdog thread of a non-empty queue and a quit event. Every blocking wait + * that the watchdog thread does waits on the quit event; this lets the thread + * quickly exit when shutting down. Also, the download scan queue will be empty + * most of the time; rather than use a spin loop, a simple event is triggered + * by the main thread when a new scan is added to an empty queue. When the + * watchdog thread knows that it has run out of elements in the queue, it will + * wait on the new item event. + * + * Memory/resource leaks due to timeout: + * In the event of a timeout, the thread must remain alive; terminating it may + * very well cause the antivirus scanner to crash or be put into an + * inconsistent state; COM resources may also not be cleaned up. The downside + * is that we need to leave the thread running; suspending it may lead to a + * deadlock. Because the scan call may be ongoing, it may be dependent on the + * memory referenced by the MSOAVINFO structure, so we cannot free mName, mPath + * or mOrigin; this means that we cannot free the Scan object since doing so + * will deallocate that memory. Note that mDownload is set to null upon timeout + * or completion, so the download itself is never leaked. If the scan does + * eventually complete, then the all the memory and resources will be freed. + * It is possible, however extremely rare, that in the event of a timeout, the + * mStateSync critical section will leak its event; this will happen only if + * the scanning thread, watchdog thread or main thread try to enter the + * critical section when one of the others is already in it. + * + * Reasoning for CheckAndSetState - there exists a race condition between the time when + * either the timeout or normal scan sets the state and when Scan::Run is + * executed on the main thread. Ex: mStatus could be set by Scan::DoScan* which + * then queues a dispatch on the main thread. Before that dispatch is executed, + * the timeout code fires and sets mStatus to AVSCAN_TIMEDOUT which then queues + * its dispatch to the main thread (the same function as DoScan*). Both + * dispatches run and both try to set the download state to AVSCAN_TIMEDOUT + * which is incorrect. + * + * There are 5 possible outcomes of the virus scan: + * AVSCAN_GOOD => the file is clean + * AVSCAN_BAD => the file has a virus + * AVSCAN_UGLY => the file had a virus, but it was cleaned + * AVSCAN_FAILED => something else went wrong with the virus scanner. + * AVSCAN_TIMEDOUT => the scan (thread setup + execution) took too long + * + * Both the good and ugly states leave the user with a benign file, so they + * transition to the finished state. Bad files are sent to the blocked state. + * The failed and timedout states transition to finished downloads. + * + * Possible Future enhancements: + * * Create an interface for scanning files in general + * * Make this a service + * * Get antivirus scanner status via WMI/registry + */ + +// IAttachementExecute supports user definable settings for certain +// security related prompts. This defines a general GUID for use in +// all projects. Individual projects can define an individual guid +// if they want to. +#ifndef MOZ_VIRUS_SCANNER_PROMPT_GUID +#define MOZ_VIRUS_SCANNER_PROMPT_GUID \ + { 0xb50563d1, 0x16b6, 0x43c2, { 0xa6, 0x6a, 0xfa, 0xe6, 0xd2, 0x11, 0xf2, \ + 0xea } } +#endif +static const GUID GUID_MozillaVirusScannerPromptGeneric = + MOZ_VIRUS_SCANNER_PROMPT_GUID; + +// Initial timeout is 30 seconds +#define WATCHDOG_TIMEOUT (30*PR_USEC_PER_SEC) + +// Maximum length for URI's passed into IAE +#define MAX_IAEURILENGTH 1683 + +class nsDownloadScannerWatchdog +{ + typedef nsDownloadScanner::Scan Scan; +public: + nsDownloadScannerWatchdog(); + ~nsDownloadScannerWatchdog(); + + nsresult Init(); + nsresult Shutdown(); + + void Watch(Scan *scan); +private: + static unsigned int __stdcall WatchdogThread(void *p); + CRITICAL_SECTION mQueueSync; + nsDeque mScanQueue; + HANDLE mThread; + HANDLE mNewItemEvent; + HANDLE mQuitEvent; +}; + +nsDownloadScanner::nsDownloadScanner() : + mAESExists(false) +{ +} + +// This destructor appeases the compiler; it would otherwise complain about an +// incomplete type for nsDownloadWatchdog in the instantiation of +// nsAutoPtr::~nsAutoPtr +// Plus, it's a handy location to call nsDownloadScannerWatchdog::Shutdown from +nsDownloadScanner::~nsDownloadScanner() { + if (mWatchdog) + (void)mWatchdog->Shutdown(); +} + +nsresult +nsDownloadScanner::Init() +{ + // This CoInitialize/CoUninitialize pattern seems to be common in the Mozilla + // codebase. All other COM calls/objects are made on different threads. + nsresult rv = NS_OK; + CoInitialize(nullptr); + + if (!IsAESAvailable()) { + CoUninitialize(); + return NS_ERROR_NOT_AVAILABLE; + } + + mAESExists = true; + + // Initialize scanning + mWatchdog = new nsDownloadScannerWatchdog(); + if (mWatchdog) { + rv = mWatchdog->Init(); + if (FAILED(rv)) + mWatchdog = nullptr; + } else { + rv = NS_ERROR_OUT_OF_MEMORY; + } + + if (NS_FAILED(rv)) + return rv; + + return rv; +} + +bool +nsDownloadScanner::IsAESAvailable() +{ + // Try to instantiate IAE to see if it's available. + RefPtr<IAttachmentExecute> ae; + HRESULT hr; + hr = CoCreateInstance(CLSID_AttachmentServices, nullptr, CLSCTX_INPROC, + IID_IAttachmentExecute, getter_AddRefs(ae)); + if (FAILED(hr)) { + NS_WARNING("Could not instantiate attachment execution service\n"); + return false; + } + return true; +} + +// If IAttachementExecute is available, use the CheckPolicy call to find out +// if this download should be prevented due to Security Zone Policy settings. +AVCheckPolicyState +nsDownloadScanner::CheckPolicy(nsIURI *aSource, nsIURI *aTarget) +{ + nsresult rv; + + if (!mAESExists || !aSource || !aTarget) + return AVPOLICY_DOWNLOAD; + + nsAutoCString source; + rv = aSource->GetSpec(source); + if (NS_FAILED(rv)) + return AVPOLICY_DOWNLOAD; + + nsCOMPtr<nsIFileURL> fileUrl(do_QueryInterface(aTarget)); + if (!fileUrl) + return AVPOLICY_DOWNLOAD; + + nsCOMPtr<nsIFile> theFile; + nsAutoString aFileName; + if (NS_FAILED(fileUrl->GetFile(getter_AddRefs(theFile))) || + NS_FAILED(theFile->GetLeafName(aFileName))) + return AVPOLICY_DOWNLOAD; + + // IAttachementExecute prohibits src data: schemes by default but we + // support them. If this is a data src, skip off doing a policy check. + // (The file will still be scanned once it lands on the local system.) + bool isDataScheme(false); + nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(aSource); + if (innerURI) + (void)innerURI->SchemeIs("data", &isDataScheme); + if (isDataScheme) + return AVPOLICY_DOWNLOAD; + + RefPtr<IAttachmentExecute> ae; + HRESULT hr; + hr = CoCreateInstance(CLSID_AttachmentServices, nullptr, CLSCTX_INPROC, + IID_IAttachmentExecute, getter_AddRefs(ae)); + if (FAILED(hr)) + return AVPOLICY_DOWNLOAD; + + (void)ae->SetClientGuid(GUID_MozillaVirusScannerPromptGeneric); + (void)ae->SetSource(NS_ConvertUTF8toUTF16(source).get()); + (void)ae->SetFileName(aFileName.get()); + + // Any failure means the file download/exec will be blocked by the system. + // S_OK or S_FALSE imply it's ok. + hr = ae->CheckPolicy(); + + if (hr == S_OK) + return AVPOLICY_DOWNLOAD; + + if (hr == S_FALSE) + return AVPOLICY_PROMPT; + + if (hr == E_INVALIDARG) + return AVPOLICY_PROMPT; + + return AVPOLICY_BLOCKED; +} + +#ifndef THREAD_MODE_BACKGROUND_BEGIN +#define THREAD_MODE_BACKGROUND_BEGIN 0x00010000 +#endif + +#ifndef THREAD_MODE_BACKGROUND_END +#define THREAD_MODE_BACKGROUND_END 0x00020000 +#endif + +unsigned int __stdcall +nsDownloadScanner::ScannerThreadFunction(void *p) +{ + HANDLE currentThread = GetCurrentThread(); + NS_ASSERTION(!NS_IsMainThread(), "Antivirus scan should not be run on the main thread"); + nsDownloadScanner::Scan *scan = static_cast<nsDownloadScanner::Scan*>(p); + if (!SetThreadPriority(currentThread, THREAD_MODE_BACKGROUND_BEGIN)) + (void)SetThreadPriority(currentThread, THREAD_PRIORITY_IDLE); + scan->DoScan(); + (void)SetThreadPriority(currentThread, THREAD_MODE_BACKGROUND_END); + _endthreadex(0); + return 0; +} + +// The sole purpose of this class is to release an object on the main thread +// It assumes that its creator will addref it and it will release itself on +// the main thread too +class ReleaseDispatcher : public mozilla::Runnable { +public: + ReleaseDispatcher(nsISupports *ptr) + : mPtr(ptr) {} + NS_IMETHOD Run(); +private: + nsISupports *mPtr; +}; + +nsresult ReleaseDispatcher::Run() { + NS_ASSERTION(NS_IsMainThread(), "Antivirus scan release dispatch should be run on the main thread"); + NS_RELEASE(mPtr); + NS_RELEASE_THIS(); + return NS_OK; +} + +nsDownloadScanner::Scan::Scan(nsDownloadScanner *scanner, nsDownload *download) + : mDLScanner(scanner), mThread(nullptr), + mDownload(download), mStatus(AVSCAN_NOTSTARTED), + mSkipSource(false) +{ + InitializeCriticalSection(&mStateSync); +} + +nsDownloadScanner::Scan::~Scan() { + DeleteCriticalSection(&mStateSync); +} + +nsresult +nsDownloadScanner::Scan::Start() +{ + mStartTime = PR_Now(); + + mThread = (HANDLE)_beginthreadex(nullptr, 0, ScannerThreadFunction, + this, CREATE_SUSPENDED, nullptr); + if (!mThread) + return NS_ERROR_OUT_OF_MEMORY; + + nsresult rv = NS_OK; + + // Get the path to the file on disk + nsCOMPtr<nsIFile> file; + rv = mDownload->GetTargetFile(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + rv = file->GetPath(mPath); + NS_ENSURE_SUCCESS(rv, rv); + + // Grab the app name + nsCOMPtr<nsIXULAppInfo> appinfo = + do_GetService(XULAPPINFO_SERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString name; + rv = appinfo->GetName(name); + NS_ENSURE_SUCCESS(rv, rv); + CopyUTF8toUTF16(name, mName); + + // Get the origin + nsCOMPtr<nsIURI> uri; + rv = mDownload->GetSource(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString origin; + rv = uri->GetSpec(origin); + NS_ENSURE_SUCCESS(rv, rv); + + // Certain virus interfaces do not like extremely long uris. + // Chop off the path and cgi data and just pass the base domain. + if (origin.Length() > MAX_IAEURILENGTH) { + rv = uri->GetPrePath(origin); + NS_ENSURE_SUCCESS(rv, rv); + } + + CopyUTF8toUTF16(origin, mOrigin); + + // We count https/ftp/http as an http download + bool isHttp(false), isFtp(false), isHttps(false); + nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(uri); + if (!innerURI) innerURI = uri; + (void)innerURI->SchemeIs("http", &isHttp); + (void)innerURI->SchemeIs("ftp", &isFtp); + (void)innerURI->SchemeIs("https", &isHttps); + mIsHttpDownload = isHttp || isFtp || isHttps; + + // IAttachementExecute prohibits src data: schemes by default but we + // support them. Mark the download if it's a data scheme, so we + // can skip off supplying the src to IAttachementExecute when we scan + // the resulting file. + (void)innerURI->SchemeIs("data", &mSkipSource); + + // ResumeThread returns the previous suspend count + if (1 != ::ResumeThread(mThread)) { + CloseHandle(mThread); + return NS_ERROR_UNEXPECTED; + } + return NS_OK; +} + +nsresult +nsDownloadScanner::Scan::Run() +{ + NS_ASSERTION(NS_IsMainThread(), "Antivirus scan dispatch should be run on the main thread"); + + // Cleanup our thread + if (mStatus != AVSCAN_TIMEDOUT) + WaitForSingleObject(mThread, INFINITE); + CloseHandle(mThread); + + DownloadState downloadState = 0; + EnterCriticalSection(&mStateSync); + switch (mStatus) { + case AVSCAN_BAD: + downloadState = nsIDownloadManager::DOWNLOAD_DIRTY; + break; + default: + case AVSCAN_FAILED: + case AVSCAN_GOOD: + case AVSCAN_UGLY: + case AVSCAN_TIMEDOUT: + downloadState = nsIDownloadManager::DOWNLOAD_FINISHED; + break; + } + LeaveCriticalSection(&mStateSync); + // Download will be null if we already timed out + if (mDownload) + (void)mDownload->SetState(downloadState); + + // Clean up some other variables + // In the event of a timeout, our destructor won't be called + mDownload = nullptr; + + NS_RELEASE_THIS(); + return NS_OK; +} + +static DWORD +ExceptionFilterFunction(DWORD exceptionCode) { + switch(exceptionCode) { + case EXCEPTION_ACCESS_VIOLATION: + case EXCEPTION_ILLEGAL_INSTRUCTION: + case EXCEPTION_IN_PAGE_ERROR: + case EXCEPTION_PRIV_INSTRUCTION: + case EXCEPTION_STACK_OVERFLOW: + return EXCEPTION_EXECUTE_HANDLER; + default: + return EXCEPTION_CONTINUE_SEARCH; + } +} + +bool +nsDownloadScanner::Scan::DoScanAES() +{ + // This warning is for the destructor of ae which will not be invoked in the + // event of a win32 exception +#pragma warning(disable: 4509) + HRESULT hr; + RefPtr<IAttachmentExecute> ae; + MOZ_SEH_TRY { + hr = CoCreateInstance(CLSID_AttachmentServices, nullptr, CLSCTX_ALL, + IID_IAttachmentExecute, getter_AddRefs(ae)); + } MOZ_SEH_EXCEPT(ExceptionFilterFunction(GetExceptionCode())) { + return CheckAndSetState(AVSCAN_NOTSTARTED,AVSCAN_FAILED); + } + + // If we (somehow) already timed out, then don't bother scanning + if (CheckAndSetState(AVSCAN_SCANNING, AVSCAN_NOTSTARTED)) { + AVScanState newState; + if (SUCCEEDED(hr)) { + bool gotException = false; + MOZ_SEH_TRY { + (void)ae->SetClientGuid(GUID_MozillaVirusScannerPromptGeneric); + (void)ae->SetLocalPath(mPath.get()); + // Provide the src for everything but data: schemes. + if (!mSkipSource) + (void)ae->SetSource(mOrigin.get()); + + // Save() will invoke the scanner + hr = ae->Save(); + } MOZ_SEH_EXCEPT(ExceptionFilterFunction(GetExceptionCode())) { + gotException = true; + } + + MOZ_SEH_TRY { + ae = nullptr; + } MOZ_SEH_EXCEPT(ExceptionFilterFunction(GetExceptionCode())) { + gotException = true; + } + + if(gotException) { + newState = AVSCAN_FAILED; + } + else if (SUCCEEDED(hr)) { // Passed the scan + newState = AVSCAN_GOOD; + } + else if (HRESULT_CODE(hr) == ERROR_FILE_NOT_FOUND) { + NS_WARNING("Downloaded file disappeared before it could be scanned"); + newState = AVSCAN_FAILED; + } + else if (hr == E_INVALIDARG) { + NS_WARNING("IAttachementExecute returned invalid argument error"); + newState = AVSCAN_FAILED; + } + else { + newState = AVSCAN_UGLY; + } + } + else { + newState = AVSCAN_FAILED; + } + return CheckAndSetState(newState, AVSCAN_SCANNING); + } + return false; +} +#pragma warning(default: 4509) + +void +nsDownloadScanner::Scan::DoScan() +{ + CoInitialize(nullptr); + + if (DoScanAES()) { + // We need to do a few more things on the main thread + NS_DispatchToMainThread(this); + } else { + // We timed out, so just release + ReleaseDispatcher* releaser = new ReleaseDispatcher(this); + if(releaser) { + NS_ADDREF(releaser); + NS_DispatchToMainThread(releaser); + } + } + + MOZ_SEH_TRY { + CoUninitialize(); + } MOZ_SEH_EXCEPT(ExceptionFilterFunction(GetExceptionCode())) { + // Not much we can do at this point... + } +} + +HANDLE +nsDownloadScanner::Scan::GetWaitableThreadHandle() const +{ + HANDLE targetHandle = INVALID_HANDLE_VALUE; + (void)DuplicateHandle(GetCurrentProcess(), mThread, + GetCurrentProcess(), &targetHandle, + SYNCHRONIZE, // Only allow clients to wait on this handle + FALSE, // cannot be inherited by child processes + 0); + return targetHandle; +} + +bool +nsDownloadScanner::Scan::NotifyTimeout() +{ + bool didTimeout = CheckAndSetState(AVSCAN_TIMEDOUT, AVSCAN_SCANNING) || + CheckAndSetState(AVSCAN_TIMEDOUT, AVSCAN_NOTSTARTED); + if (didTimeout) { + // We need to do a few more things on the main thread + NS_DispatchToMainThread(this); + } + return didTimeout; +} + +bool +nsDownloadScanner::Scan::CheckAndSetState(AVScanState newState, AVScanState expectedState) { + bool gotExpectedState = false; + EnterCriticalSection(&mStateSync); + if((gotExpectedState = (mStatus == expectedState))) + mStatus = newState; + LeaveCriticalSection(&mStateSync); + return gotExpectedState; +} + +nsresult +nsDownloadScanner::ScanDownload(nsDownload *download) +{ + if (!mAESExists) + return NS_ERROR_NOT_AVAILABLE; + + // No ref ptr, see comment below + Scan *scan = new Scan(this, download); + if (!scan) + return NS_ERROR_OUT_OF_MEMORY; + + NS_ADDREF(scan); + + nsresult rv = scan->Start(); + + // Note that we only release upon error. On success, the scan is passed off + // to a new thread. It is eventually released in Scan::Run on the main thread. + if (NS_FAILED(rv)) + NS_RELEASE(scan); + else + // Notify the watchdog + mWatchdog->Watch(scan); + + return rv; +} + +nsDownloadScannerWatchdog::nsDownloadScannerWatchdog() + : mNewItemEvent(nullptr), mQuitEvent(nullptr) { + InitializeCriticalSection(&mQueueSync); +} +nsDownloadScannerWatchdog::~nsDownloadScannerWatchdog() { + DeleteCriticalSection(&mQueueSync); +} + +nsresult +nsDownloadScannerWatchdog::Init() { + // Both events are auto-reset + mNewItemEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); + if (INVALID_HANDLE_VALUE == mNewItemEvent) + return NS_ERROR_OUT_OF_MEMORY; + mQuitEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); + if (INVALID_HANDLE_VALUE == mQuitEvent) { + (void)CloseHandle(mNewItemEvent); + return NS_ERROR_OUT_OF_MEMORY; + } + + // This thread is always running, however it will be asleep + // for most of the dlmgr's lifetime + mThread = (HANDLE)_beginthreadex(nullptr, 0, WatchdogThread, + this, 0, nullptr); + if (!mThread) { + (void)CloseHandle(mNewItemEvent); + (void)CloseHandle(mQuitEvent); + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +nsresult +nsDownloadScannerWatchdog::Shutdown() { + // Tell the watchdog thread to quite + (void)SetEvent(mQuitEvent); + (void)WaitForSingleObject(mThread, INFINITE); + (void)CloseHandle(mThread); + // Manually clear and release the queued scans + while (mScanQueue.GetSize() != 0) { + Scan *scan = reinterpret_cast<Scan*>(mScanQueue.Pop()); + NS_RELEASE(scan); + } + (void)CloseHandle(mNewItemEvent); + (void)CloseHandle(mQuitEvent); + return NS_OK; +} + +void +nsDownloadScannerWatchdog::Watch(Scan *scan) { + bool wasEmpty; + // Note that there is no release in this method + // The scan will be released by the watchdog ALWAYS on the main thread + // when either the watchdog thread processes the scan or the watchdog + // is shut down + NS_ADDREF(scan); + EnterCriticalSection(&mQueueSync); + wasEmpty = mScanQueue.GetSize()==0; + mScanQueue.Push(scan); + LeaveCriticalSection(&mQueueSync); + // If the queue was empty, then the watchdog thread is/will be asleep + if (wasEmpty) + (void)SetEvent(mNewItemEvent); +} + +unsigned int +__stdcall +nsDownloadScannerWatchdog::WatchdogThread(void *p) { + NS_ASSERTION(!NS_IsMainThread(), "Antivirus scan watchdog should not be run on the main thread"); + nsDownloadScannerWatchdog *watchdog = (nsDownloadScannerWatchdog*)p; + HANDLE waitHandles[3] = {watchdog->mNewItemEvent, watchdog->mQuitEvent, INVALID_HANDLE_VALUE}; + DWORD waitStatus; + DWORD queueItemsLeft = 0; + // Loop until quit event or error + while (0 != queueItemsLeft || + ((WAIT_OBJECT_0 + 1) != + (waitStatus = + WaitForMultipleObjects(2, waitHandles, FALSE, INFINITE)) && + waitStatus != WAIT_FAILED)) { + Scan *scan = nullptr; + PRTime startTime, expectedEndTime, now; + DWORD waitTime; + + // Pop scan from queue + EnterCriticalSection(&watchdog->mQueueSync); + scan = reinterpret_cast<Scan*>(watchdog->mScanQueue.Pop()); + queueItemsLeft = watchdog->mScanQueue.GetSize(); + LeaveCriticalSection(&watchdog->mQueueSync); + + // Calculate expected end time + startTime = scan->GetStartTime(); + expectedEndTime = WATCHDOG_TIMEOUT + startTime; + now = PR_Now(); + // PRTime is not guaranteed to be a signed integral type (afaik), but + // currently it is + if (now > expectedEndTime) { + waitTime = 0; + } else { + // This is a positive value, and we know that it will not overflow + // (bounded by WATCHDOG_TIMEOUT) + // waitTime is in milliseconds, nspr uses microseconds + waitTime = static_cast<DWORD>((expectedEndTime - now)/PR_USEC_PER_MSEC); + } + HANDLE hThread = waitHandles[2] = scan->GetWaitableThreadHandle(); + + // Wait for the thread (obj 1) or quit event (obj 0) + waitStatus = WaitForMultipleObjects(2, (waitHandles+1), FALSE, waitTime); + CloseHandle(hThread); + + ReleaseDispatcher* releaser = new ReleaseDispatcher(scan); + if(!releaser) + continue; + NS_ADDREF(releaser); + // Got quit event or error + if (waitStatus == WAIT_FAILED || waitStatus == WAIT_OBJECT_0) { + NS_DispatchToMainThread(releaser); + break; + // Thread exited normally + } else if (waitStatus == (WAIT_OBJECT_0+1)) { + NS_DispatchToMainThread(releaser); + continue; + // Timeout case + } else { + NS_ASSERTION(waitStatus == WAIT_TIMEOUT, "Unexpected wait status in dlmgr watchdog thread"); + if (!scan->NotifyTimeout()) { + // If we didn't time out, then release the thread + NS_DispatchToMainThread(releaser); + } else { + // NotifyTimeout did a dispatch which will release the scan, so we + // don't need to release the scan + NS_RELEASE(releaser); + } + } + } + _endthreadex(0); + return 0; +} diff --git a/components/downloads/src/nsDownloadScanner.h b/components/downloads/src/nsDownloadScanner.h new file mode 100644 index 000000000..3301489fe --- /dev/null +++ b/components/downloads/src/nsDownloadScanner.h @@ -0,0 +1,121 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +/* vim: se cin sw=2 ts=2 et : */ + +#ifndef nsDownloadScanner_h_ +#define nsDownloadScanner_h_ + +#ifdef WIN32_LEAN_AND_MEAN +#undef WIN32_LEAN_AND_MEAN +#endif +#include <windows.h> +#define AVVENDOR +#include <objidl.h> +#include <msoav.h> +#include <shlobj.h> + +#include "nsAutoPtr.h" +#include "nsThreadUtils.h" +#include "nsTArray.h" +#include "nsIObserver.h" +#include "nsIURI.h" + +enum AVScanState +{ + AVSCAN_NOTSTARTED = 0, + AVSCAN_SCANNING, + AVSCAN_GOOD, + AVSCAN_BAD, + AVSCAN_UGLY, + AVSCAN_FAILED, + AVSCAN_TIMEDOUT +}; + +enum AVCheckPolicyState +{ + AVPOLICY_DOWNLOAD, + AVPOLICY_PROMPT, + AVPOLICY_BLOCKED +}; + +// See nsDownloadScanner.cpp for declaration and definition +class nsDownloadScannerWatchdog; +class nsDownload; + +class nsDownloadScanner +{ +public: + nsDownloadScanner(); + ~nsDownloadScanner(); + nsresult Init(); + nsresult ScanDownload(nsDownload *download); + AVCheckPolicyState CheckPolicy(nsIURI *aSource, nsIURI *aTarget); + +private: + bool mAESExists; + nsTArray<CLSID> mScanCLSID; + bool IsAESAvailable(); + bool EnumerateOAVProviders(); + + nsAutoPtr<nsDownloadScannerWatchdog> mWatchdog; + + static unsigned int __stdcall ScannerThreadFunction(void *p); + class Scan : public mozilla::Runnable + { + public: + Scan(nsDownloadScanner *scanner, nsDownload *download); + ~Scan(); + nsresult Start(); + + // Returns the time that Start was called + PRTime GetStartTime() const { return mStartTime; } + // Returns a copy of the thread handle that can be waited on, but not + // terminated + // The caller is responsible for closing the handle + // If the thread has terminated, then this will return the pseudo-handle + // INVALID_HANDLE_VALUE + HANDLE GetWaitableThreadHandle() const; + + // Called on a secondary thread to notify the scan that it has timed out + // this is used only by the watchdog thread + bool NotifyTimeout(); + + private: + nsDownloadScanner *mDLScanner; + PRTime mStartTime; + HANDLE mThread; + RefPtr<nsDownload> mDownload; + // Guards mStatus + CRITICAL_SECTION mStateSync; + AVScanState mStatus; + nsString mPath; + nsString mName; + nsString mOrigin; + // Also true if it is an ftp download + bool mIsHttpDownload; + bool mSkipSource; + + /* @summary Sets the Scan's state to newState if the current state is + expectedState + * @param newState The new state of the scan + * @param expectedState The state that the caller expects the scan to be in + * @return If the old state matched expectedState + */ + bool CheckAndSetState(AVScanState newState, AVScanState expectedState); + + NS_IMETHOD Run(); + + void DoScan(); + bool DoScanAES(); + bool DoScanOAV(); + + friend unsigned int __stdcall nsDownloadScanner::ScannerThreadFunction(void *); + }; + // Used to give access to Scan + friend class nsDownloadScannerWatchdog; +}; +#endif + diff --git a/components/downloads/src/nsHelperAppDlg.js b/components/downloads/src/nsHelperAppDlg.js new file mode 100644 index 000000000..0e5cfdaf0 --- /dev/null +++ b/components/downloads/src/nsHelperAppDlg.js @@ -0,0 +1,1138 @@ +/* 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/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 = prefs.getBoolPref(PREF_BD_USEDOWNLOADDIR, false); + + 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); + +#ifdef XP_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); + } +#endif + + 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... +#ifdef XP_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; +#else + // 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; +#endif + }, + + // 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) { + 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) + { +#ifdef XP_WIN + if (file instanceof Components.interfaces.nsILocalFileWin) { + try { + return file.getVersionInfoField("FileDescription"); + } catch (e) {} + } +#endif + + 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)); + +#ifdef XP_WIN + otherHandler.label = this.getFileDisplayName(this.chosenApp.executable); +#else + otherHandler.label = this.chosenApp.name; +#endif + + 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() { +#ifdef XP_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 // XP_WIN +#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 // MOZ_WIDGET_GTK == 3 + 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 +#endif // XP_WIN + 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]); |