summaryrefslogtreecommitdiff
path: root/components/downloads/src
diff options
context:
space:
mode:
Diffstat (limited to 'components/downloads/src')
-rw-r--r--components/downloads/src/DownloadLastDir.jsm195
-rw-r--r--components/downloads/src/DownloadPaths.jsm88
-rw-r--r--components/downloads/src/DownloadTaskbarProgress.jsm400
-rw-r--r--components/downloads/src/DownloadUtils.jsm600
-rw-r--r--components/downloads/src/SQLFunctions.cpp146
-rw-r--r--components/downloads/src/SQLFunctions.h46
-rw-r--r--components/downloads/src/nsDownloadManager.cpp3711
-rw-r--r--components/downloads/src/nsDownloadManager.h454
-rw-r--r--components/downloads/src/nsDownloadManagerUI.js107
-rw-r--r--components/downloads/src/nsDownloadProxy.h179
-rw-r--r--components/downloads/src/nsDownloadScanner.cpp728
-rw-r--r--components/downloads/src/nsDownloadScanner.h121
-rw-r--r--components/downloads/src/nsHelperAppDlg.js1138
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, &nothing, &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]);