summaryrefslogtreecommitdiff
path: root/components/downloads
diff options
context:
space:
mode:
authorMoonchild <moonchild@palemoon.org>2022-02-12 17:47:03 +0000
committerMatt A. Tobin <email@mattatobin.com>2022-02-12 14:23:18 -0600
commitf66babd8b8368ada3e5aa29cdef1c77291ee4ddd (patch)
treee3842e2a6bf19090185f9c475b3846e1bb79ac97 /components/downloads
downloadGRE-f66babd8b8368ada3e5aa29cdef1c77291ee4ddd.tar.gz
Create the Goanna Runtime Environment
Diffstat (limited to 'components/downloads')
-rw-r--r--components/downloads/content/DownloadProgressListener.js117
-rw-r--r--components/downloads/content/download.xml327
-rw-r--r--components/downloads/content/downloads.css50
-rw-r--r--components/downloads/content/downloads.js1314
-rw-r--r--components/downloads/content/downloads.xul154
-rw-r--r--components/downloads/content/unknownContentType.xul103
-rw-r--r--components/downloads/jar.mn12
-rw-r--r--components/downloads/locale/downloads.dtd52
-rw-r--r--components/downloads/locale/downloads.properties141
-rw-r--r--components/downloads/locale/settingsChange.dtd6
-rw-r--r--components/downloads/locale/unknownContentType.dtd26
-rw-r--r--components/downloads/locale/unknownContentType.properties19
-rw-r--r--components/downloads/moz.build57
-rw-r--r--components/downloads/nsDownloadManagerUI.manifest2
-rw-r--r--components/downloads/nsHelperAppDlg.manifest2
-rw-r--r--components/downloads/public/nsIDownload.idl175
-rw-r--r--components/downloads/public/nsIDownloadManager.idl358
-rw-r--r--components/downloads/public/nsIDownloadManagerUI.idl55
-rw-r--r--components/downloads/public/nsIDownloadProgressListener.idl60
-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
32 files changed, 10943 insertions, 0 deletions
diff --git a/components/downloads/content/DownloadProgressListener.js b/components/downloads/content/DownloadProgressListener.js
new file mode 100644
index 000000000..ab349baf2
--- /dev/null
+++ b/components/downloads/content/DownloadProgressListener.js
@@ -0,0 +1,117 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * DownloadProgressListener "class" is used to help update download items shown
+ * in the Download Manager UI such as displaying amount transferred, transfer
+ * rate, and time left for each download.
+ *
+ * This class implements the nsIDownloadProgressListener interface.
+ */
+function DownloadProgressListener() {}
+
+DownloadProgressListener.prototype = {
+ // nsISupports
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDownloadProgressListener]),
+
+ // nsIDownloadProgressListener
+
+ onDownloadStateChange: function dlPL_onDownloadStateChange(aState, aDownload)
+ {
+ // Update window title in-case we don't get all progress notifications
+ onUpdateProgress();
+
+ let dl;
+ let state = aDownload.state;
+ switch (state) {
+ case nsIDM.DOWNLOAD_QUEUED:
+ prependList(aDownload);
+ break;
+
+ case nsIDM.DOWNLOAD_BLOCKED_POLICY:
+ prependList(aDownload);
+ // Should fall through, this is a final state but DOWNLOAD_QUEUED
+ // is skipped. See nsDownloadManager::AddDownload.
+ case nsIDM.DOWNLOAD_FAILED:
+ case nsIDM.DOWNLOAD_CANCELED:
+ case nsIDM.DOWNLOAD_BLOCKED_PARENTAL:
+ case nsIDM.DOWNLOAD_DIRTY:
+ case nsIDM.DOWNLOAD_FINISHED:
+ downloadCompleted(aDownload);
+ if (state == nsIDM.DOWNLOAD_FINISHED)
+ autoRemoveAndClose(aDownload);
+ break;
+ case nsIDM.DOWNLOAD_DOWNLOADING: {
+ dl = getDownload(aDownload.id);
+
+ // At this point, we know if we are an indeterminate download or not
+ dl.setAttribute("progressmode", aDownload.percentComplete == -1 ?
+ "undetermined" : "normal");
+
+ // As well as knowing the referrer
+ let referrer = aDownload.referrer;
+ if (referrer)
+ dl.setAttribute("referrer", referrer.spec);
+
+ break;
+ }
+ }
+
+ // autoRemoveAndClose could have already closed our window...
+ try {
+ if (!dl)
+ dl = getDownload(aDownload.id);
+
+ // Update to the new state
+ dl.setAttribute("state", state);
+
+ // Update ui text values after switching states
+ updateTime(dl);
+ updateStatus(dl);
+ updateButtons(dl);
+ } catch (e) { }
+ },
+
+ onProgressChange: function dlPL_onProgressChange(aWebProgress, aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress, aDownload)
+ {
+ var download = getDownload(aDownload.id);
+
+ // Update this download's progressmeter
+ if (aDownload.percentComplete != -1) {
+ download.setAttribute("progress", aDownload.percentComplete);
+
+ // Dispatch ValueChange for a11y
+ let event = document.createEvent("Events");
+ event.initEvent("ValueChange", true, true);
+ document.getAnonymousElementByAttribute(download, "anonid", "progressmeter")
+ .dispatchEvent(event);
+ }
+
+ // Update the progress so the status can be correctly updated
+ download.setAttribute("currBytes", aDownload.amountTransferred);
+ download.setAttribute("maxBytes", aDownload.size);
+
+ // Update the rest of the UI (bytes transferred, bytes total, download rate,
+ // time remaining).
+ updateStatus(download, aDownload);
+
+ // Update window title
+ onUpdateProgress();
+ },
+
+ onStateChange: function(aWebProgress, aRequest, aState, aStatus, aDownload)
+ {
+ },
+
+ onSecurityChange: function(aWebProgress, aRequest, aState, aDownload)
+ {
+ }
+};
diff --git a/components/downloads/content/download.xml b/components/downloads/content/download.xml
new file mode 100644
index 000000000..1d4b87270
--- /dev/null
+++ b/components/downloads/content/download.xml
@@ -0,0 +1,327 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE bindings [
+ <!ENTITY % downloadDTD SYSTEM "chrome://mozapps/locale/downloads/downloads.dtd" >
+ %downloadDTD;
+]>
+
+<bindings id="downloadBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="download-base" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+ <resources>
+ <stylesheet src="chrome://mozapps/skin/downloads/downloads.css"/>
+ </resources>
+ <implementation>
+ <property name="paused">
+ <getter>
+ <![CDATA[
+ return parseInt(this.getAttribute("state")) == Components.interfaces.nsIDownloadManager.DOWNLOAD_PAUSED;
+ ]]>
+ </getter>
+ </property>
+ <property name="openable">
+ <getter>
+ <![CDATA[
+ return parseInt(this.getAttribute("state")) == Components.interfaces.nsIDownloadManager.DOWNLOAD_FINISHED;
+ ]]>
+ </getter>
+ </property>
+ <property name="inProgress">
+ <getter>
+ <![CDATA[
+ var state = parseInt(this.getAttribute("state"));
+ const dl = Components.interfaces.nsIDownloadManager;
+ return state == dl.DOWNLOAD_NOTSTARTED ||
+ state == dl.DOWNLOAD_QUEUED ||
+ state == dl.DOWNLOAD_DOWNLOADING ||
+ state == dl.DOWNLOAD_PAUSED ||
+ state == dl.DOWNLOAD_SCANNING;
+ ]]>
+ </getter>
+ </property>
+ <property name="removable">
+ <getter>
+ <![CDATA[
+ var state = parseInt(this.getAttribute("state"));
+ const dl = Components.interfaces.nsIDownloadManager;
+ return state == dl.DOWNLOAD_FINISHED ||
+ state == dl.DOWNLOAD_CANCELED ||
+ state == dl.DOWNLOAD_BLOCKED_PARENTAL ||
+ state == dl.DOWNLOAD_BLOCKED_POLICY ||
+ state == dl.DOWNLOAD_DIRTY ||
+ state == dl.DOWNLOAD_FAILED;
+ ]]>
+ </getter>
+ </property>
+ <property name="buttons">
+ <getter>
+ <![CDATA[
+ var startEl = document.getAnonymousNodes(this);
+ if (!startEl.length)
+ startEl = [this];
+
+ const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ return startEl[0].getElementsByTagNameNS(XULNS, "button");
+ ]]>
+ </getter>
+ </property>
+ </implementation>
+ </binding>
+
+ <binding id="download-starting" extends="chrome://mozapps/content/downloads/download.xml#download-base">
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox pack="center">
+ <xul:image class="downloadTypeIcon" validate="always"
+ xbl:inherits="src=image"/>
+ </xul:vbox>
+ <xul:vbox pack="start" flex="1">
+ <xul:label xbl:inherits="value=target,tooltiptext=target"
+ crop="center" class="name"/>
+ <xul:progressmeter mode="normal" value="0" flex="1"
+ anonid="progressmeter"/>
+ <xul:label value="&starting.label;" class="status"/>
+ <xul:spacer flex="1"/>
+ </xul:vbox>
+ <xul:vbox pack="center">
+ <xul:button class="cancel mini-button" tooltiptext="&cmd.cancel.label;"
+ cmd="cmd_cancel" ondblclick="event.stopPropagation();"
+ oncommand="performCommand('cmd_cancel', this);"/>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="download-downloading" extends="chrome://mozapps/content/downloads/download.xml#download-base">
+ <content>
+ <xul:hbox flex="1" class="downloadContentBox">
+ <xul:vbox pack="center">
+ <xul:image class="downloadTypeIcon" validate="always"
+ xbl:inherits="src=image"/>
+ </xul:vbox>
+ <xul:vbox flex="1">
+ <xul:label xbl:inherits="value=target,tooltiptext=target"
+ crop="center" flex="2" class="name"/>
+ <xul:hbox>
+ <xul:vbox flex="1">
+ <xul:progressmeter mode="normal" value="0" flex="1"
+ anonid="progressmeter"
+ xbl:inherits="value=progress,mode=progressmode"/>
+ </xul:vbox>
+ <xul:button class="pause mini-button" tooltiptext="&cmd.pause.label;"
+ cmd="cmd_pause" ondblclick="event.stopPropagation();"
+ oncommand="performCommand('cmd_pause', this);"/>
+ <xul:button class="cancel mini-button" tooltiptext="&cmd.cancel.label;"
+ cmd="cmd_cancel" ondblclick="event.stopPropagation();"
+ oncommand="performCommand('cmd_cancel', this);"/>
+ </xul:hbox>
+ <xul:label xbl:inherits="value=status,tooltiptext=statusTip" flex="1"
+ crop="right" class="status"/>
+ <xul:spacer flex="1"/>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="download-paused" extends="chrome://mozapps/content/downloads/download.xml#download-base">
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox pack="center">
+ <xul:image class="downloadTypeIcon" validate="always"
+ xbl:inherits="src=image"/>
+ </xul:vbox>
+ <xul:vbox flex="1">
+ <xul:label xbl:inherits="value=target,tooltiptext=target"
+ crop="center" flex="2" class="name"/>
+ <xul:hbox>
+ <xul:vbox flex="1">
+ <xul:progressmeter mode="normal" value="0" flex="1"
+ anonid="progressmeter"
+ xbl:inherits="value=progress,mode=progressmode"/>
+ </xul:vbox>
+ <xul:button class="resume mini-button" tooltiptext="&cmd.resume.label;"
+ cmd="cmd_resume" ondblclick="event.stopPropagation();"
+ oncommand="performCommand('cmd_resume', this);"/>
+ <xul:button class="cancel mini-button" tooltiptext="&cmd.cancel.label;"
+ cmd="cmd_cancel" ondblclick="event.stopPropagation();"
+ oncommand="performCommand('cmd_cancel', this);"/>
+ </xul:hbox>
+ <xul:label xbl:inherits="value=status,tooltiptext=statusTip" flex="1"
+ crop="right" class="status"/>
+ <xul:spacer flex="1"/>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="download-done" extends="chrome://mozapps/content/downloads/download.xml#download-base">
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox pack="center">
+ <xul:image class="downloadTypeIcon" validate="always"
+ xbl:inherits="src=image"/>
+ </xul:vbox>
+ <xul:vbox pack="start" flex="1">
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=target,tooltiptext=target"
+ crop="center" flex="1" class="name"/>
+ <xul:label xbl:inherits="value=dateTime,tooltiptext=dateTimeTip"
+ class="dateTime"/>
+ </xul:hbox>
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=status,tooltiptext=statusTip"
+ crop="end" flex="1" class="status"/>
+ </xul:hbox>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="download-canceled" extends="chrome://mozapps/content/downloads/download.xml#download-base">
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox pack="center">
+ <xul:image class="downloadTypeIcon" validate="always"
+ xbl:inherits="src=image"/>
+ </xul:vbox>
+ <xul:vbox pack="start" flex="1">
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=target,tooltiptext=target"
+ crop="center" flex="1" class="name"/>
+ <xul:label xbl:inherits="value=dateTime,tooltiptext=dateTimeTip"
+ class="dateTime"/>
+ </xul:hbox>
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=status,tooltiptext=statusTip"
+ crop="end" flex="1" class="status"/>
+ <xul:button class="retry mini-button" tooltiptext="&cmd.retry.label;"
+ cmd="cmd_retry" ondblclick="event.stopPropagation();"
+ oncommand="performCommand('cmd_retry', this);"/>
+ </xul:hbox>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="download-failed" extends="chrome://mozapps/content/downloads/download.xml#download-base">
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox pack="center">
+ <xul:image class="downloadTypeIcon" validate="always"
+ xbl:inherits="src=image"/>
+ </xul:vbox>
+ <xul:vbox pack="start" flex="1">
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=target,tooltiptext=target"
+ crop="center" flex="1" class="name"/>
+ <xul:label xbl:inherits="value=dateTime,tooltiptext=dateTimeTip"
+ class="dateTime"/>
+ </xul:hbox>
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=status,tooltiptext=statusTip"
+ crop="end" flex="1" class="status"/>
+ <xul:button class="retry mini-button" tooltiptext="&cmd.retry.label;"
+ cmd="cmd_retry" ondblclick="event.stopPropagation();"
+ oncommand="performCommand('cmd_retry', this);"/>
+ </xul:hbox>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="download-blocked-parental" extends="chrome://mozapps/content/downloads/download.xml#download-base">
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox pack="center">
+ <xul:image class="downloadTypeIcon blockedIcon"/>
+ </xul:vbox>
+ <xul:vbox pack="start" flex="1">
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=target,tooltiptext=target"
+ crop="center" flex="1" class="name"/>
+ <xul:label xbl:inherits="value=dateTime,tooltiptext=dateTimeTip"
+ class="dateTime"/>
+ </xul:hbox>
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=status,tooltiptext=statusTip"
+ crop="end" flex="1" class="status"/>
+ </xul:hbox>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="download-blocked-policy" extends="chrome://mozapps/content/downloads/download.xml#download-base">
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox pack="center">
+ <xul:image class="downloadTypeIcon blockedIcon"/>
+ </xul:vbox>
+ <xul:vbox pack="start" flex="1">
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=target,tooltiptext=target"
+ crop="center" flex="1" class="name"/>
+ <xul:label xbl:inherits="value=dateTime,tooltiptext=dateTimeTip"
+ class="dateTime"/>
+ </xul:hbox>
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=status,tooltiptext=statusTip"
+ crop="end" flex="1" class="status"/>
+ </xul:hbox>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="download-scanning" extends="chrome://mozapps/content/downloads/download.xml#download-base">
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox pack="center">
+ <xul:image class="downloadTypeIcon" validate="always"
+ xbl:inherits="src=image"/>
+ </xul:vbox>
+ <xul:vbox pack="start" flex="1">
+ <xul:label xbl:inherits="value=target,tooltiptext=target"
+ crop="center" flex="2" class="name"/>
+ <xul:hbox>
+ <xul:vbox flex="1">
+ <xul:progressmeter mode="undetermined" flex="1" />
+ </xul:vbox>
+ </xul:hbox>
+ <xul:label value="&scanning.label;" class="status"/>
+ <xul:spacer flex="1"/>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="download-dirty" extends="chrome://mozapps/content/downloads/download.xml#download-base">
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox pack="center">
+ <xul:image class="downloadTypeIcon blockedIcon"/>
+ </xul:vbox>
+ <xul:vbox pack="start" flex="1">
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=target,tooltiptext=target"
+ crop="center" flex="1" class="name"/>
+ <xul:label xbl:inherits="value=dateTime,tooltiptext=dateTimeTip"
+ class="dateTime"/>
+ </xul:hbox>
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=status,tooltiptext=statusTip"
+ crop="end" flex="1" class="status"/>
+ </xul:hbox>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ </binding>
+
+</bindings>
diff --git a/components/downloads/content/downloads.css b/components/downloads/content/downloads.css
new file mode 100644
index 000000000..dcb648d62
--- /dev/null
+++ b/components/downloads/content/downloads.css
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+richlistitem[type="download"] {
+ -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-starting');
+ -moz-box-orient: vertical;
+}
+
+richlistitem[type="download"][state="0"] {
+ -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-downloading');
+}
+
+richlistitem[type="download"][state="1"] {
+ -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-done');
+}
+
+richlistitem[type="download"][state="2"] {
+ -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-failed');
+}
+
+richlistitem[type="download"][state="3"] {
+ -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-canceled');
+}
+
+richlistitem[type="download"][state="4"] {
+ -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-paused');
+}
+
+richlistitem[type="download"][state="6"] {
+ -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-blocked-parental');
+}
+
+richlistitem[type="download"][state="7"] {
+ -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-scanning');
+}
+
+richlistitem[type="download"][state="8"] {
+ -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-dirty');
+}
+
+richlistitem[type="download"][state="9"] {
+ -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-blocked-policy');
+}
+
+/* Only focus buttons in the selected item*/
+richlistitem[type="download"]:not([selected="true"]) button {
+ -moz-user-focus: none;
+}
+
diff --git a/components/downloads/content/downloads.js b/components/downloads/content/downloads.js
new file mode 100644
index 000000000..299717336
--- /dev/null
+++ b/components/downloads/content/downloads.js
@@ -0,0 +1,1314 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Globals
+
+const PREF_BDM_CLOSEWHENDONE = "browser.download.manager.closeWhenDone";
+const PREF_BDM_CONFIRMOPENEXE = "browser.download.confirmOpenExecutable";
+const PREF_BDM_SCANWHENDONE = "browser.download.manager.scanWhenDone";
+
+const nsLocalFile = Components.Constructor("@mozilla.org/file/local;1",
+ "nsILocalFile", "initWithPath");
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/DownloadUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+ "resource://gre/modules/PluralForm.jsm");
+
+const nsIDM = Ci.nsIDownloadManager;
+
+var gDownloadManager = Cc["@mozilla.org/download-manager;1"].getService(nsIDM);
+var gDownloadManagerUI = Cc["@mozilla.org/download-manager-ui;1"].
+ getService(Ci.nsIDownloadManagerUI);
+
+var gDownloadListener = null;
+var gDownloadsView = null;
+var gSearchBox = null;
+var gSearchTerms = [];
+var gBuilder = 0;
+
+// This variable is used when performing commands on download items and gives
+// the command the ability to do something after all items have been operated
+// on. The following convention is used to handle the value of the variable:
+// whenever we aren't performing a command, the value is |undefined|; just
+// before executing commands, the value will be set to |null|; and when
+// commands want to create a callback, they set the value to be a callback
+// function to be executed after all download items have been visited.
+var gPerformAllCallback;
+
+// Control the performance of the incremental list building by setting how many
+// milliseconds to wait before building more of the list and how many items to
+// add between each delay.
+const gListBuildDelay = 300;
+const gListBuildChunk = 3;
+
+// Array of download richlistitem attributes to check when searching
+const gSearchAttributes = [
+ "target",
+ "status",
+ "dateTime",
+];
+
+// If the user has interacted with the window in a significant way, we should
+// not auto-close the window. Tough UI decisions about what is "significant."
+var gUserInteracted = false;
+
+// These strings will be converted to the corresponding ones from the string
+// bundle on startup.
+var gStr = {
+ paused: "paused",
+ cannotPause: "cannotPause",
+ doneStatus: "doneStatus",
+ doneSize: "doneSize",
+ doneSizeUnknown: "doneSizeUnknown",
+ stateFailed: "stateFailed",
+ stateCanceled: "stateCanceled",
+ stateBlockedParentalControls: "stateBlocked",
+ stateBlockedPolicy: "stateBlockedPolicy",
+ stateDirty: "stateDirty",
+ downloadsTitleFiles: "downloadsTitleFiles",
+ downloadsTitlePercent: "downloadsTitlePercent",
+ fileExecutableSecurityWarningTitle: "fileExecutableSecurityWarningTitle",
+};
+
+// The statement to query for downloads that are active or match the search
+var gStmt = null;
+
+// Start/Stop Observers
+
+function downloadCompleted(aDownload)
+{
+ // The download is changing state, so update the clear list button
+ updateClearListButton();
+
+ // Wrap this in try...catch since this can be called while shutting down...
+ // it doesn't really matter if it fails then since well.. we're shutting down
+ // and there's no UI to update!
+ try {
+ let dl = getDownload(aDownload.id);
+
+ // Update attributes now that we've finished
+ dl.setAttribute("startTime", Math.round(aDownload.startTime / 1000));
+ dl.setAttribute("endTime", Date.now());
+ dl.setAttribute("currBytes", aDownload.amountTransferred);
+ dl.setAttribute("maxBytes", aDownload.size);
+
+ // Move the download below active if it should stay in the list
+ if (downloadMatchesSearch(dl)) {
+ // Iterate down until we find a non-active download
+ let next = dl.nextSibling;
+ while (next && next.inProgress)
+ next = next.nextSibling;
+
+ // Move the item
+ gDownloadsView.insertBefore(dl, next);
+ } else {
+ removeFromView(dl);
+ }
+
+ // getTypeFromFile fails if it can't find a type for this file.
+ try {
+ // Refresh the icon, so that executable icons are shown.
+ var mimeService = Cc["@mozilla.org/mime;1"].
+ getService(Ci.nsIMIMEService);
+ var contentType = mimeService.getTypeFromFile(aDownload.targetFile);
+
+ var listItem = getDownload(aDownload.id)
+ var oldImage = listItem.getAttribute("image");
+ // Tacking on contentType bypasses cache
+ listItem.setAttribute("image", oldImage + "&contentType=" + contentType);
+ } catch (e) { }
+
+ if (gDownloadManager.activeDownloadCount == 0)
+ document.title = document.documentElement.getAttribute("statictitle");
+
+ gDownloadManagerUI.getAttention();
+ }
+ catch (e) { }
+}
+
+function autoRemoveAndClose(aDownload)
+{
+ var pref = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+
+ if (gDownloadManager.activeDownloadCount == 0) {
+ // For the moment, just use the simple heuristic that if this window was
+ // opened by the download process, rather than by the user, it should
+ // auto-close if the pref is set that way. If the user opened it themselves,
+ // it should not close until they explicitly close it. Additionally, the
+ // preference to control the feature may not be set, so defaulting to
+ // keeping the window open.
+ let autoClose = false;
+ try {
+ autoClose = pref.getBoolPref(PREF_BDM_CLOSEWHENDONE);
+ } catch (e) { }
+ var autoOpened =
+ !window.opener || window.opener.location.href == window.location.href;
+ if (autoClose && autoOpened && !gUserInteracted) {
+ gCloseDownloadManager();
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// This function can be overwritten by extensions that wish to place the
+// Download Window in another part of the UI.
+function gCloseDownloadManager()
+{
+ window.close();
+}
+
+// Download Event Handlers
+
+function cancelDownload(aDownload)
+{
+ gDownloadManager.cancelDownload(aDownload.getAttribute("dlid"));
+
+ // XXXben -
+ // If we got here because we resumed the download, we weren't using a temp file
+ // because we used saveURL instead. (this is because the proper download mechanism
+ // employed by the helper app service isn't fully accessible yet... should be fixed...
+ // talk to bz...)
+ // the upshot is we have to delete the file if it exists.
+ var f = getLocalFileFromNativePathOrUrl(aDownload.getAttribute("file"));
+
+ if (f.exists())
+ f.remove(false);
+}
+
+function pauseDownload(aDownload)
+{
+ var id = aDownload.getAttribute("dlid");
+ gDownloadManager.pauseDownload(id);
+}
+
+function resumeDownload(aDownload)
+{
+ gDownloadManager.resumeDownload(aDownload.getAttribute("dlid"));
+}
+
+function removeDownload(aDownload)
+{
+ gDownloadManager.removeDownload(aDownload.getAttribute("dlid"));
+}
+
+function retryDownload(aDownload)
+{
+ removeFromView(aDownload);
+ gDownloadManager.retryDownload(aDownload.getAttribute("dlid"));
+}
+
+function showDownload(aDownload)
+{
+ var f = getLocalFileFromNativePathOrUrl(aDownload.getAttribute("file"));
+
+ try {
+ // Show the directory containing the file and select the file
+ f.reveal();
+ } catch (e) {
+ // If reveal fails for some reason (e.g., it's not implemented on unix or
+ // the file doesn't exist), try using the parent if we have it.
+ let parent = f.parent.QueryInterface(Ci.nsILocalFile);
+ if (!parent)
+ return;
+
+ try {
+ // "Double click" the parent directory to show where the file should be
+ parent.launch();
+ } catch (e) {
+ // If launch also fails (probably because it's not implemented), let the
+ // OS handler try to open the parent
+ openExternal(parent);
+ }
+ }
+}
+
+function onDownloadDblClick(aEvent)
+{
+ // Only do the default action for double primary clicks
+ if (aEvent.button == 0 && aEvent.target.selected)
+ doDefaultForSelected();
+}
+
+function openDownload(aDownload)
+{
+ var f = getLocalFileFromNativePathOrUrl(aDownload.getAttribute("file"));
+ if (f.isExecutable()) {
+ var dontAsk = false;
+ var pref = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+ try {
+ dontAsk = !pref.getBoolPref(PREF_BDM_CONFIRMOPENEXE);
+ } catch (e) { }
+
+#ifdef XP_WIN
+ // On Vista and above, we rely on native security prompting for
+ // downloaded content unless it's disabled.
+ try {
+ var sysInfo = Cc["@mozilla.org/system-info;1"].
+ getService(Ci.nsIPropertyBag2);
+ if (parseFloat(sysInfo.getProperty("version")) >= 6 &&
+ pref.getBoolPref(PREF_BDM_SCANWHENDONE)) {
+ dontAsk = true;
+ }
+ } catch (ex) { }
+#endif
+
+ if (!dontAsk) {
+ var strings = document.getElementById("downloadStrings");
+ var name = aDownload.getAttribute("target");
+ var message = strings.getFormattedString("fileExecutableSecurityWarning", [name, name]);
+
+ let title = gStr.fileExecutableSecurityWarningTitle;
+
+ var promptSvc = Cc["@mozilla.org/embedcomp/prompt-service;1"].
+ getService(Ci.nsIPromptService);
+ var open = promptSvc.confirm(window, title, message);
+
+ if (!open)
+ return;
+ }
+ }
+ try {
+ try {
+ let download = gDownloadManager.getDownload(aDownload.getAttribute("dlid"));
+ let mimeInfo = download.MIMEInfo;
+ if (mimeInfo.preferredAction == mimeInfo.useHelperApp) {
+ mimeInfo.launchWithFile(f);
+ return;
+ }
+ } catch (ex) {
+ }
+ f.launch();
+ } catch (ex) {
+ // if launch fails, try sending it through the system's external
+ // file: URL handler
+ openExternal(f);
+ }
+}
+
+function openReferrer(aDownload)
+{
+ openURL(getReferrerOrSource(aDownload));
+}
+
+function copySourceLocation(aDownload)
+{
+ var uri = aDownload.getAttribute("uri");
+ var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper);
+
+ // Check if we should initialize a callback
+ if (gPerformAllCallback === null) {
+ let uris = [];
+ gPerformAllCallback = aURI => aURI ? uris.push(aURI) :
+ clipboard.copyString(uris.join("\n"));
+ }
+
+ // We have a callback to use, so use it to add a uri
+ if (typeof gPerformAllCallback == "function")
+ gPerformAllCallback(uri);
+ else {
+ // It's a plain copy source, so copy it
+ clipboard.copyString(uri);
+ }
+}
+
+/**
+ * Remove the currently shown downloads from the download list.
+ */
+function clearDownloadList() {
+ // Clear the whole list if there's no search
+ if (gSearchTerms == "") {
+ gDownloadManager.cleanUp();
+ return;
+ }
+
+ // Remove each download starting from the end until we hit a download
+ // that is in progress
+ let item;
+ while ((item = gDownloadsView.lastChild) && !item.inProgress)
+ removeDownload(item);
+
+ // Clear the input as if the user did it and move focus to the list
+ gSearchBox.value = "";
+ gSearchBox.doCommand();
+ gDownloadsView.focus();
+}
+
+// This is called by the progress listener.
+var gLastComputedMean = -1;
+var gLastActiveDownloads = 0;
+function onUpdateProgress()
+{
+ let numActiveDownloads = gDownloadManager.activeDownloadCount;
+
+ // Use the default title and reset "last" values if there's no downloads
+ if (numActiveDownloads == 0) {
+ document.title = document.documentElement.getAttribute("statictitle");
+ gLastComputedMean = -1;
+ gLastActiveDownloads = 0;
+
+ return;
+ }
+
+ // Establish the mean transfer speed and amount downloaded.
+ var mean = 0;
+ var base = 0;
+ var dls = gDownloadManager.activeDownloads;
+ while (dls.hasMoreElements()) {
+ let dl = dls.getNext();
+ if (dl.percentComplete < 100 && dl.size > 0) {
+ mean += dl.amountTransferred;
+ base += dl.size;
+ }
+ }
+
+ // Calculate the percent transferred, unless we don't have a total file size
+ let title = gStr.downloadsTitlePercent;
+ if (base == 0)
+ title = gStr.downloadsTitleFiles;
+ else
+ mean = Math.floor((mean / base) * 100);
+
+ // Update title of window
+ if (mean != gLastComputedMean || gLastActiveDownloads != numActiveDownloads) {
+ gLastComputedMean = mean;
+ gLastActiveDownloads = numActiveDownloads;
+
+ // Get the correct plural form and insert number of downloads and percent
+ title = PluralForm.get(numActiveDownloads, title);
+ title = replaceInsert(title, 1, numActiveDownloads);
+ title = replaceInsert(title, 2, mean);
+
+ document.title = title;
+ }
+}
+
+// Startup, Shutdown
+
+function Startup()
+{
+ gDownloadsView = document.getElementById("downloadView");
+ gSearchBox = document.getElementById("searchbox");
+
+ // convert strings to those in the string bundle
+ let sb = document.getElementById("downloadStrings");
+ let getStr = string => sb.getString(string);
+ for (let [name, value] of Object.entries(gStr))
+ gStr[name] = typeof value == "string" ? getStr(value) : value.map(getStr);
+
+ initStatement();
+ buildDownloadList(true);
+
+ // The DownloadProgressListener (DownloadProgressListener.js) handles progress
+ // notifications.
+ gDownloadListener = new DownloadProgressListener();
+ gDownloadManager.addListener(gDownloadListener);
+
+ // If the UI was displayed because the user interacted, we need to make sure
+ // we update gUserInteracted accordingly.
+ if (window.arguments[1] == Ci.nsIDownloadManagerUI.REASON_USER_INTERACTED)
+ gUserInteracted = true;
+
+ // downloads can finish before Startup() does, so check if the window should
+ // close and act accordingly
+ if (!autoRemoveAndClose())
+ gDownloadsView.focus();
+
+ let obs = Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+ obs.addObserver(gDownloadObserver, "download-manager-remove-download", false);
+ obs.addObserver(gDownloadObserver, "browser-lastwindow-close-granted", false);
+
+ // Clear the search box and move focus to the list on escape from the box
+ gSearchBox.addEventListener("keypress", function(e) {
+ if (e.keyCode == e.DOM_VK_ESCAPE) {
+ // Move focus to the list instead of closing the window
+ gDownloadsView.focus();
+ e.preventDefault();
+ }
+ }, false);
+
+ let DownloadTaskbarProgress =
+ Cu.import("resource://gre/modules/DownloadTaskbarProgress.jsm", {}).DownloadTaskbarProgress;
+ DownloadTaskbarProgress.onDownloadWindowLoad(window);
+}
+
+function Shutdown()
+{
+ gDownloadManager.removeListener(gDownloadListener);
+
+ let obs = Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+ obs.removeObserver(gDownloadObserver, "download-manager-remove-download");
+ obs.removeObserver(gDownloadObserver, "browser-lastwindow-close-granted");
+
+ clearTimeout(gBuilder);
+ gStmt.reset();
+ gStmt.finalize();
+}
+
+var gDownloadObserver = {
+ observe: function gdo_observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "download-manager-remove-download":
+ // A null subject here indicates "remove multiple", so we just rebuild.
+ if (!aSubject) {
+ // Rebuild the default view
+ buildDownloadList(true);
+ break;
+ }
+
+ // Otherwise, remove a single download
+ let id = aSubject.QueryInterface(Ci.nsISupportsPRUint32);
+ let dl = getDownload(id.data);
+ removeFromView(dl);
+ break;
+ case "browser-lastwindow-close-granted":
+ if (gDownloadManager.activeDownloadCount == 0) {
+ setTimeout(gCloseDownloadManager, 0);
+ }
+ break;
+ }
+ }
+};
+
+// View Context Menus
+
+var gContextMenus = [
+ // DOWNLOAD_DOWNLOADING
+ [
+ "menuitem_pause"
+ , "menuitem_cancel"
+ , "menuseparator"
+ , "menuitem_show"
+ , "menuseparator"
+ , "menuitem_openReferrer"
+ , "menuitem_copyLocation"
+ , "menuseparator"
+ , "menuitem_selectAll"
+ ],
+ // DOWNLOAD_FINISHED
+ [
+ "menuitem_open"
+ , "menuitem_show"
+ , "menuseparator"
+ , "menuitem_openReferrer"
+ , "menuitem_copyLocation"
+ , "menuseparator"
+ , "menuitem_selectAll"
+ , "menuseparator"
+ , "menuitem_removeFromList"
+ ],
+ // DOWNLOAD_FAILED
+ [
+ "menuitem_retry"
+ , "menuseparator"
+ , "menuitem_openReferrer"
+ , "menuitem_copyLocation"
+ , "menuseparator"
+ , "menuitem_selectAll"
+ , "menuseparator"
+ , "menuitem_removeFromList"
+ ],
+ // DOWNLOAD_CANCELED
+ [
+ "menuitem_retry"
+ , "menuseparator"
+ , "menuitem_openReferrer"
+ , "menuitem_copyLocation"
+ , "menuseparator"
+ , "menuitem_selectAll"
+ , "menuseparator"
+ , "menuitem_removeFromList"
+ ],
+ // DOWNLOAD_PAUSED
+ [
+ "menuitem_resume"
+ , "menuitem_cancel"
+ , "menuseparator"
+ , "menuitem_show"
+ , "menuseparator"
+ , "menuitem_openReferrer"
+ , "menuitem_copyLocation"
+ , "menuseparator"
+ , "menuitem_selectAll"
+ ],
+ // DOWNLOAD_QUEUED
+ [
+ "menuitem_cancel"
+ , "menuseparator"
+ , "menuitem_show"
+ , "menuseparator"
+ , "menuitem_openReferrer"
+ , "menuitem_copyLocation"
+ , "menuseparator"
+ , "menuitem_selectAll"
+ ],
+ // DOWNLOAD_BLOCKED_PARENTAL
+ [
+ "menuitem_openReferrer"
+ , "menuitem_copyLocation"
+ , "menuseparator"
+ , "menuitem_selectAll"
+ , "menuseparator"
+ , "menuitem_removeFromList"
+ ],
+ // DOWNLOAD_SCANNING
+ [
+ "menuitem_show"
+ , "menuseparator"
+ , "menuitem_openReferrer"
+ , "menuitem_copyLocation"
+ , "menuseparator"
+ , "menuitem_selectAll"
+ ],
+ // DOWNLOAD_DIRTY
+ [
+ "menuitem_openReferrer"
+ , "menuitem_copyLocation"
+ , "menuseparator"
+ , "menuitem_selectAll"
+ , "menuseparator"
+ , "menuitem_removeFromList"
+ ],
+ // DOWNLOAD_BLOCKED_POLICY
+ [
+ "menuitem_openReferrer"
+ , "menuitem_copyLocation"
+ , "menuseparator"
+ , "menuitem_selectAll"
+ , "menuseparator"
+ , "menuitem_removeFromList"
+ ]
+];
+
+function buildContextMenu(aEvent)
+{
+ if (aEvent.target.id != "downloadContextMenu")
+ return false;
+
+ var popup = document.getElementById("downloadContextMenu");
+ while (popup.hasChildNodes())
+ popup.removeChild(popup.firstChild);
+
+ if (gDownloadsView.selectedItem) {
+ let dl = gDownloadsView.selectedItem;
+ let idx = parseInt(dl.getAttribute("state"));
+ if (idx < 0)
+ idx = 0;
+
+ var menus = gContextMenus[idx];
+ for (let i = 0; i < menus.length; ++i) {
+ let menuitem = document.getElementById(menus[i]).cloneNode(true);
+ let cmd = menuitem.getAttribute("cmd");
+ if (cmd)
+ menuitem.disabled = !gDownloadViewController.isCommandEnabled(cmd, dl);
+
+ popup.appendChild(menuitem);
+ }
+
+ return true;
+ }
+
+ return false;
+}
+// Drag and Drop
+var gDownloadDNDObserver =
+{
+ onDragStart: function (aEvent)
+ {
+ if (!gDownloadsView.selectedItem)
+ return;
+ var dl = gDownloadsView.selectedItem;
+ var f = getLocalFileFromNativePathOrUrl(dl.getAttribute("file"));
+ if (!f.exists())
+ return;
+
+ var dt = aEvent.dataTransfer;
+ dt.mozSetDataAt("application/x-moz-file", f, 0);
+ var url = Services.io.newFileURI(f).spec;
+ dt.setData("text/uri-list", url);
+ dt.setData("text/plain", url);
+ dt.effectAllowed = "copyMove";
+ dt.addElement(dl);
+ },
+
+ onDragOver: function (aEvent)
+ {
+ var types = aEvent.dataTransfer.types;
+ if (types.includes("text/uri-list") ||
+ types.includes("text/x-moz-url") ||
+ types.includes("text/plain"))
+ aEvent.preventDefault();
+ },
+
+ onDrop: function(aEvent)
+ {
+ var dt = aEvent.dataTransfer;
+ // If dragged item is from our source, do not try to
+ // redownload already downloaded file.
+ if (dt.mozGetDataAt("application/x-moz-file", 0))
+ return;
+
+ var url = dt.getData("URL");
+ var name;
+ if (!url) {
+ url = dt.getData("text/x-moz-url") || dt.getData("text/plain");
+ [url, name] = url.split("\n");
+ }
+ if (url) {
+ let sourceDoc = dt.mozSourceNode ? dt.mozSourceNode.ownerDocument : document;
+ saveURL(url, name ? name : url, null, true, true, null, sourceDoc);
+ }
+ }
+}
+
+function pasteHandler() {
+ let trans = Cc["@mozilla.org/widget/transferable;1"].
+ createInstance(Ci.nsITransferable);
+ trans.init(null);
+ let flavors = ["text/x-moz-url", "text/unicode"];
+ flavors.forEach(trans.addDataFlavor);
+
+ Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard);
+
+ // Getting the data or creating the nsIURI might fail
+ try {
+ let data = {};
+ trans.getAnyTransferData({}, data, {});
+ let [url, name] = data.value.QueryInterface(Ci.nsISupportsString).data.split("\n");
+
+ if (!url)
+ return;
+
+ let uri = Services.io.newURI(url, null, null);
+
+ saveURL(uri.spec, name || uri.spec, null, true, true, null, document);
+ } catch (ex) {}
+}
+
+// Command Updating and Command Handlers
+
+var gDownloadViewController = {
+ isCommandEnabled: function(aCommand, aItem)
+ {
+ let dl = aItem;
+ let download = null; // used for getting an nsIDownload object
+
+ switch (aCommand) {
+ case "cmd_cancel":
+ return dl.inProgress;
+ case "cmd_open": {
+ let file = getLocalFileFromNativePathOrUrl(dl.getAttribute("file"));
+ return dl.openable && file.exists();
+ }
+ case "cmd_show": {
+ let file = getLocalFileFromNativePathOrUrl(dl.getAttribute("file"));
+ return file.exists();
+ }
+ case "cmd_pause":
+ download = gDownloadManager.getDownload(dl.getAttribute("dlid"));
+ return dl.inProgress && !dl.paused && download.resumable;
+ case "cmd_pauseResume":
+ download = gDownloadManager.getDownload(dl.getAttribute("dlid"));
+ return (dl.inProgress || dl.paused) && download.resumable;
+ case "cmd_resume":
+ download = gDownloadManager.getDownload(dl.getAttribute("dlid"));
+ return dl.paused && download.resumable;
+ case "cmd_openReferrer":
+ return dl.hasAttribute("referrer");
+ case "cmd_removeFromList":
+ case "cmd_retry":
+ return dl.removable;
+ case "cmd_copyLocation":
+ return true;
+ }
+ return false;
+ },
+
+ doCommand: function(aCommand, aItem)
+ {
+ if (this.isCommandEnabled(aCommand, aItem))
+ this.commands[aCommand](aItem);
+ },
+
+ commands: {
+ cmd_cancel: function(aSelectedItem) {
+ cancelDownload(aSelectedItem);
+ },
+ cmd_open: function(aSelectedItem) {
+ openDownload(aSelectedItem);
+ },
+ cmd_openReferrer: function(aSelectedItem) {
+ openReferrer(aSelectedItem);
+ },
+ cmd_pause: function(aSelectedItem) {
+ pauseDownload(aSelectedItem);
+ },
+ cmd_pauseResume: function(aSelectedItem) {
+ if (aSelectedItem.paused)
+ this.cmd_resume(aSelectedItem);
+ else
+ this.cmd_pause(aSelectedItem);
+ },
+ cmd_removeFromList: function(aSelectedItem) {
+ removeDownload(aSelectedItem);
+ },
+ cmd_resume: function(aSelectedItem) {
+ resumeDownload(aSelectedItem);
+ },
+ cmd_retry: function(aSelectedItem) {
+ retryDownload(aSelectedItem);
+ },
+ cmd_show: function(aSelectedItem) {
+ showDownload(aSelectedItem);
+ },
+ cmd_copyLocation: function(aSelectedItem) {
+ copySourceLocation(aSelectedItem);
+ },
+ }
+};
+
+/**
+ * Helper function to do commands.
+ *
+ * @param aCmd
+ * The command to be performed.
+ * @param aItem
+ * The richlistitem that represents the download that will have the
+ * command performed on it. If this is null, the command is performed on
+ * all downloads. If the item passed in is not a richlistitem that
+ * represents a download, it will walk up the parent nodes until it finds
+ * a DOM node that is.
+ */
+function performCommand(aCmd, aItem)
+{
+ let elm = aItem;
+ if (!elm) {
+ // If we don't have a desired download item, do the command for all
+ // selected items. Initialize the callback to null so commands know to add
+ // a callback if they want. We will call the callback with empty arguments
+ // after performing the command on each selected download item.
+ gPerformAllCallback = null;
+
+ // Convert the nodelist into an array to keep a copy of the download items
+ let items = [];
+ for (let i = gDownloadsView.selectedItems.length; --i >= 0; )
+ items.unshift(gDownloadsView.selectedItems[i]);
+
+ // Do the command for each download item
+ for (let item of items)
+ performCommand(aCmd, item);
+
+ // Call the callback with no arguments and reset because we're done
+ if (typeof gPerformAllCallback == "function")
+ gPerformAllCallback();
+ gPerformAllCallback = undefined;
+
+ return;
+ }
+ while (elm.nodeName != "richlistitem" ||
+ elm.getAttribute("type") != "download") {
+ elm = elm.parentNode;
+ }
+
+ gDownloadViewController.doCommand(aCmd, elm);
+}
+
+function setSearchboxFocus()
+{
+ gSearchBox.focus();
+ gSearchBox.select();
+}
+
+function openExternal(aFile)
+{
+ var uri = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService).newFileURI(aFile);
+
+ var protocolSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"].
+ getService(Ci.nsIExternalProtocolService);
+ protocolSvc.loadUrl(uri);
+
+ return;
+}
+
+// Utility Functions
+
+/**
+ * Create a download richlistitem with the provided attributes. Some attributes
+ * are *required* while optional ones will only be set on the item if provided.
+ *
+ * @param aAttrs
+ * An object that must have the following properties: dlid, file,
+ * target, uri, state, progress, startTime, endTime, currBytes,
+ * maxBytes; optional properties: referrer
+ * @return An initialized download richlistitem
+ */
+function createDownloadItem(aAttrs)
+{
+ let dl = document.createElement("richlistitem");
+
+ // Copy the attributes from the argument into the item
+ for (let attr in aAttrs)
+ dl.setAttribute(attr, aAttrs[attr]);
+
+ // Initialize other attributes
+ dl.setAttribute("type", "download");
+ dl.setAttribute("id", "dl" + aAttrs.dlid);
+ dl.setAttribute("image", "moz-icon://" + aAttrs.file + "?size=32");
+ dl.setAttribute("lastSeconds", Infinity);
+
+ // Initialize more complex attributes
+ updateTime(dl);
+ updateStatus(dl);
+
+ try {
+ let file = getLocalFileFromNativePathOrUrl(aAttrs.file);
+ dl.setAttribute("path", file.nativePath || file.path);
+ return dl;
+ } catch (e) {
+ // aFile might not be a file: url or a valid native path
+ // see bug #392386 for details
+ }
+ return null;
+}
+
+/**
+ * Updates the disabled state of the buttons of a downlaod.
+ *
+ * @param aItem
+ * The richlistitem representing the download.
+ */
+function updateButtons(aItem)
+{
+ let buttons = aItem.buttons;
+
+ for (let i = 0; i < buttons.length; ++i) {
+ let cmd = buttons[i].getAttribute("cmd");
+ let enabled = gDownloadViewController.isCommandEnabled(cmd, aItem);
+ buttons[i].disabled = !enabled;
+
+ if ("cmd_pause" == cmd && !enabled) {
+ // We need to add the tooltip indicating that the download cannot be
+ // paused now.
+ buttons[i].setAttribute("tooltiptext", gStr.cannotPause);
+ }
+ }
+}
+
+/**
+ * Updates the status for a download item depending on its state
+ *
+ * @param aItem
+ * The richlistitem that has various download attributes.
+ * @param aDownload
+ * The nsDownload from the backend. This is an optional parameter, but
+ * is useful for certain states such as DOWNLOADING.
+ */
+function updateStatus(aItem, aDownload) {
+ let status = "";
+ let statusTip = "";
+
+ let state = Number(aItem.getAttribute("state"));
+ switch (state) {
+ case nsIDM.DOWNLOAD_PAUSED:
+ {
+ let currBytes = Number(aItem.getAttribute("currBytes"));
+ let maxBytes = Number(aItem.getAttribute("maxBytes"));
+
+ let transfer = DownloadUtils.getTransferTotal(currBytes, maxBytes);
+ status = replaceInsert(gStr.paused, 1, transfer);
+
+ break;
+ }
+ case nsIDM.DOWNLOAD_DOWNLOADING:
+ {
+ let currBytes = Number(aItem.getAttribute("currBytes"));
+ let maxBytes = Number(aItem.getAttribute("maxBytes"));
+ // If we don't have an active download, assume 0 bytes/sec
+ let speed = aDownload ? aDownload.speed : 0;
+ let lastSec = Number(aItem.getAttribute("lastSeconds"));
+
+ let newLast;
+ [status, newLast] =
+ DownloadUtils.getDownloadStatus(currBytes, maxBytes, speed, lastSec);
+
+ // Update lastSeconds to be the new value
+ aItem.setAttribute("lastSeconds", newLast);
+
+ break;
+ }
+ case nsIDM.DOWNLOAD_FINISHED:
+ case nsIDM.DOWNLOAD_FAILED:
+ case nsIDM.DOWNLOAD_CANCELED:
+ case nsIDM.DOWNLOAD_BLOCKED_PARENTAL:
+ case nsIDM.DOWNLOAD_BLOCKED_POLICY:
+ case nsIDM.DOWNLOAD_DIRTY:
+ {
+ let stateSize = {};
+ stateSize[nsIDM.DOWNLOAD_FINISHED] = function() {
+ // Display the file size, but show "Unknown" for negative sizes
+ let fileSize = Number(aItem.getAttribute("maxBytes"));
+ let sizeText = gStr.doneSizeUnknown;
+ if (fileSize >= 0) {
+ let [size, unit] = DownloadUtils.convertByteUnits(fileSize);
+ sizeText = replaceInsert(gStr.doneSize, 1, size);
+ sizeText = replaceInsert(sizeText, 2, unit);
+ }
+ return sizeText;
+ };
+ stateSize[nsIDM.DOWNLOAD_FAILED] = () => gStr.stateFailed;
+ stateSize[nsIDM.DOWNLOAD_CANCELED] = () => gStr.stateCanceled;
+ stateSize[nsIDM.DOWNLOAD_BLOCKED_PARENTAL] = () => gStr.stateBlockedParentalControls;
+ stateSize[nsIDM.DOWNLOAD_BLOCKED_POLICY] = () => gStr.stateBlockedPolicy;
+ stateSize[nsIDM.DOWNLOAD_DIRTY] = () => gStr.stateDirty;
+
+ // Insert 1 is the download size or download state
+ status = replaceInsert(gStr.doneStatus, 1, stateSize[state]());
+
+ let [displayHost, fullHost] =
+ DownloadUtils.getURIHost(getReferrerOrSource(aItem));
+
+ // Insert 2 is the eTLD + 1 or other variations of the host
+ status = replaceInsert(status, 2, displayHost);
+ // Set the tooltip to be the full host
+ statusTip = fullHost;
+
+ break;
+ }
+ }
+
+ aItem.setAttribute("status", status);
+ aItem.setAttribute("statusTip", statusTip != "" ? statusTip : status);
+}
+
+/**
+ * Updates the time that gets shown for completed download items
+ *
+ * @param aItem
+ * The richlistitem representing a download in the UI
+ */
+function updateTime(aItem)
+{
+ // Don't bother updating for things that aren't finished
+ if (aItem.inProgress)
+ return;
+
+ let end = new Date(parseInt(aItem.getAttribute("endTime")));
+ let [dateCompact, dateComplete] = DownloadUtils.getReadableDates(end);
+ aItem.setAttribute("dateTime", dateCompact);
+ aItem.setAttribute("dateTimeTip", dateComplete);
+}
+
+/**
+ * Helper function to replace a placeholder string with a real string
+ *
+ * @param aText
+ * Source text containing placeholder (e.g., #1)
+ * @param aIndex
+ * Index number of placeholder to replace
+ * @param aValue
+ * New string to put in place of placeholder
+ * @return The string with placeholder replaced with the new string
+ */
+function replaceInsert(aText, aIndex, aValue)
+{
+ return aText.replace("#" + aIndex, aValue);
+}
+
+/**
+ * Perform the default action for the currently selected download item
+ */
+function doDefaultForSelected()
+{
+ // Make sure we have something selected
+ let item = gDownloadsView.selectedItem;
+ if (!item)
+ return;
+
+ // Get the default action (first item in the menu)
+ let state = Number(item.getAttribute("state"));
+ let menuitem = document.getElementById(gContextMenus[state][0]);
+
+ // Try to do the action if the command is enabled
+ gDownloadViewController.doCommand(menuitem.getAttribute("cmd"), item);
+}
+
+function removeFromView(aDownload)
+{
+ // Make sure we have an item to remove
+ if (!aDownload) return;
+
+ let index = gDownloadsView.selectedIndex;
+ gDownloadsView.removeChild(aDownload);
+ gDownloadsView.selectedIndex = Math.min(index, gDownloadsView.itemCount - 1);
+
+ // We might have removed the last item, so update the clear list button
+ updateClearListButton();
+}
+
+function getReferrerOrSource(aDownload)
+{
+ // Give the referrer if we have it set
+ if (aDownload.hasAttribute("referrer"))
+ return aDownload.getAttribute("referrer");
+
+ // Otherwise, provide the source
+ return aDownload.getAttribute("uri");
+}
+
+/**
+ * Initiate building the download list to have the active downloads followed by
+ * completed ones filtered by the search term if necessary.
+ *
+ * @param aForceBuild
+ * Force the list to be built even if the search terms don't change
+ */
+function buildDownloadList(aForceBuild)
+{
+ // Stringify the previous search
+ let prevSearch = gSearchTerms.join(" ");
+
+ // Array of space-separated lower-case search terms
+ gSearchTerms = gSearchBox.value.replace(/^\s+|\s+$/g, "").
+ toLowerCase().split(/\s+/);
+
+ // Unless forced, don't rebuild the download list if the search didn't change
+ if (!aForceBuild && gSearchTerms.join(" ") == prevSearch)
+ return;
+
+ // Clear out values before using them
+ clearTimeout(gBuilder);
+ gStmt.reset();
+
+ // Clear the list before adding items by replacing with a shallow copy
+ let empty = gDownloadsView.cloneNode(false);
+ gDownloadsView.parentNode.replaceChild(empty, gDownloadsView);
+ gDownloadsView = empty;
+
+ try {
+ gStmt.bindByIndex(0, nsIDM.DOWNLOAD_NOTSTARTED);
+ gStmt.bindByIndex(1, nsIDM.DOWNLOAD_DOWNLOADING);
+ gStmt.bindByIndex(2, nsIDM.DOWNLOAD_PAUSED);
+ gStmt.bindByIndex(3, nsIDM.DOWNLOAD_QUEUED);
+ gStmt.bindByIndex(4, nsIDM.DOWNLOAD_SCANNING);
+ } catch (e) {
+ // Something must have gone wrong when binding, so clear and quit
+ gStmt.reset();
+ return;
+ }
+
+ // Take a quick break before we actually start building the list
+ gBuilder = setTimeout(function() {
+ // Start building the list
+ stepListBuilder(1);
+
+ // We just tried to add a single item, so we probably need to enable
+ updateClearListButton();
+ }, 0);
+}
+
+/**
+ * Incrementally build the download list by adding at most the requested number
+ * of items if there are items to add. After doing that, it will schedule
+ * another chunk of items specified by gListBuildDelay and gListBuildChunk.
+ *
+ * @param aNumItems
+ * Number of items to add to the list before taking a break
+ */
+function stepListBuilder(aNumItems) {
+ try {
+ // If we're done adding all items, we can quit
+ if (!gStmt.executeStep()) {
+ // Send a notification that we finished, but wait for clear list to update
+ updateClearListButton();
+ setTimeout(() => Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService).
+ notifyObservers(window, "download-manager-ui-done", null), 0);
+
+ return;
+ }
+
+ // Try to get the attribute values from the statement
+ let attrs = {
+ dlid: gStmt.getInt64(0),
+ file: gStmt.getString(1),
+ target: gStmt.getString(2),
+ uri: gStmt.getString(3),
+ state: gStmt.getInt32(4),
+ startTime: Math.round(gStmt.getInt64(5) / 1000),
+ endTime: Math.round(gStmt.getInt64(6) / 1000),
+ currBytes: gStmt.getInt64(8),
+ maxBytes: gStmt.getInt64(9)
+ };
+
+ // Only add the referrer if it's not null
+ let referrer = gStmt.getString(7);
+ if (referrer)
+ attrs.referrer = referrer;
+
+ // If the download is active, grab the real progress, otherwise default 100
+ let isActive = gStmt.getInt32(10);
+ attrs.progress = isActive ? gDownloadManager.getDownload(attrs.dlid).
+ percentComplete : 100;
+
+ // Make the item and add it to the end if it's active or matches the search
+ let item = createDownloadItem(attrs);
+ if (item && (isActive || downloadMatchesSearch(item))) {
+ // Add item to the end
+ gDownloadsView.appendChild(item);
+
+ // Because of the joys of XBL, we can't update the buttons until the
+ // download object is in the document.
+ updateButtons(item);
+ } else {
+ // We didn't add an item, so bump up the number of items to process, but
+ // not a whole number so that we eventually do pause for a chunk break
+ aNumItems += .9;
+ }
+ } catch (e) {
+ // Something went wrong when stepping or getting values, so clear and quit
+ gStmt.reset();
+ return;
+ }
+
+ // Add another item to the list if we should; otherwise, let the UI update
+ // and continue later
+ if (aNumItems > 1) {
+ stepListBuilder(aNumItems - 1);
+ } else {
+ // Use a shorter delay for earlier downloads to display them faster
+ let delay = Math.min(gDownloadsView.itemCount * 10, gListBuildDelay);
+ gBuilder = setTimeout(stepListBuilder, delay, gListBuildChunk);
+ }
+}
+
+/**
+ * Add a download to the front of the download list
+ *
+ * @param aDownload
+ * The nsIDownload to make into a richlistitem
+ */
+function prependList(aDownload)
+{
+ let attrs = {
+ dlid: aDownload.id,
+ file: aDownload.target.spec,
+ target: aDownload.displayName,
+ uri: aDownload.source.spec,
+ state: aDownload.state,
+ progress: aDownload.percentComplete,
+ startTime: Math.round(aDownload.startTime / 1000),
+ endTime: Date.now(),
+ currBytes: aDownload.amountTransferred,
+ maxBytes: aDownload.size
+ };
+
+ // Make the item and add it to the beginning
+ let item = createDownloadItem(attrs);
+ if (item) {
+ // Add item to the beginning
+ gDownloadsView.insertBefore(item, gDownloadsView.firstChild);
+
+ // Because of the joys of XBL, we can't update the buttons until the
+ // download object is in the document.
+ updateButtons(item);
+
+ // We might have added an item to an empty list, so update button
+ updateClearListButton();
+ }
+}
+
+/**
+ * Check if the download matches the current search term based on the texts
+ * shown to the user. All search terms are checked to see if each matches any
+ * of the displayed texts.
+ *
+ * @param aItem
+ * Download richlistitem to check if it matches the current search
+ * @return Boolean true if it matches the search; false otherwise
+ */
+function downloadMatchesSearch(aItem)
+{
+ // Search through the download attributes that are shown to the user and
+ // make it into one big string for easy combined searching
+ let combinedSearch = "";
+ for (let attr of gSearchAttributes)
+ combinedSearch += aItem.getAttribute(attr).toLowerCase() + " ";
+
+ // Make sure each of the terms are found
+ for (let term of gSearchTerms)
+ if (combinedSearch.indexOf(term) == -1)
+ return false;
+
+ return true;
+}
+
+// we should be using real URLs all the time, but until
+// bug 239948 is fully fixed, this will do...
+//
+// note, this will thrown an exception if the native path
+// is not valid (for example a native Windows path on a Mac)
+// see bug #392386 for details
+function getLocalFileFromNativePathOrUrl(aPathOrUrl)
+{
+ if (aPathOrUrl.substring(0, 7) == "file://") {
+ // if this is a URL, get the file from that
+ let ioSvc = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+
+ // XXX it's possible that using a null char-set here is bad
+ const fileUrl = ioSvc.newURI(aPathOrUrl, null, null).
+ QueryInterface(Ci.nsIFileURL);
+ return fileUrl.file.clone().QueryInterface(Ci.nsILocalFile);
+ }
+ // if it's a pathname, create the nsILocalFile directly
+ var f = new nsLocalFile(aPathOrUrl);
+
+ return f;
+}
+
+/**
+ * Update the disabled state of the clear list button based on whether or not
+ * there are items in the list that can potentially be removed.
+ */
+function updateClearListButton()
+{
+ let button = document.getElementById("clearListButton");
+ // The button is enabled if we have items in the list and we can clean up
+ button.disabled = !(gDownloadsView.itemCount && gDownloadManager.canCleanUp);
+}
+
+function getDownload(aID)
+{
+ return document.getElementById("dl" + aID);
+}
+
+/**
+ * Initialize the statement which is used to retrieve the list of downloads.
+ */
+function initStatement()
+{
+ if (gStmt)
+ gStmt.finalize();
+
+ gStmt = gDownloadManager.DBConnection.createStatement(
+ "SELECT id, target, name, source, state, startTime, endTime, referrer, " +
+ "currBytes, maxBytes, state IN (?1, ?2, ?3, ?4, ?5) isActive " +
+ "FROM moz_downloads " +
+ "ORDER BY isActive DESC, endTime DESC, startTime DESC");
+}
diff --git a/components/downloads/content/downloads.xul b/components/downloads/content/downloads.xul
new file mode 100644
index 000000000..b5ca87a0c
--- /dev/null
+++ b/components/downloads/content/downloads.xul
@@ -0,0 +1,154 @@
+<?xml version="1.0"?>
+
+# -*- Mode: XML; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#ifdef XP_UNIX
+#define XP_GNOME 1
+#endif
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://mozapps/content/downloads/downloads.css"?>
+<?xml-stylesheet href="chrome://mozapps/skin/downloads/downloads.css"?>
+
+<!DOCTYPE window [
+<!ENTITY % downloadManagerDTD SYSTEM "chrome://mozapps/locale/downloads/downloads.dtd">
+%downloadManagerDTD;
+<!ENTITY % editMenuOverlayDTD SYSTEM "chrome://global/locale/editMenuOverlay.dtd">
+%editMenuOverlayDTD;
+]>
+
+<window xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="downloadManager" windowtype="Download:Manager"
+ orient="vertical" title="&downloads.title;" statictitle="&downloads.title;"
+ width="&window.width2;" height="&window.height;" screenX="10" screenY="10"
+ persist="width height screenX screenY sizemode"
+ onload="Startup();" onunload="Shutdown();"
+ onclose="return closeWindow(false);">
+
+ <script type="application/javascript" src="chrome://mozapps/content/downloads/downloads.js"/>
+ <script type="application/javascript" src="chrome://mozapps/content/downloads/DownloadProgressListener.js"/>
+ <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
+ <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+
+ <stringbundleset id="downloadSet">
+ <stringbundle id="brandStrings" src="chrome://branding/locale/brand.properties"/>
+ <stringbundle id="downloadStrings" src="chrome://mozapps/locale/downloads/downloads.properties"/>
+ </stringbundleset>
+
+ <!-- Use this commandset for command which do not depened on focus or selection -->
+ <commandset id="generalCommands">
+ <command id="cmd_findDownload" oncommand="setSearchboxFocus();"/>
+ <command id="cmd_selectAllDownloads" oncommand="gDownloadsView.selectAll();"/>
+ <command id="cmd_clearList" oncommand="clearDownloadList();"/>
+ </commandset>
+
+ <keyset id="downloadKeys">
+ <key keycode="VK_RETURN" oncommand="doDefaultForSelected();"/>
+ <key id="key_pauseResume" key=" " oncommand="performCommand('cmd_pauseResume');"/>
+ <key id="key_removeFromList" keycode="VK_DELETE" oncommand="performCommand('cmd_removeFromList');"/>
+ <key id="key_close" key="&cmd.close.commandKey;" oncommand="closeWindow(true);" modifiers="accel"/>
+#ifdef XP_GNOME
+ <key id="key_close2" key="&cmd.close2Unix.commandKey;" oncommand="closeWindow(true);" modifiers="accel,shift"/>
+#else
+ <key id="key_close2" key="&cmd.close2.commandKey;" oncommand="closeWindow(true);" modifiers="accel"/>
+#endif
+ <key keycode="VK_ESCAPE" oncommand="closeWindow(true);"/>
+
+ <key id="key_findDownload"
+ key="&cmd.find.commandKey;"
+ modifiers="accel"
+ command="cmd_findDownload"/>
+ <key id="key_findDownload2"
+ key="&cmd.search.commandKey;"
+ modifiers="accel"
+ command="cmd_findDownload"/>
+ <key id="key_selectAllDownloads"
+ key="&selectAllCmd.key;"
+ modifiers="accel"
+ command="cmd_selectAllDownloads"/>
+ <key id="pasteKey"
+ key="V"
+ modifiers="accel"
+ oncommand="pasteHandler();"/>
+ </keyset>
+
+ <vbox id="contextMenuPalette" hidden="true">
+ <menuitem id="menuitem_pause"
+ label="&cmd.pause.label;" accesskey="&cmd.pause.accesskey;"
+ oncommand="performCommand('cmd_pause');"
+ cmd="cmd_pause"/>
+ <menuitem id="menuitem_resume"
+ label="&cmd.resume.label;" accesskey="&cmd.resume.accesskey;"
+ oncommand="performCommand('cmd_resume');"
+ cmd="cmd_resume"/>
+ <menuitem id="menuitem_cancel"
+ label="&cmd.cancel.label;" accesskey="&cmd.cancel.accesskey;"
+ oncommand="performCommand('cmd_cancel');"
+ cmd="cmd_cancel"/>
+
+ <menuitem id="menuitem_open" default="true"
+ label="&cmd.open.label;" accesskey="&cmd.open.accesskey;"
+ oncommand="performCommand('cmd_open');"
+ cmd="cmd_open"/>
+ <menuitem id="menuitem_show"
+ label="&cmd.show.label;"
+ accesskey="&cmd.show.accesskey;"
+ oncommand="performCommand('cmd_show');"
+ cmd="cmd_show"/>
+
+ <menuitem id="menuitem_retry" default="true"
+ label="&cmd.retry.label;" accesskey="&cmd.retry.accesskey;"
+ oncommand="performCommand('cmd_retry');"
+ cmd="cmd_retry"/>
+
+ <menuitem id="menuitem_removeFromList"
+ label="&cmd.removeFromList.label;" accesskey="&cmd.removeFromList.accesskey;"
+ oncommand="performCommand('cmd_removeFromList');"
+ cmd="cmd_removeFromList"/>
+
+ <menuseparator id="menuseparator"/>
+
+ <menuitem id="menuitem_openReferrer"
+ label="&cmd.goToDownloadPage.label;"
+ accesskey="&cmd.goToDownloadPage.accesskey;"
+ oncommand="performCommand('cmd_openReferrer');"
+ cmd="cmd_openReferrer"/>
+
+ <menuitem id="menuitem_copyLocation"
+ label="&cmd.copyDownloadLink.label;"
+ accesskey="&cmd.copyDownloadLink.accesskey;"
+ oncommand="performCommand('cmd_copyLocation');"
+ cmd="cmd_copyLocation"/>
+
+ <menuitem id="menuitem_selectAll"
+ label="&selectAllCmd.label;"
+ accesskey="&selectAllCmd.accesskey;"
+ command="cmd_selectAllDownloads"/>
+ </vbox>
+
+ <menupopup id="downloadContextMenu" onpopupshowing="return buildContextMenu(event);"/>
+
+ <richlistbox id="downloadView" seltype="multiple" flex="1"
+ context="downloadContextMenu"
+ ondblclick="onDownloadDblClick(event);"
+ ondragstart="gDownloadDNDObserver.onDragStart(event);"
+ ondragover="gDownloadDNDObserver.onDragOver(event);event.stopPropagation();"
+ ondrop="gDownloadDNDObserver.onDrop(event)">
+ </richlistbox>
+
+ <windowdragbox id="search" align="center">
+ <button id="clearListButton" command="cmd_clearList"
+ label="&cmd.clearList.label;"
+ accesskey="&cmd.clearList.accesskey;"
+ tooltiptext="&cmd.clearList.tooltip;"/>
+ <spacer flex="1"/>
+ <textbox type="search" id="searchbox" class="compact"
+ aria-controls="downloadView"
+ oncommand="buildDownloadList();" placeholder="&searchBox.label;"/>
+ </windowdragbox>
+
+</window>
diff --git a/components/downloads/content/unknownContentType.xul b/components/downloads/content/unknownContentType.xul
new file mode 100644
index 000000000..42d356e9f
--- /dev/null
+++ b/components/downloads/content/unknownContentType.xul
@@ -0,0 +1,103 @@
+<?xml version="1.0"?>
+# -*- Mode: XML; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://mozapps/skin/downloads/unknownContentType.css" type="text/css"?>
+
+<!DOCTYPE dialog [
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+ %brandDTD;
+ <!ENTITY % uctDTD SYSTEM "chrome://mozapps/locale/downloads/unknownContentType.dtd" >
+ %uctDTD;
+ <!ENTITY % scDTD SYSTEM "chrome://mozapps/locale/downloads/settingsChange.dtd" >
+ %scDTD;
+]>
+
+<dialog id="unknownContentType"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="dialog.initDialog();" onunload="if (dialog) dialog.onCancel();"
+#ifdef XP_WIN
+ style="width: 36em;"
+#else
+ style="width: 34em;"
+#endif
+ screenX="" screenY=""
+ persist="screenX screenY"
+ aria-describedby="intro location whichIs type from source unknownPrompt"
+ ondialogaccept="return dialog.onOK()"
+ ondialogcancel="return dialog.onCancel()">
+
+
+ <stringbundle id="strings" src="chrome://mozapps/locale/downloads/unknownContentType.properties"/>
+
+ <vbox flex="1" id="container">
+ <description id="intro">&intro2.label;</description>
+ <separator class="thin"/>
+ <hbox align="start" class="small-indent">
+ <image id="contentTypeImage"/>
+ <vbox flex="1">
+ <description id="location" class="plain" crop="start" flex="1"/>
+ <separator class="thin"/>
+ <hbox align="center">
+ <label id="whichIs" value="&whichIs.label;"/>
+ <textbox id="type" class="plain" readonly="true" flex="1" noinitialfocus="true"/>
+ </hbox>
+ <hbox align="center">
+ <label value="&from.label;" id="from"/>
+ <description id="source" class="plain" crop="start" flex="1"/>
+ </hbox>
+ </vbox>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <hbox align="center" id="basicBox" collapsed="true">
+ <label id="unknownPrompt" value="&unknownPromptText.label;" flex="1"/>
+ </hbox>
+
+ <groupbox flex="1" id="normalBox">
+ <caption label="&actionQuestion.label;"/>
+ <separator class="thin"/>
+ <radiogroup id="mode" class="small-indent">
+ <hbox>
+ <radio id="open" label="&openWith.label;" accesskey="&openWith.accesskey;"/>
+ <deck id="modeDeck" flex="1">
+ <hbox id="openHandlerBox" flex="1" align="center"/>
+ <hbox flex="1" align="center">
+ <button id="chooseButton" oncommand="dialog.chooseApp();"
+ label="&chooseHandler.label;" accesskey="&chooseHandler.accesskey;"/>
+ </hbox>
+ </deck>
+ </hbox>
+
+ <radio id="save" label="&saveFile.label;" accesskey="&saveFile.accesskey;"/>
+ </radiogroup>
+ <separator class="thin"/>
+ <hbox class="small-indent">
+ <checkbox id="rememberChoice" label="&rememberChoice.label;"
+ accesskey="&rememberChoice.accesskey;"
+ oncommand="dialog.toggleRememberChoice(event.target);"/>
+ </hbox>
+
+ <separator/>
+#ifdef XP_UNIX
+ <description id="settingsChange" hidden="true">&settingsChangePreferences.label;</description>
+#else
+ <description id="settingsChange" hidden="true">&settingsChangeOptions.label;</description>
+#endif
+ <separator class="thin"/>
+ </groupbox>
+ </vbox>
+
+ <menulist id="openHandler" flex="1">
+ <menupopup id="openHandlerPopup" oncommand="dialog.openHandlerCommand();">
+ <menuitem id="defaultHandler" default="true" crop="right"/>
+ <menuitem id="otherHandler" hidden="true" crop="left"/>
+ <menuseparator/>
+ <menuitem id="choose" label="&other.label;"/>
+ </menupopup>
+ </menulist>
+</dialog>
diff --git a/components/downloads/jar.mn b/components/downloads/jar.mn
new file mode 100644
index 000000000..29a3d0ee2
--- /dev/null
+++ b/components/downloads/jar.mn
@@ -0,0 +1,12 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+toolkit.jar:
+% content mozapps %content/mozapps/
+* content/mozapps/downloads/unknownContentType.xul (content/unknownContentType.xul)
+* content/mozapps/downloads/downloads.xul (content/downloads.xul)
+* content/mozapps/downloads/downloads.js (content/downloads.js)
+ content/mozapps/downloads/DownloadProgressListener.js (content/DownloadProgressListener.js)
+ content/mozapps/downloads/downloads.css (content/downloads.css)
+ content/mozapps/downloads/download.xml (content/download.xml)
diff --git a/components/downloads/locale/downloads.dtd b/components/downloads/locale/downloads.dtd
new file mode 100644
index 000000000..3c6373cfb
--- /dev/null
+++ b/components/downloads/locale/downloads.dtd
@@ -0,0 +1,52 @@
+<!-- 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/. -->
+
+<!-- LOCALIZATION NOTE (window.width2, window.height): These values should be
+close to the golden ratio (1.618:1) while making sure it's wide enough for long
+file names and tall enough to hint that there are more downloads in the list -->
+<!ENTITY window.width2 "485">
+<!ENTITY window.height "300">
+
+<!ENTITY starting.label "Starting…">
+<!ENTITY scanning.label "Scanning for viruses…">
+
+<!ENTITY downloads.title "Downloads">
+
+<!ENTITY cmd.pause.label "Pause">
+<!ENTITY cmd.pause.accesskey "P">
+<!ENTITY cmd.resume.label "Resume">
+<!ENTITY cmd.resume.accesskey "R">
+<!ENTITY cmd.cancel.label "Cancel">
+<!ENTITY cmd.cancel.accesskey "C">
+<!ENTITY cmd.show.label "Open Containing Folder">
+<!ENTITY cmd.show.accesskey "F">
+<!ENTITY cmd.showMac.label "Show in Finder">
+<!ENTITY cmd.showMac.accesskey "F">
+<!ENTITY cmd.open.label "Open">
+<!ENTITY cmd.open.accesskey "O">
+<!ENTITY cmd.openWith.label "Open With…">
+<!ENTITY cmd.openWith.accesskey "h">
+<!ENTITY cmd.retry.label "Retry">
+<!ENTITY cmd.retry.accesskey "R">
+<!ENTITY cmd.goToDownloadPage.label "Go to Download Page">
+<!ENTITY cmd.goToDownloadPage.accesskey "G">
+<!ENTITY cmd.copyDownloadLink.label "Copy Download Link">
+<!ENTITY cmd.copyDownloadLink.accesskey "L">
+<!ENTITY cmd.removeFromList.label "Remove From List">
+<!ENTITY cmd.removeFromList.accesskey "e">
+
+<!ENTITY cmd.close.commandKey "w">
+<!ENTITY cmd.close2.commandKey "j">
+<!ENTITY cmd.close2Unix.commandKey "y">
+<!ENTITY cmd.clearList.label "Clear List">
+<!ENTITY cmd.clearList.tooltip "Removes completed, canceled, and failed downloads from the list">
+<!ENTITY cmd.clearList.accesskey "C">
+<!ENTITY cmd.find.commandKey "f">
+<!ENTITY cmd.search.commandKey "k">
+
+<!ENTITY closeWhenDone.label "Close when downloads complete">
+<!ENTITY closeWhenDone.tooltip "Closes the Downloads window when all files are done downloading">
+
+<!ENTITY showFolder.label "Show this Folder">
+<!ENTITY searchBox.label "Search…">
diff --git a/components/downloads/locale/downloads.properties b/components/downloads/locale/downloads.properties
new file mode 100644
index 000000000..af95022f1
--- /dev/null
+++ b/components/downloads/locale/downloads.properties
@@ -0,0 +1,141 @@
+# 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/.
+
+# LOCALIZATION NOTE (seconds, minutes, hours, days): Semi-colon list of plural
+# forms. See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+seconds=second;seconds
+minutes=minute;minutes
+hours=hour;hours
+days=day;days
+
+# LOCALIZATION NOTE (paused): — is the "em dash" (long dash)
+paused=Paused — #1
+downloading=Downloading
+notStarted=Not Started
+failed=Failed
+finished=Finished
+canceled=Canceled
+
+cannotPause=This download cannot be paused
+
+downloadErrorAlertTitle=Download Error
+downloadErrorGeneric=The download cannot be saved because an unknown error occurred.\n\nPlease try again.
+
+# LOCALIZATION NOTE: we don't have proper plural support in the CPP code; bug 463102
+quitCancelDownloadsAlertTitle=Cancel All Downloads?
+quitCancelDownloadsAlertMsg=If you exit now, 1 download will be canceled. Are you sure you want to exit?
+quitCancelDownloadsAlertMsgMultiple=If you exit now, %S downloads will be canceled. Are you sure you want to exit?
+quitCancelDownloadsAlertMsgMac=If you quit now, 1 download will be canceled. Are you sure you want to quit?
+quitCancelDownloadsAlertMsgMacMultiple=If you quit now, %S downloads will be canceled. Are you sure you want to quit?
+offlineCancelDownloadsAlertTitle=Cancel All Downloads?
+offlineCancelDownloadsAlertMsg=If you go offline now, 1 download will be canceled. Are you sure you want to go offline?
+offlineCancelDownloadsAlertMsgMultiple=If you go offline now, %S downloads will be canceled. Are you sure you want to go offline?
+leavePrivateBrowsingCancelDownloadsAlertTitle=Cancel All Downloads?
+leavePrivateBrowsingWindowsCancelDownloadsAlertMsg2=If you close all Private Browsing windows now, 1 download will be canceled. Are you sure you want to leave Private Browsing?
+leavePrivateBrowsingWindowsCancelDownloadsAlertMsgMultiple2=If you close all Private Browsing windows now, %S downloads will be canceled. Are you sure you want to leave Private Browsing?
+cancelDownloadsOKText=Cancel 1 Download
+cancelDownloadsOKTextMultiple=Cancel %S Downloads
+dontQuitButtonWin=Don’t Exit
+dontQuitButtonMac=Don’t Quit
+dontGoOfflineButton=Stay Online
+dontLeavePrivateBrowsingButton2=Stay in Private Browsing
+downloadsCompleteTitle=Downloads Complete
+downloadsCompleteMsg=All files have finished downloading.
+
+# LOCALIZATION NOTE (infiniteRate):
+# If download speed is a JavaScript Infinity value, this phrase is used
+infiniteRate=Really fast
+
+# LOCALIZATION NOTE (statusFormat3): — is the "em dash" (long dash)
+# %1$S transfer progress; %2$S rate number; %3$S rate unit; %4$S time left
+# example: 4 minutes left — 1.1 of 11.1 GB (2.2 MB/sec)
+statusFormat3=%4$S — %1$S (%2$S %3$S/sec)
+
+# LOCALIZATION NOTE (statusFormatInfiniteRate): — is the "em dash" (long dash)
+# %1$S transfer progress; %2$S substitute phrase for Infinity speed; %3$S time left
+# example: 4 minutes left — 1.1 of 11.1 GB (Really fast)
+statusFormatInfiniteRate=%3$S — %1$S (%2$S)
+
+# LOCALIZATION NOTE (statusFormatNoRate): — is the "em dash" (long dash)
+# %1$S transfer progress; %2$S time left
+# example: 4 minutes left — 1.1 of 11.1 GB
+statusFormatNoRate=%2$S — %1$S
+
+bytes=bytes
+kilobyte=KB
+megabyte=MB
+gigabyte=GB
+
+# LOCALIZATION NOTE (transferSameUnits2):
+# %1$S progress number; %2$S total number; %3$S total unit
+# example: 1.1 of 333 MB
+transferSameUnits2=%1$S of %2$S %3$S
+# LOCALIZATION NOTE (transferDiffUnits2):
+# %1$S progress number; %2$S progress unit; %3$S total number; %4$S total unit
+# example: 11.1 MB of 3.3 GB
+transferDiffUnits2=%1$S %2$S of %3$S %4$S
+# LOCALIZATION NOTE (transferNoTotal2):
+# %1$S progress number; %2$S unit
+# example: 111 KB
+transferNoTotal2=%1$S %2$S
+
+# LOCALIZATION NOTE (timePair2): %1$S time number; %2$S time unit
+# example: 1 minute; 11 hours
+timePair2=%1$S %2$S
+# LOCALIZATION NOTE (timeLeftSingle2): %1$S time left
+# example: 1 minute remaining; 11 hours remaining
+timeLeftSingle2=%1$S remaining
+# LOCALIZATION NOTE (timeLeftDouble2): %1$S time left; %2$S time left sub units
+# example: 11 hours, 2 minutes remaining; 1 day, 22 hours remaining
+timeLeftDouble2=%1$S, %2$S remaining
+timeFewSeconds=A few seconds remaining
+timeUnknown=Unknown time remaining
+
+# LOCALIZATION NOTE (doneStatus): — is the "em dash" (long dash)
+# #1 download size for FINISHED or download state; #2 host (e.g., eTLD + 1, IP)
+# #2 can also be doneScheme or doneFileScheme for special URIs like file:
+# examples: 1.1 MB — website2.com; Canceled — 222.net
+doneStatus=#1 — #2
+# LOCALIZATION NOTE (doneSize): #1 size number; #2 size unit
+doneSize=#1 #2
+doneSizeUnknown=Unknown size
+# LOCALIZATION NOTE (doneScheme): #1 URI scheme like data: jar: about:
+doneScheme2=%1$S resource
+# LOCALIZATION NOTE (doneFileScheme): Special case of doneScheme for file:
+# This is used as an eTLD replacement for local files, so make it lower case
+doneFileScheme=local file
+
+stateFailed=Failed
+stateCanceled=Canceled
+# LOCALIZATION NOTE (stateBlocked): 'Parental Controls' should be capitalized
+stateBlocked=Blocked by Parental Controls
+stateDirty=Blocked: Download may contain a virus or spyware
+# LOCALIZATION NOTE (stateBlockedPolicy): 'Security Zone Policy' should be capitalized
+ stateBlockedPolicy=This download has been blocked by your Security Zone Policy
+
+# LOCALIZATION NOTE (yesterday): Displayed time for files finished yesterday
+yesterday=Yesterday
+# LOCALIZATION NOTE (monthDate): #1 month name; #2 date number; e.g., January 22
+monthDate2=%1$S %2$S
+
+fileDoesNotExistOpenTitle=Cannot Open %S
+fileDoesNotExistShowTitle=Cannot Show %S
+fileDoesNotExistError=%S does not exist. It may have been renamed, moved, or deleted since it was downloaded.
+
+chooseAppFilePickerTitle=Open With…
+
+# LOCALIZATION NOTE (downloadsTitleFiles, downloadsTitlePercent): Semi-colon list of
+# plural forms. See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 number of files; #2 overall download percent (only for downloadsTitlePercent)
+# examples: 2% of 1 file - Downloads; 22% of 11 files - Downloads
+downloadsTitleFiles=#1 file - Downloads;#1 files - Downloads
+downloadsTitlePercent=#2% of #1 file - Downloads;#2% of #1 files - Downloads
+
+fileExecutableSecurityWarning=“%S” is an executable file. Executable files may contain viruses or other malicious code that could harm your computer. Use caution when opening this file. Are you sure you want to launch “%S”?
+fileExecutableSecurityWarningTitle=Open Executable File?
+
+displayNameDesktop=Desktop
+
+# Desktop folder name for downloaded files
+downloadsFolder=Downloads
diff --git a/components/downloads/locale/settingsChange.dtd b/components/downloads/locale/settingsChange.dtd
new file mode 100644
index 000000000..f28f7f341
--- /dev/null
+++ b/components/downloads/locale/settingsChange.dtd
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+
+<!ENTITY settingsChangePreferences.label "Settings can be changed in &brandShortName;'s Preferences.">
+<!ENTITY settingsChangeOptions.label "Settings can be changed in &brandShortName;'s Options.">
diff --git a/components/downloads/locale/unknownContentType.dtd b/components/downloads/locale/unknownContentType.dtd
new file mode 100644
index 000000000..e0fbf7368
--- /dev/null
+++ b/components/downloads/locale/unknownContentType.dtd
@@ -0,0 +1,26 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY intro2.label "You have chosen to open:">
+<!ENTITY from.label "from:">
+<!ENTITY actionQuestion.label "What should &brandShortName; do with this file?">
+
+<!ENTITY openWith.label "Open with">
+<!ENTITY openWith.accesskey "o">
+<!ENTITY other.label "Other…">
+
+<!ENTITY saveFile.label "Save File">
+<!ENTITY saveFile.accesskey "s">
+
+<!ENTITY rememberChoice.label "Do this automatically for files like this from now on.">
+<!ENTITY rememberChoice.accesskey "a">
+
+<!ENTITY whichIs.label "which is:">
+
+<!ENTITY chooseHandlerMac.label "Choose…">
+<!ENTITY chooseHandlerMac.accesskey "C">
+<!ENTITY chooseHandler.label "Browse…">
+<!ENTITY chooseHandler.accesskey "B">
+
+<!ENTITY unknownPromptText.label "Would you like to save this file?">
diff --git a/components/downloads/locale/unknownContentType.properties b/components/downloads/locale/unknownContentType.properties
new file mode 100644
index 000000000..e599133ce
--- /dev/null
+++ b/components/downloads/locale/unknownContentType.properties
@@ -0,0 +1,19 @@
+# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+title=Opening %S
+saveDialogTitle=Enter name of file to save to…
+defaultApp=%S (default)
+chooseAppFilePickerTitle=Choose Helper Application
+badApp=The application you chose (“%S”) could not be found. Check the file name or choose another application.
+badApp.title=Application not found
+badPermissions=The file could not be saved because you do not have the proper permissions. Choose another save directory.
+badPermissions.title=Invalid Save Permissions
+selectDownloadDir=Select Download Folder
+unknownAccept.label=Save File
+unknownCancel.label=Cancel
+fileType=%S file
+# LOCALIZATION NOTE (orderedFileSizeWithType): first %S is type, second %S is size, and third %S is unit
+orderedFileSizeWithType=%1$S (%2$S %3$S)
diff --git a/components/downloads/moz.build b/components/downloads/moz.build
new file mode 100644
index 000000000..ffd526570
--- /dev/null
+++ b/components/downloads/moz.build
@@ -0,0 +1,57 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# 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/.
+
+DEFINES['GOOGLE_PROTOBUF_NO_RTTI'] = True
+DEFINES['GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER'] = True
+
+CXXFLAGS += CONFIG['TK_CFLAGS']
+
+if CONFIG['GNU_CXX']:
+ CXXFLAGS += ['-Wno-shadow']
+
+XPIDL_SOURCES += [
+ 'public/nsIDownload.idl',
+ 'public/nsIDownloadManager.idl',
+ 'public/nsIDownloadManagerUI.idl',
+ 'public/nsIDownloadProgressListener.idl',
+]
+
+SOURCES += [
+ 'src/nsDownloadManager.cpp',
+ 'src/SQLFunctions.cpp',
+]
+
+if CONFIG['OS_ARCH'] == 'WINNT':
+ SOURCES += ['src/nsDownloadScanner.cpp']
+
+EXTRA_COMPONENTS += ['nsHelperAppDlg.manifest']
+
+EXTRA_PP_COMPONENTS += ['src/nsHelperAppDlg.js']
+
+# The Communicator Downloads Manager uses its own DownloadManagerUI
+# component and it can't be guaranteed that its implementation will override
+# toolkit's so don't include toolkit's
+if not CONFIG['BINOC_DOWNLOADS']:
+ EXTRA_COMPONENTS += [
+ 'nsDownloadManagerUI.manifest',
+ 'src/nsDownloadManagerUI.js',
+ ]
+
+EXTRA_JS_MODULES += [
+ 'src/DownloadLastDir.jsm',
+ 'src/DownloadPaths.jsm',
+ 'src/DownloadUtils.jsm',
+]
+
+EXTRA_PP_JS_MODULES += ['src/DownloadTaskbarProgress.jsm']
+
+LOCAL_INCLUDES += [
+ '/ipc/chromium/src',
+ '/libs/protobuf',
+]
+
+XPIDL_MODULE = 'downloads'
+FINAL_LIBRARY = 'xul'
+JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file
diff --git a/components/downloads/nsDownloadManagerUI.manifest b/components/downloads/nsDownloadManagerUI.manifest
new file mode 100644
index 000000000..4073c23fb
--- /dev/null
+++ b/components/downloads/nsDownloadManagerUI.manifest
@@ -0,0 +1,2 @@
+component {7dfdf0d1-aff6-4a34-bad1-d0fe74601642} nsDownloadManagerUI.js
+contract @mozilla.org/download-manager-ui;1 {7dfdf0d1-aff6-4a34-bad1-d0fe74601642}
diff --git a/components/downloads/nsHelperAppDlg.manifest b/components/downloads/nsHelperAppDlg.manifest
new file mode 100644
index 000000000..8824b45a2
--- /dev/null
+++ b/components/downloads/nsHelperAppDlg.manifest
@@ -0,0 +1,2 @@
+component {F68578EB-6EC2-4169-AE19-8C6243F0ABE1} nsHelperAppDlg.js
+contract @mozilla.org/helperapplauncherdialog;1 {F68578EB-6EC2-4169-AE19-8C6243F0ABE1}
diff --git a/components/downloads/public/nsIDownload.idl b/components/downloads/public/nsIDownload.idl
new file mode 100644
index 000000000..47eb48780
--- /dev/null
+++ b/components/downloads/public/nsIDownload.idl
@@ -0,0 +1,175 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsITransfer.idl"
+
+interface nsIURI;
+interface nsIFile;
+interface nsIObserver;
+interface nsICancelable;
+interface nsIWebProgressListener;
+interface nsIMIMEInfo;
+
+/**
+ * Represents a download object.
+ *
+ * @note This object is no longer updated once it enters a completed state.
+ * Completed states are the following:
+ * nsIDownloadManager::DOWNLOAD_FINISHED
+ * nsIDownloadManager::DOWNLOAD_FAILED
+ * nsIDownloadManager::DOWNLOAD_CANCELED
+ * nsIDownloadManager::DOWNLOAD_BLOCKED_PARENTAL
+ * nsIDownloadManager::DOWNLOAD_DIRTY
+ * nsIDownloadManager::DOWNLOAD_BLOCKED_POLICY
+ */
+[scriptable, uuid(2258f465-656e-4566-87cb-f791dbaf0322)]
+interface nsIDownload : nsITransfer {
+
+ /**
+ * The target of a download is always a file on the local file system.
+ */
+ readonly attribute nsIFile targetFile;
+
+ /**
+ * The percentage of transfer completed.
+ * If the file size is unknown it'll be -1 here.
+ */
+ readonly attribute long percentComplete;
+
+ /**
+ * The amount of bytes downloaded so far.
+ */
+ readonly attribute long long amountTransferred;
+
+ /**
+ * The size of file in bytes.
+ * Unknown size is represented by -1.
+ */
+ readonly attribute long long size;
+
+ /**
+ * The source of the transfer.
+ */
+ readonly attribute nsIURI source;
+
+ /**
+ * The target of the transfer.
+ */
+ readonly attribute nsIURI target;
+
+ /**
+ * Object that can be used to cancel the download.
+ * Will be null after the download is finished.
+ */
+ readonly attribute nsICancelable cancelable;
+
+ /**
+ * The user-readable description of the transfer.
+ */
+ readonly attribute AString displayName;
+
+ /**
+ * The time a transfer was started.
+ */
+ readonly attribute long long startTime;
+
+ /**
+ * The speed of the transfer in bytes/sec.
+ */
+ readonly attribute double speed;
+
+ /**
+ * Optional. If set, it will contain the target's relevant MIME information.
+ * This includes its MIME Type, helper app, and whether that helper should be
+ * executed.
+ */
+ readonly attribute nsIMIMEInfo MIMEInfo;
+
+ /**
+ * The id of the download that is stored in the database - not globally unique.
+ * For example, a private download and a public one might have identical ids.
+ * Can only be safely used for direct database manipulation in the database that
+ * contains this download. Use the guid property instead for safe, database-agnostic
+ * searching and manipulation.
+ *
+ * @deprecated
+ */
+ readonly attribute unsigned long id;
+
+ /**
+ * The guid of the download that is stored in the database.
+ * Has the form of twelve alphanumeric characters.
+ */
+ readonly attribute ACString guid;
+
+ /**
+ * The state of the download.
+ * @see nsIDownloadManager and nsIXPInstallManagerUI
+ */
+ readonly attribute short state;
+
+ /**
+ * The referrer uri of the download. This is only valid for HTTP downloads,
+ * and can be null.
+ */
+ readonly attribute nsIURI referrer;
+
+ /**
+ * Indicates if the download can be resumed after being paused or not. This
+ * is only the case if the download is over HTTP/1.1 or FTP and if the
+ * server supports it.
+ */
+ readonly attribute boolean resumable;
+
+ /**
+ * Indicates if the download was initiated from a context marked as private,
+ * controlling whether it should be stored in a permanent manner or not.
+ */
+ readonly attribute boolean isPrivate;
+
+ /**
+ * Cancel this download if it's currently in progress.
+ */
+ void cancel();
+
+ /**
+ * Pause this download if it is in progress.
+ *
+ * @throws NS_ERROR_UNEXPECTED if it cannot be paused.
+ */
+ void pause();
+
+ /**
+ * Resume this download if it is paused.
+ *
+ * @throws NS_ERROR_UNEXPECTED if it cannot be resumed or is not paused.
+ */
+ void resume();
+
+ /**
+ * Instruct the download manager to remove this download. Whereas
+ * cancel simply cancels the transfer, but retains information about it,
+ * remove removes all knowledge of it.
+ *
+ * @see nsIDownloadManager.removeDownload for more detail
+ * @throws NS_ERROR_FAILURE if the download is active.
+ */
+ void remove();
+
+ /**
+ * Instruct the download manager to retry this failed download
+ * @throws NS_ERROR_NOT_AVAILABLE if the download is not known.
+ * @throws NS_ERROR_FAILURE if the download is not in the following states:
+ * nsIDownloadManager::DOWNLOAD_CANCELED
+ * nsIDownloadManager::DOWNLOAD_FAILED
+ */
+ void retry();
+};
+
+%{C++
+// {b02be33b-d47c-4bd3-afd9-402a942426b0}
+#define NS_DOWNLOAD_CID \
+ { 0xb02be33b, 0xd47c, 0x4bd3, { 0xaf, 0xd9, 0x40, 0x2a, 0x94, 0x24, 0x26, 0xb0 } }
+%}
diff --git a/components/downloads/public/nsIDownloadManager.idl b/components/downloads/public/nsIDownloadManager.idl
new file mode 100644
index 000000000..d7eba8940
--- /dev/null
+++ b/components/downloads/public/nsIDownloadManager.idl
@@ -0,0 +1,358 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Keeps track of ongoing downloads, in the form of nsIDownload's.
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+interface nsIFile;
+interface nsIDownload;
+interface nsICancelable;
+interface nsIMIMEInfo;
+interface nsIDownloadProgressListener;
+interface nsISimpleEnumerator;
+interface mozIStorageConnection;
+
+[scriptable, function, uuid(0c07ffeb-791b-49f3-ae38-2c331fd55a52)]
+interface nsIDownloadManagerResult : nsISupports {
+ /**
+ * Process an asynchronous result from getDownloadByGUID.
+ *
+ * @param aStatus The result code of the operation:
+ * * NS_OK: an item was found. No other success values are returned.
+ * * NS_ERROR_NOT_AVAILABLE: no such item was found.
+ * * Other error values are possible, but less well-defined.
+ */
+ void handleResult(in nsresult aStatus, in nsIDownload aDownload);
+};
+
+[scriptable, uuid(b29aac15-7ec4-4ab3-a53b-08f78aed3b34)]
+interface nsIDownloadManager : nsISupports {
+ /**
+ * Download type for generic file download.
+ */
+ const short DOWNLOAD_TYPE_DOWNLOAD = 0;
+
+ /**
+ * Download state for uninitialized download object.
+ */
+ const short DOWNLOAD_NOTSTARTED = -1;
+
+ /**
+ * Download is currently transferring data.
+ */
+ const short DOWNLOAD_DOWNLOADING = 0;
+
+ /**
+ * Download completed including any processing of the target
+ * file. (completed)
+ */
+ const short DOWNLOAD_FINISHED = 1;
+
+ /**
+ * Transfer failed due to error. (completed)
+ */
+ const short DOWNLOAD_FAILED = 2;
+
+ /**
+ * Download was canceled by the user. (completed)
+ */
+ const short DOWNLOAD_CANCELED = 3;
+
+ /**
+ * Transfer was paused by the user.
+ */
+ const short DOWNLOAD_PAUSED = 4;
+
+ /**
+ * Download is active but data has not yet been received.
+ */
+ const short DOWNLOAD_QUEUED = 5;
+
+ /**
+ * Transfer request was blocked by parental controls proxies. (completed)
+ */
+ const short DOWNLOAD_BLOCKED_PARENTAL = 6;
+
+ /**
+ * Transferred download is being scanned by virus scanners.
+ */
+ const short DOWNLOAD_SCANNING = 7;
+
+ /**
+ * A virus was detected in the download. The target will most likely
+ * no longer exist. (completed)
+ */
+ const short DOWNLOAD_DIRTY = 8;
+
+ /**
+ * Win specific: Request was blocked by zone policy settings.
+ * (see bug #416683) (completed)
+ */
+ const short DOWNLOAD_BLOCKED_POLICY = 9;
+
+
+ /**
+ * Creates an nsIDownload and adds it to be managed by the download manager.
+ *
+ * @param aSource The source URI of the transfer. Must not be null.
+ *
+ * @param aTarget The target URI of the transfer. Must not be null.
+ *
+ * @param aDisplayName The user-readable description of the transfer.
+ * Can be empty.
+ *
+ * @param aMIMEInfo The MIME info associated with the target,
+ * including MIME type and helper app when appropriate.
+ * This parameter is optional.
+ *
+ * @param startTime Time when the download started
+ *
+ * @param aTempFile The location of a temporary file; i.e. a file in which
+ * the received data will be stored, but which is not
+ * equal to the target file. (will be moved to the real
+ * target by the DownloadManager, when the download is
+ * finished). This will be null for all callers except for
+ * nsExternalHelperAppHandler. Addons should generally pass
+ * null for aTempFile. This will be moved to the real target
+ * by the download manager when the download is finished,
+ * and the action indicated by aMIMEInfo will be executed.
+ *
+ * @param aCancelable An object that can be used to abort the download.
+ * Must not be null.
+ *
+ * @param aIsPrivate Used to determine the privacy status of the new download.
+ * If true, the download is stored in a manner that leaves
+ * no permanent trace outside of the current private session.
+ *
+ * @return The newly created download item with the passed-in properties.
+ *
+ * @note This does not actually start a download. If you want to add and
+ * start a download, you need to create an nsIWebBrowserPersist, pass it
+ * as the aCancelable object, call this method, set the progressListener
+ * as the returned download object, then call saveURI.
+ */
+ nsIDownload addDownload(in short aDownloadType,
+ in nsIURI aSource,
+ in nsIURI aTarget,
+ in AString aDisplayName,
+ in nsIMIMEInfo aMIMEInfo,
+ in PRTime aStartTime,
+ in nsIFile aTempFile,
+ in nsICancelable aCancelable,
+ in boolean aIsPrivate);
+
+ /**
+ * Retrieves a download managed by the download manager. This can be one that
+ * is in progress, or one that has completed in the past and is stored in the
+ * database.
+ *
+ * @param aID The unique ID of the download.
+ * @return The download with the specified ID.
+ * @throws NS_ERROR_NOT_AVAILABLE if the download is not in the database.
+ */
+ nsIDownload getDownload(in unsigned long aID);
+
+ /**
+ * Retrieves a download managed by the download manager. This can be one that
+ * is in progress, or one that has completed in the past and is stored in the
+ * database. The result of this method is returned via an asynchronous callback,
+ * the parameter of which will be an nsIDownload object, or null if none exists
+ * with the provided GUID.
+ *
+ * @param aGUID The unique GUID of the download.
+ * @param aCallback The callback to invoke with the result of the search.
+ */
+ void getDownloadByGUID(in ACString aGUID, in nsIDownloadManagerResult aCallback);
+
+ /**
+ * Cancels the download with the specified ID if it's currently in-progress.
+ * This calls cancel(NS_BINDING_ABORTED) on the nsICancelable provided by the
+ * download.
+ *
+ * @param aID The unique ID of the download.
+ * @throws NS_ERROR_FAILURE if the download is not in-progress.
+ */
+ void cancelDownload(in unsigned long aID);
+
+ /**
+ * Removes the download with the specified id if it's not currently
+ * in-progress. Whereas cancelDownload simply cancels the transfer, but
+ * retains information about it, removeDownload removes all knowledge of it.
+ *
+ * Also notifies observers of the "download-manager-remove-download-guid"
+ * topic with the download guid as the subject to allow any DM consumers to
+ * react to the removal.
+ *
+ * Also may notify observers of the "download-manager-remove-download" topic
+ * with the download id as the subject, if the download removed is public
+ * or if global private browsing mode is in use. This notification is deprecated;
+ * the guid notification should be relied upon instead.
+ *
+ * @param aID The unique ID of the download.
+ * @throws NS_ERROR_FAILURE if the download is active.
+ */
+ void removeDownload(in unsigned long aID);
+
+ /**
+ * Removes all inactive downloads that were started inclusively within the
+ * specified time frame.
+ *
+ * @param aBeginTime
+ * The start time to remove downloads by in microseconds.
+ * @param aEndTime
+ * The end time to remove downloads by in microseconds.
+ */
+ void removeDownloadsByTimeframe(in long long aBeginTime,
+ in long long aEndTime);
+
+ /**
+ * Pause the specified download.
+ *
+ * @param aID The unique ID of the download.
+ * @throws NS_ERROR_FAILURE if the download is not in-progress.
+ */
+ void pauseDownload(in unsigned long aID);
+
+ /**
+ * Resume the specified download.
+ *
+ * @param aID The unique ID of the download.
+ * @throws NS_ERROR_FAILURE if the download is not in-progress.
+ */
+ void resumeDownload(in unsigned long aID);
+
+ /**
+ * Retries a failed download.
+ *
+ * @param aID The unique ID of the download.
+ * @throws NS_ERROR_NOT_AVAILALE if the download id is not known.
+ * @throws NS_ERROR_FAILURE if the download is not in the following states:
+ * nsIDownloadManager::DOWNLOAD_CANCELED
+ * nsIDownloadManager::DOWNLOAD_FAILED
+ */
+ void retryDownload(in unsigned long aID);
+
+ /**
+ * The database connection to the downloads database.
+ */
+ readonly attribute mozIStorageConnection DBConnection;
+ readonly attribute mozIStorageConnection privateDBConnection;
+
+ /**
+ * Whether or not there are downloads that can be cleaned up (removed)
+ * i.e. downloads that have completed, have failed or have been canceled.
+ * In global private browsing mode, this reports the status of the relevant
+ * private or public downloads. In per-window mode, it only reports for
+ * public ones.
+ */
+ readonly attribute boolean canCleanUp;
+
+ /**
+ * Whether or not there are private downloads that can be cleaned up (removed)
+ * i.e. downloads that have completed, have failed or have been canceled.
+ */
+readonly attribute boolean canCleanUpPrivate;
+
+ /**
+ * Removes completed, failed, and canceled downloads from the list.
+ * In global private browsing mode, this operates on the relevant
+ * private or public downloads. In per-window mode, it only operates
+ * on public ones.
+ *
+ * Also notifies observers of the "download-manager-remove-download-gui"
+ * and "download-manager-remove-download" topics with a null subject to
+ * allow any DM consumers to react to the removals.
+ */
+ void cleanUp();
+
+ /**
+ * Removes completed, failed, and canceled downloads from the list
+ * of private downloads.
+ *
+ * Also notifies observers of the "download-manager-remove-download-gui"
+ * and "download-manager-remove-download" topics with a null subject to
+ * allow any DM consumers to react to the removals.
+ */
+void cleanUpPrivate();
+
+ /**
+ * The number of files currently being downloaded.
+ *
+ * In global private browsing mode, this reports the status of the relevant
+ * private or public downloads. In per-window mode, it only reports public
+ * ones.
+ */
+ readonly attribute long activeDownloadCount;
+
+ /**
+ * The number of private files currently being downloaded.
+ */
+ readonly attribute long activePrivateDownloadCount;
+
+ /**
+ * An enumeration of active nsIDownloads
+ *
+ * In global private browsing mode, this reports the status of the relevant
+ * private or public downloads. In per-window mode, it only reports public
+ * ones.
+ */
+ readonly attribute nsISimpleEnumerator activeDownloads;
+
+ /**
+ * An enumeration of active private nsIDownloads
+ */
+ readonly attribute nsISimpleEnumerator activePrivateDownloads;
+
+ /**
+ * Adds a listener to the download manager. It is expected that this
+ * listener will only access downloads via their deprecated integer id attribute,
+ * and when global private browsing compatibility mode is disabled, this listener
+ * will receive no notifications for downloads marked private.
+ */
+ void addListener(in nsIDownloadProgressListener aListener);
+
+ /**
+ * Adds a listener to the download manager. This listener must be able to
+ * understand and use the guid attribute of downloads for all interactions
+ * with the download manager.
+ */
+ void addPrivacyAwareListener(in nsIDownloadProgressListener aListener);
+
+ /**
+ * Removes a listener from the download manager.
+ */
+ void removeListener(in nsIDownloadProgressListener aListener);
+
+ /**
+ * Returns the platform default downloads directory.
+ */
+ readonly attribute nsIFile defaultDownloadsDirectory;
+
+ /**
+ * Returns the user configured downloads directory.
+ * The path is dependent on two user configurable prefs
+ * set in preferences:
+ *
+ * browser.download.folderList
+ * Indicates the location users wish to save downloaded
+ * files too.
+ * Values:
+ * 0 - The desktop is the default download location.
+ * 1 - The system's downloads folder is the default download location.
+ * 2 - The default download location is elsewhere as specified in
+ * browser.download.dir. If invalid, userDownloadsDirectory
+ * will fallback on defaultDownloadsDirectory.
+ *
+ * browser.download.dir -
+ * A local path the user may have selected at some point
+ * where downloaded files are saved. The use of which is
+ * enabled when folderList equals 2.
+ */
+ readonly attribute nsIFile userDownloadsDirectory;
+};
+
+
diff --git a/components/downloads/public/nsIDownloadManagerUI.idl b/components/downloads/public/nsIDownloadManagerUI.idl
new file mode 100644
index 000000000..b5ceff5b0
--- /dev/null
+++ b/components/downloads/public/nsIDownloadManagerUI.idl
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+interface nsIInterfaceRequestor;
+interface nsIDownload;
+
+[scriptable, uuid(0c76d4cf-0b06-4c1a-9bea-520c7bbdba99)]
+interface nsIDownloadManagerUI : nsISupports {
+ /**
+ * The reason that should be passed when the user requests to show the
+ * download manager's UI.
+ */
+ const short REASON_USER_INTERACTED = 0;
+
+ /**
+ * The reason that should be passed to the show method when we are displaying
+ * the UI because a new download is being added to it.
+ */
+ const short REASON_NEW_DOWNLOAD = 1;
+
+ /**
+ * Shows the Download Manager's UI to the user.
+ *
+ * @param [optional] aWindowContext
+ * The parent window context to show the UI.
+ * @param [optional] aDownload
+ * The download to be preselected upon opening.
+ * @param [optional] aReason
+ * The reason to show the download manager's UI. This defaults to
+ * REASON_USER_INTERACTED, and should be one of the previously listed
+ * constants.
+ * @param [optional] aUsePrivateUI
+ * Pass true as this argument to hint to the implementation that it
+ * should only display private downloads in the UI, if possible.
+ */
+ void show([optional] in nsIInterfaceRequestor aWindowContext,
+ [optional] in nsIDownload aDownload,
+ [optional] in short aReason,
+ [optional] in boolean aUsePrivateUI);
+
+ /**
+ * Indicates if the UI is visible or not.
+ */
+ readonly attribute boolean visible;
+
+ /**
+ * Brings attention to the UI if it is already visible
+ *
+ * @throws NS_ERROR_UNEXPECTED if the UI is not visible.
+ */
+ void getAttention();
+};
+
diff --git a/components/downloads/public/nsIDownloadProgressListener.idl b/components/downloads/public/nsIDownloadProgressListener.idl
new file mode 100644
index 000000000..e406f64d6
--- /dev/null
+++ b/components/downloads/public/nsIDownloadProgressListener.idl
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* A minimally extended progress listener used by download manager
+ * to update its default UI. This is implemented in nsDownloadProgressListener.js.
+ * See nsIWebProgressListener for documentation, and use its constants. This isn't
+ * too pretty, but the alternative is having this extend nsIWebProgressListener and
+ * adding an |item| attribute, which would mean a separate nsIDownloadProgressListener
+ * for every nsIDownloadItem, which is a waste...
+ */
+
+#include "nsISupports.idl"
+
+interface nsIWebProgress;
+interface nsIRequest;
+interface nsIURI;
+interface nsIDownload;
+interface nsIDOMDocument;
+
+[scriptable, uuid(7acb07ea-cac2-4c15-a3ad-23aaa789ed51)]
+interface nsIDownloadProgressListener : nsISupports {
+
+ /**
+ * document
+ * The document of the download manager frontend.
+ */
+
+ attribute nsIDOMDocument document;
+
+ /**
+ * Dispatched whenever the state of the download changes.
+ *
+ * @param aState The previous download sate.
+ * @param aDownload The download object.
+ * @see nsIDownloadManager for download states.
+ */
+ void onDownloadStateChange(in short aState, in nsIDownload aDownload);
+
+ void onStateChange(in nsIWebProgress aWebProgress,
+ in nsIRequest aRequest,
+ in unsigned long aStateFlags,
+ in nsresult aStatus,
+ in nsIDownload aDownload);
+
+ void onProgressChange(in nsIWebProgress aWebProgress,
+ in nsIRequest aRequest,
+ in long long aCurSelfProgress,
+ in long long aMaxSelfProgress,
+ in long long aCurTotalProgress,
+ in long long aMaxTotalProgress,
+ in nsIDownload aDownload);
+
+ void onSecurityChange(in nsIWebProgress aWebProgress,
+ in nsIRequest aRequest,
+ in unsigned long aState,
+ in nsIDownload aDownload);
+
+};
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]);