diff options
Diffstat (limited to 'components/downloads')
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, ¬hing, &button); + + aCancelDownloads->SetData(button == 1); + } +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsDownload + +NS_IMPL_CLASSINFO(nsDownload, nullptr, 0, NS_DOWNLOAD_CID) +NS_IMPL_ISUPPORTS_CI( + nsDownload + , nsIDownload + , nsITransfer + , nsIWebProgressListener + , nsIWebProgressListener2 +) + +nsDownload::nsDownload() : mDownloadState(nsIDownloadManager::DOWNLOAD_NOTSTARTED), + mID(0), + mPercentComplete(0), + mCurrBytes(0), + mMaxBytes(-1), + mStartTime(0), + mLastUpdate(PR_Now() - (uint32_t)gUpdateInterval), + mResumedAt(-1), + mSpeed(0), + mHasMultipleFiles(false), + mPrivate(false), + mAutoResume(DONT_RESUME) +{ +} + +nsDownload::~nsDownload() +{ +} + +NS_IMETHODIMP nsDownload::SetSha256Hash(const nsACString& aHash) { + MOZ_ASSERT(NS_IsMainThread(), "Must call SetSha256Hash on main thread"); + // This will be used later to query the application reputation service. + mHash = aHash; + return NS_OK; +} + +NS_IMETHODIMP nsDownload::SetSignatureInfo(nsIArray* aSignatureInfo) { + MOZ_ASSERT(NS_IsMainThread(), "Must call SetSignatureInfo on main thread"); + // This will be used later to query the application reputation service. + mSignatureInfo = aSignatureInfo; + return NS_OK; +} + +NS_IMETHODIMP nsDownload::SetRedirects(nsIArray* aRedirects) { + MOZ_ASSERT(NS_IsMainThread(), "Must call SetRedirects on main thread"); + // This will be used later to query the application reputation service. + mRedirects = aRedirects; + return NS_OK; +} + +#ifdef MOZ_ENABLE_GIO +static void gio_set_metadata_done(GObject *source_obj, GAsyncResult *res, gpointer user_data) +{ + GError *err = nullptr; + g_file_set_attributes_finish(G_FILE(source_obj), res, nullptr, &err); + if (err) { +#ifdef DEBUG + NS_DebugBreak(NS_DEBUG_WARNING, "Set file metadata failed: ", err->message, __FILE__, __LINE__); +#endif + g_error_free(err); + } +} +#endif + +nsresult +nsDownload::SetState(DownloadState aState) +{ + NS_ASSERTION(mDownloadState != aState, + "Trying to set the download state to what it already is set to!"); + + int16_t oldState = mDownloadState; + mDownloadState = aState; + + // We don't want to lose access to our member variables + RefPtr<nsDownload> kungFuDeathGrip = this; + + // When the state changed listener is dispatched, queries to the database and + // the download manager api should reflect what the nsIDownload object would + // return. So, if a download is done (finished, canceled, etc.), it should + // first be removed from the current downloads. We will also have to update + // the database *before* notifying listeners. At this point, you can safely + // dispatch to the observers as well. + switch (aState) { + case nsIDownloadManager::DOWNLOAD_BLOCKED_PARENTAL: + case nsIDownloadManager::DOWNLOAD_BLOCKED_POLICY: + case nsIDownloadManager::DOWNLOAD_DIRTY: + case nsIDownloadManager::DOWNLOAD_CANCELED: + case nsIDownloadManager::DOWNLOAD_FAILED: + + // Transfers are finished, so break the reference cycle + Finalize(); + break; +#ifdef DOWNLOAD_SCANNER + case nsIDownloadManager::DOWNLOAD_SCANNING: + { + nsresult rv = mDownloadManager->mScanner ? mDownloadManager->mScanner->ScanDownload(this) : NS_ERROR_NOT_INITIALIZED; + // If we failed, then fall through to 'download finished' + if (NS_SUCCEEDED(rv)) + break; + mDownloadState = aState = nsIDownloadManager::DOWNLOAD_FINISHED; + } +#endif + case nsIDownloadManager::DOWNLOAD_FINISHED: + { + nsresult rv = ExecuteDesiredAction(); + if (NS_FAILED(rv)) { + // We've failed to execute the desired action. As a result, we should + // fail the download so the user can try again. + (void)FailDownload(rv, nullptr); + return rv; + } + + // Now that we're done with handling the download, clean it up + Finalize(); + + nsCOMPtr<nsIPrefBranch> pref(do_GetService(NS_PREFSERVICE_CONTRACTID)); + + // Master pref to control this function. + bool showTaskbarAlert = true; + if (pref) + pref->GetBoolPref(PREF_BDM_SHOWALERTONCOMPLETE, &showTaskbarAlert); + + if (showTaskbarAlert) { + int32_t alertInterval = 2000; + if (pref) + pref->GetIntPref(PREF_BDM_SHOWALERTINTERVAL, &alertInterval); + + int64_t alertIntervalUSec = alertInterval * PR_USEC_PER_MSEC; + int64_t goat = PR_Now() - mStartTime; + showTaskbarAlert = goat > alertIntervalUSec; + + int32_t size = mPrivate ? + mDownloadManager->mCurrentPrivateDownloads.Count() : + mDownloadManager->mCurrentDownloads.Count(); + if (showTaskbarAlert && size == 0) { + nsCOMPtr<nsIAlertsService> alerts = + do_GetService("@mozilla.org/alerts-service;1"); + if (alerts) { + nsXPIDLString title, message; + + mDownloadManager->mBundle->GetStringFromName( + u"downloadsCompleteTitle", + getter_Copies(title)); + mDownloadManager->mBundle->GetStringFromName( + u"downloadsCompleteMsg", + getter_Copies(message)); + + bool removeWhenDone = + mDownloadManager->GetRetentionBehavior() == 0; + + // If downloads are automatically removed per the user's + // retention policy, there's no reason to make the text clickable + // because if it is, they'll click open the download manager and + // the items they downloaded will have been removed. + alerts->ShowAlertNotification( + NS_LITERAL_STRING(DOWNLOAD_MANAGER_ALERT_ICON), title, + message, !removeWhenDone, + mPrivate ? NS_LITERAL_STRING("private") : NS_LITERAL_STRING("non-private"), + mDownloadManager, EmptyString(), NS_LITERAL_STRING("auto"), + EmptyString(), EmptyString(), nullptr, mPrivate, + false /* requireInteraction */); + } + } + } + +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) + nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(mTarget); + nsCOMPtr<nsIFile> file; + nsAutoString path; + + if (fileURL && + NS_SUCCEEDED(fileURL->GetFile(getter_AddRefs(file))) && + file && + NS_SUCCEEDED(file->GetPath(path))) { + + // On Windows and Gtk, add the download to the system's "recent documents" + // list, with a pref to disable. + { + bool addToRecentDocs = true; + if (pref) + pref->GetBoolPref(PREF_BDM_ADDTORECENTDOCS, &addToRecentDocs); + if (addToRecentDocs && !mPrivate) { +#ifdef XP_WIN + ::SHAddToRecentDocs(SHARD_PATHW, path.get()); +#elif defined(MOZ_WIDGET_GTK) + GtkRecentManager* manager = gtk_recent_manager_get_default(); + + gchar* uri = g_filename_to_uri(NS_ConvertUTF16toUTF8(path).get(), + nullptr, nullptr); + if (uri) { + gtk_recent_manager_add_item(manager, uri); + g_free(uri); + } +#endif + } +#ifdef MOZ_ENABLE_GIO + // Use GIO to store the source URI for later display in the file manager. + GFile* gio_file = g_file_new_for_path(NS_ConvertUTF16toUTF8(path).get()); + nsCString source_uri; + rv = mSource->GetSpec(source_uri); + NS_ENSURE_SUCCESS(rv, rv); + GFileInfo *file_info = g_file_info_new(); + g_file_info_set_attribute_string(file_info, "metadata::download-uri", source_uri.get()); + g_file_set_attributes_async(gio_file, + file_info, + G_FILE_QUERY_INFO_NONE, + G_PRIORITY_DEFAULT, + nullptr, gio_set_metadata_done, nullptr); + g_object_unref(file_info); + g_object_unref(gio_file); +#endif + } + } + +#ifdef XP_WIN + // Adjust file attributes so that by default, new files are indexed + // by desktop search services. Skip off those that land in the temp + // folder. + nsCOMPtr<nsIFile> tempDir, fileDir; + rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(tempDir)); + NS_ENSURE_SUCCESS(rv, rv); + (void)file->GetParent(getter_AddRefs(fileDir)); + + bool isTemp = false; + if (fileDir) + (void)fileDir->Equals(tempDir, &isTemp); + + nsCOMPtr<nsILocalFileWin> localFileWin(do_QueryInterface(file)); + if (!isTemp && localFileWin) + (void)localFileWin->SetFileAttributesWin(nsILocalFileWin::WFA_SEARCH_INDEXED); +#endif + +#endif + // Now remove the download if the user's retention policy is "Remove when Done" + if (mDownloadManager->GetRetentionBehavior() == 0) + mDownloadManager->RemoveDownload(mGUID); + } + break; + default: + break; + } + + // Before notifying the listener, we must update the database so that calls + // to it work out properly. + nsresult rv = UpdateDB(); + NS_ENSURE_SUCCESS(rv, rv); + + mDownloadManager->NotifyListenersOnDownloadStateChange(oldState, this); + + switch (mDownloadState) { + case nsIDownloadManager::DOWNLOAD_DOWNLOADING: + // Only send the dl-start event to downloads that are actually starting. + if (oldState == nsIDownloadManager::DOWNLOAD_QUEUED) { + if (!mPrivate) + mDownloadManager->SendEvent(this, "dl-start"); + } + break; + case nsIDownloadManager::DOWNLOAD_FAILED: + if (!mPrivate) + mDownloadManager->SendEvent(this, "dl-failed"); + break; + case nsIDownloadManager::DOWNLOAD_SCANNING: + if (!mPrivate) + mDownloadManager->SendEvent(this, "dl-scanning"); + break; + case nsIDownloadManager::DOWNLOAD_FINISHED: + if (!mPrivate) + mDownloadManager->SendEvent(this, "dl-done"); + break; + case nsIDownloadManager::DOWNLOAD_BLOCKED_PARENTAL: + case nsIDownloadManager::DOWNLOAD_BLOCKED_POLICY: + if (!mPrivate) + mDownloadManager->SendEvent(this, "dl-blocked"); + break; + case nsIDownloadManager::DOWNLOAD_DIRTY: + if (!mPrivate) + mDownloadManager->SendEvent(this, "dl-dirty"); + break; + case nsIDownloadManager::DOWNLOAD_CANCELED: + if (!mPrivate) + mDownloadManager->SendEvent(this, "dl-cancel"); + break; + default: + break; + } + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsIWebProgressListener2 + +NS_IMETHODIMP +nsDownload::OnProgressChange64(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, + int64_t aCurSelfProgress, + int64_t aMaxSelfProgress, + int64_t aCurTotalProgress, + int64_t aMaxTotalProgress) +{ + if (!mRequest) + mRequest = aRequest; // used for pause/resume + + if (mDownloadState == nsIDownloadManager::DOWNLOAD_QUEUED) { + // Obtain the referrer + nsresult rv; + nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest)); + nsCOMPtr<nsIURI> referrer = mReferrer; + if (channel) + (void)NS_GetReferrerFromChannel(channel, getter_AddRefs(mReferrer)); + + // Restore the original referrer if the new one isn't useful + if (!mReferrer) + mReferrer = referrer; + + // If we have a MIME info, we know that exthandler has already added this to + // the history, but if we do not, we'll have to add it ourselves. + if (!mMIMEInfo && !mPrivate) { + nsCOMPtr<nsIDownloadHistory> dh = + do_GetService(NS_DOWNLOADHISTORY_CONTRACTID); + if (dh) + (void)dh->AddDownload(mSource, mReferrer, mStartTime, mTarget); + } + + // Fetch the entityID, but if we can't get it, don't panic (non-resumable) + nsCOMPtr<nsIResumableChannel> resumableChannel(do_QueryInterface(aRequest)); + if (resumableChannel) + (void)resumableChannel->GetEntityID(mEntityID); + + // Before we update the state and dispatch state notifications, we want to + // ensure that we have the correct state for this download with regards to + // its percent completion and size. + SetProgressBytes(0, aMaxTotalProgress); + + // Update the state and the database + rv = SetState(nsIDownloadManager::DOWNLOAD_DOWNLOADING); + NS_ENSURE_SUCCESS(rv, rv); + } + + // filter notifications since they come in so frequently + PRTime now = PR_Now(); + PRIntervalTime delta = now - mLastUpdate; + if (delta < gUpdateInterval) + return NS_OK; + + mLastUpdate = now; + + // Calculate the speed using the elapsed delta time and bytes downloaded + // during that time for more accuracy. + double elapsedSecs = double(delta) / PR_USEC_PER_SEC; + if (elapsedSecs > 0) { + double speed = double(aCurTotalProgress - mCurrBytes) / elapsedSecs; + if (mCurrBytes == 0) { + mSpeed = speed; + } else { + // Calculate 'smoothed average' of 10 readings. + mSpeed = mSpeed * 0.9 + speed * 0.1; + } + } + + SetProgressBytes(aCurTotalProgress, aMaxTotalProgress); + + // Report to the listener our real sizes + int64_t currBytes, maxBytes; + (void)GetAmountTransferred(&currBytes); + (void)GetSize(&maxBytes); + mDownloadManager->NotifyListenersOnProgressChange( + aWebProgress, aRequest, currBytes, maxBytes, currBytes, maxBytes, this); + + // If the maximums are different, then there must be more than one file + if (aMaxSelfProgress != aMaxTotalProgress) + mHasMultipleFiles = true; + + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::OnRefreshAttempted(nsIWebProgress *aWebProgress, + nsIURI *aUri, + int32_t aDelay, + bool aSameUri, + bool *allowRefresh) +{ + *allowRefresh = true; + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsIWebProgressListener + +NS_IMETHODIMP +nsDownload::OnProgressChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, + int32_t aCurSelfProgress, + int32_t aMaxSelfProgress, + int32_t aCurTotalProgress, + int32_t aMaxTotalProgress) +{ + return OnProgressChange64(aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress); +} + +NS_IMETHODIMP +nsDownload::OnLocationChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, nsIURI *aLocation, + uint32_t aFlags) +{ + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::OnStatusChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, nsresult aStatus, + const char16_t *aMessage) +{ + if (NS_FAILED(aStatus)) + return FailDownload(aStatus, aMessage); + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::OnStateChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, uint32_t aStateFlags, + nsresult aStatus) +{ + MOZ_ASSERT(NS_IsMainThread(), "Must call OnStateChange in main thread"); + + // We don't want to lose access to our member variables + RefPtr<nsDownload> kungFuDeathGrip = this; + + // Check if we're starting a request; the NETWORK flag is necessary to not + // pick up the START of *each* file but only for the whole request + if ((aStateFlags & STATE_START) && (aStateFlags & STATE_IS_NETWORK)) { + nsresult rv; + nsCOMPtr<nsIHttpChannel> channel = do_QueryInterface(aRequest, &rv); + if (NS_SUCCEEDED(rv)) { + uint32_t status; + rv = channel->GetResponseStatus(&status); + // HTTP 450 - Blocked by parental control proxies + if (NS_SUCCEEDED(rv) && status == 450) { + // Cancel using the provided object + (void)Cancel(); + + // Fail the download + (void)SetState(nsIDownloadManager::DOWNLOAD_BLOCKED_PARENTAL); + } + } + } else if ((aStateFlags & STATE_STOP) && (aStateFlags & STATE_IS_NETWORK) && + IsFinishable()) { + // We got both STOP and NETWORK so that means the whole request is done + // (and not just a single file if there are multiple files) + if (NS_SUCCEEDED(aStatus)) { + // We can't completely trust the bytes we've added up because we might be + // missing on some/all of the progress updates (especially from cache). + // Our best bet is the file itself, but if for some reason it's gone or + // if we have multiple files, the next best is what we've calculated. + int64_t fileSize; + nsCOMPtr<nsIFile> file; + // We need a nsIFile clone to deal with file size caching issues. :( + nsCOMPtr<nsIFile> clone; + if (!mHasMultipleFiles && + NS_SUCCEEDED(GetTargetFile(getter_AddRefs(file))) && + NS_SUCCEEDED(file->Clone(getter_AddRefs(clone))) && + NS_SUCCEEDED(clone->GetFileSize(&fileSize)) && fileSize > 0) { + mCurrBytes = mMaxBytes = fileSize; + + // If we resumed, keep the fact that we did and fix size calculations + if (WasResumed()) + mResumedAt = 0; + } else if (mMaxBytes == -1) { + mMaxBytes = mCurrBytes; + } else { + mCurrBytes = mMaxBytes; + } + + mPercentComplete = 100; + mLastUpdate = PR_Now(); + +#ifdef DOWNLOAD_SCANNER + bool scan = true; + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (prefs) + (void)prefs->GetBoolPref(PREF_BDM_SCANWHENDONE, &scan); + + if (scan) + (void)SetState(nsIDownloadManager::DOWNLOAD_SCANNING); + else + (void)SetState(nsIDownloadManager::DOWNLOAD_FINISHED); +#else + (void)SetState(nsIDownloadManager::DOWNLOAD_FINISHED); +#endif + } else if (aStatus != NS_BINDING_ABORTED) { + // We failed for some unknown reason -- fail with a generic message + (void)FailDownload(aStatus, nullptr); + } + } + + mDownloadManager->NotifyListenersOnStateChange(aWebProgress, aRequest, + aStateFlags, aStatus, this); + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::OnSecurityChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, uint32_t aState) +{ + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsIDownload + +NS_IMETHODIMP +nsDownload::Init(nsIURI *aSource, + nsIURI *aTarget, + const nsAString& aDisplayName, + nsIMIMEInfo *aMIMEInfo, + PRTime aStartTime, + nsIFile *aTempFile, + nsICancelable *aCancelable, + bool aIsPrivate) +{ + NS_WARNING("Huh...how did we get here?!"); + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetState(int16_t *aState) +{ + *aState = mDownloadState; + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetDisplayName(nsAString &aDisplayName) +{ + aDisplayName = mDisplayName; + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetCancelable(nsICancelable **aCancelable) +{ + *aCancelable = mCancelable; + NS_IF_ADDREF(*aCancelable); + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetTarget(nsIURI **aTarget) +{ + *aTarget = mTarget; + NS_IF_ADDREF(*aTarget); + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetSource(nsIURI **aSource) +{ + *aSource = mSource; + NS_IF_ADDREF(*aSource); + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetStartTime(int64_t *aStartTime) +{ + *aStartTime = mStartTime; + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetPercentComplete(int32_t *aPercentComplete) +{ + *aPercentComplete = mPercentComplete; + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetAmountTransferred(int64_t *aAmountTransferred) +{ + *aAmountTransferred = mCurrBytes + (WasResumed() ? mResumedAt : 0); + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetSize(int64_t *aSize) +{ + *aSize = mMaxBytes + (WasResumed() && mMaxBytes != -1 ? mResumedAt : 0); + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetMIMEInfo(nsIMIMEInfo **aMIMEInfo) +{ + *aMIMEInfo = mMIMEInfo; + NS_IF_ADDREF(*aMIMEInfo); + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetTargetFile(nsIFile **aTargetFile) +{ + nsresult rv; + + nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(mTarget, &rv); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIFile> file; + rv = fileURL->GetFile(getter_AddRefs(file)); + if (NS_FAILED(rv)) { + return rv; + } + + file.forget(aTargetFile); + return rv; +} + +NS_IMETHODIMP +nsDownload::GetSpeed(double *aSpeed) +{ + *aSpeed = mSpeed; + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetId(uint32_t *aId) +{ + if (mPrivate) { + return NS_ERROR_NOT_AVAILABLE; + } + *aId = mID; + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetGuid(nsACString &aGUID) +{ + aGUID = mGUID; + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetReferrer(nsIURI **referrer) +{ + NS_IF_ADDREF(*referrer = mReferrer); + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetResumable(bool *resumable) +{ + *resumable = IsResumable(); + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::GetIsPrivate(bool *isPrivate) +{ + *isPrivate = mPrivate; + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsDownload Helper Functions + +void +nsDownload::Finalize() +{ + // We're stopping, so break the cycle we created at download start + mCancelable = nullptr; + + // Reset values that aren't needed anymore, so the DB can be updated as well + mEntityID.Truncate(); + mTempFile = nullptr; + + // Remove ourself from the active downloads + nsCOMArray<nsDownload>& currentDownloads = mPrivate ? + mDownloadManager->mCurrentPrivateDownloads : + mDownloadManager->mCurrentDownloads; + (void)currentDownloads.RemoveObject(this); + + // Make sure we do not automatically resume + mAutoResume = DONT_RESUME; +} + +nsresult +nsDownload::ExecuteDesiredAction() +{ + // nsExternalHelperAppHandler is the only caller of AddDownload that sets a + // tempfile parameter. In this case, execute the desired action according to + // the saved mime info. + if (!mTempFile) { + return NS_OK; + } + + // We need to bail if for some reason the temp file got removed + bool fileExists; + if (NS_FAILED(mTempFile->Exists(&fileExists)) || !fileExists) + return NS_ERROR_FILE_NOT_FOUND; + + // Assume an unknown action is save to disk + nsHandlerInfoAction action = nsIMIMEInfo::saveToDisk; + if (mMIMEInfo) { + nsresult rv = mMIMEInfo->GetPreferredAction(&action); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsresult rv = NS_OK; + switch (action) { + case nsIMIMEInfo::saveToDisk: + // Move the file to the proper location + rv = MoveTempToTarget(); + if (NS_SUCCEEDED(rv)) { + rv = FixTargetPermissions(); + } + break; + case nsIMIMEInfo::useHelperApp: + case nsIMIMEInfo::useSystemDefault: + // For these cases we have to move the file to the target location and + // open with the appropriate application + rv = OpenWithApplication(); + break; + default: + break; + } + + return rv; +} + +nsresult +nsDownload::FixTargetPermissions() +{ + nsCOMPtr<nsIFile> target; + nsresult rv = GetTargetFile(getter_AddRefs(target)); + NS_ENSURE_SUCCESS(rv, rv); + + // Set perms according to umask. + nsCOMPtr<nsIPropertyBag2> infoService = + do_GetService("@mozilla.org/system-info;1"); + uint32_t gUserUmask = 0; + rv = infoService->GetPropertyAsUint32(NS_LITERAL_STRING("umask"), + &gUserUmask); + if (NS_SUCCEEDED(rv)) { + (void)target->SetPermissions(0666 & ~gUserUmask); + } + return NS_OK; +} + +nsresult +nsDownload::MoveTempToTarget() +{ + nsCOMPtr<nsIFile> target; + nsresult rv = GetTargetFile(getter_AddRefs(target)); + NS_ENSURE_SUCCESS(rv, rv); + + // MoveTo will fail if the file already exists, but we've already obtained + // confirmation from the user that this is OK, so remove it if it exists. + bool fileExists; + if (NS_SUCCEEDED(target->Exists(&fileExists)) && fileExists) { + rv = target->Remove(false); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Extract the new leaf name from the file location + nsAutoString fileName; + rv = target->GetLeafName(fileName); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIFile> dir; + rv = target->GetParent(getter_AddRefs(dir)); + NS_ENSURE_SUCCESS(rv, rv); + rv = mTempFile->MoveTo(dir, fileName); + return rv; +} + +nsresult +nsDownload::OpenWithApplication() +{ + // First move the temporary file to the target location + nsCOMPtr<nsIFile> target; + nsresult rv = GetTargetFile(getter_AddRefs(target)); + NS_ENSURE_SUCCESS(rv, rv); + + // Move the temporary file to the target location + rv = MoveTempToTarget(); + NS_ENSURE_SUCCESS(rv, rv); + + bool deleteTempFileOnExit; + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (!prefs || NS_FAILED(prefs->GetBoolPref(PREF_BH_DELETETEMPFILEONEXIT, + &deleteTempFileOnExit))) { + // No prefservice or no pref set; use default value + // Some users have been very verbal about temp files being deleted on + // app exit - they don't like it - but we'll continue to do this on + // all platforms for now. + deleteTempFileOnExit = true; + } + + // Always schedule files to be deleted at the end of the private browsing + // mode, regardless of the value of the pref. + if (deleteTempFileOnExit || mPrivate) { + + // Make the tmp file readonly so users won't lose changes. + target->SetPermissions(0400); + + // Use the ExternalHelperAppService to push the temporary file to the list + // of files to be deleted on exit. + nsCOMPtr<nsPIExternalAppLauncher> appLauncher(do_GetService + (NS_EXTERNALHELPERAPPSERVICE_CONTRACTID)); + + // Even if we are unable to get this service we return the result + // of LaunchWithFile() which makes more sense. + if (appLauncher) { + if (mPrivate) { + (void)appLauncher->DeleteTemporaryPrivateFileWhenPossible(target); + } else { + (void)appLauncher->DeleteTemporaryFileOnExit(target); + } + } + } + + return mMIMEInfo->LaunchWithFile(target); +} + +void +nsDownload::SetStartTime(int64_t aStartTime) +{ + mStartTime = aStartTime; + mLastUpdate = aStartTime; +} + +void +nsDownload::SetProgressBytes(int64_t aCurrBytes, int64_t aMaxBytes) +{ + mCurrBytes = aCurrBytes; + mMaxBytes = aMaxBytes; + + // Get the real bytes that include resume position + int64_t currBytes, maxBytes; + (void)GetAmountTransferred(&currBytes); + (void)GetSize(&maxBytes); + + if (currBytes == maxBytes) + mPercentComplete = 100; + else if (maxBytes <= 0) + mPercentComplete = -1; + else + mPercentComplete = (int32_t)((double)currBytes / maxBytes * 100 + .5); +} + +NS_IMETHODIMP +nsDownload::Pause() +{ + if (!IsResumable()) + return NS_ERROR_UNEXPECTED; + + nsresult rv = CancelTransfer(); + NS_ENSURE_SUCCESS(rv, rv); + + return SetState(nsIDownloadManager::DOWNLOAD_PAUSED); +} + +nsresult +nsDownload::CancelTransfer() +{ + nsresult rv = NS_OK; + if (mCancelable) { + rv = mCancelable->Cancel(NS_BINDING_ABORTED); + // we're done with this, so break the cycle + mCancelable = nullptr; + } + + return rv; +} + +NS_IMETHODIMP +nsDownload::Cancel() +{ + // Don't cancel if download is already finished + if (IsFinished()) + return NS_OK; + + // Have the download cancel its connection + (void)CancelTransfer(); + + // Dump the temp file because we know we don't need the file anymore. The + // underlying transfer creating the file doesn't delete the file because it + // can't distinguish between a pause that cancels the transfer or a real + // cancel. + if (mTempFile) { + bool exists; + mTempFile->Exists(&exists); + if (exists) + mTempFile->Remove(false); + } + + nsCOMPtr<nsIFile> file; + if (NS_SUCCEEDED(GetTargetFile(getter_AddRefs(file)))) + { + bool exists; + file->Exists(&exists); + if (exists) + file->Remove(false); + } + + nsresult rv = SetState(nsIDownloadManager::DOWNLOAD_CANCELED); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +nsDownload::Resume() +{ + if (!IsPaused() || !IsResumable()) + return NS_ERROR_UNEXPECTED; + + nsresult rv; + nsCOMPtr<nsIWebBrowserPersist> wbp = + do_CreateInstance("@mozilla.org/embedding/browser/nsWebBrowserPersist;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = wbp->SetPersistFlags(nsIWebBrowserPersist::PERSIST_FLAGS_APPEND_TO_FILE | + nsIWebBrowserPersist::PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION); + NS_ENSURE_SUCCESS(rv, rv); + + // Create a new channel for the source URI + nsCOMPtr<nsIChannel> channel; + nsCOMPtr<nsIInterfaceRequestor> ir(do_QueryInterface(wbp)); + rv = NS_NewChannel(getter_AddRefs(channel), + mSource, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + nsIContentPolicy::TYPE_OTHER, + nullptr, // aLoadGroup + ir); + + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrivateBrowsingChannel> pbChannel = do_QueryInterface(channel); + if (pbChannel) { + pbChannel->SetPrivate(mPrivate); + } + + // Make sure we can get a file, either the temporary or the real target, for + // both purposes of file size and a target to write to + nsCOMPtr<nsIFile> targetLocalFile(mTempFile); + if (!targetLocalFile) { + rv = GetTargetFile(getter_AddRefs(targetLocalFile)); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Get the file size to be used as an offset, but if anything goes wrong + // along the way, we'll silently restart at 0. + int64_t fileSize; + // We need a nsIFile clone to deal with file size caching issues. :( + nsCOMPtr<nsIFile> clone; + if (NS_FAILED(targetLocalFile->Clone(getter_AddRefs(clone))) || + NS_FAILED(clone->GetFileSize(&fileSize))) + fileSize = 0; + + // Set the channel to resume at the right position along with the entityID + nsCOMPtr<nsIResumableChannel> resumableChannel(do_QueryInterface(channel)); + if (!resumableChannel) + return NS_ERROR_UNEXPECTED; + rv = resumableChannel->ResumeAt(fileSize, mEntityID); + NS_ENSURE_SUCCESS(rv, rv); + + // If we know the max size, we know what it should be when resuming + int64_t maxBytes; + GetSize(&maxBytes); + SetProgressBytes(0, maxBytes != -1 ? maxBytes - fileSize : -1); + // Track where we resumed because progress notifications restart at 0 + mResumedAt = fileSize; + + // Set the referrer + if (mReferrer) { + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(channel)); + if (httpChannel) { + rv = httpChannel->SetReferrer(mReferrer); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // Creates a cycle that will be broken when the download finishes + mCancelable = wbp; + (void)wbp->SetProgressListener(this); + + // Save the channel using nsIWBP + rv = wbp->SaveChannel(channel, targetLocalFile); + if (NS_FAILED(rv)) { + mCancelable = nullptr; + (void)wbp->SetProgressListener(nullptr); + return rv; + } + + return SetState(nsIDownloadManager::DOWNLOAD_DOWNLOADING); +} + +NS_IMETHODIMP +nsDownload::Remove() +{ + return mDownloadManager->RemoveDownload(mGUID); +} + +NS_IMETHODIMP +nsDownload::Retry() +{ + return mDownloadManager->RetryDownload(mGUID); +} + +bool +nsDownload::IsPaused() +{ + return mDownloadState == nsIDownloadManager::DOWNLOAD_PAUSED; +} + +bool +nsDownload::IsResumable() +{ + return !mEntityID.IsEmpty(); +} + +bool +nsDownload::WasResumed() +{ + return mResumedAt != -1; +} + +bool +nsDownload::ShouldAutoResume() +{ + return mAutoResume == AUTO_RESUME; +} + +bool +nsDownload::IsFinishable() +{ + return mDownloadState == nsIDownloadManager::DOWNLOAD_NOTSTARTED || + mDownloadState == nsIDownloadManager::DOWNLOAD_QUEUED || + mDownloadState == nsIDownloadManager::DOWNLOAD_DOWNLOADING; +} + +bool +nsDownload::IsFinished() +{ + return mDownloadState == nsIDownloadManager::DOWNLOAD_FINISHED; +} + +nsresult +nsDownload::UpdateDB() +{ + NS_ASSERTION(mID, "Download ID is stored as zero. This is bad!"); + NS_ASSERTION(mDownloadManager, "Egads! We have no download manager!"); + + mozIStorageStatement *stmt = mPrivate ? + mDownloadManager->mUpdatePrivateDownloadStatement : mDownloadManager->mUpdateDownloadStatement; + + nsAutoString tempPath; + if (mTempFile) + (void)mTempFile->GetPath(tempPath); + nsresult rv = stmt->BindStringByName(NS_LITERAL_CSTRING("tempPath"), tempPath); + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("startTime"), mStartTime); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("endTime"), mLastUpdate); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("state"), mDownloadState); + NS_ENSURE_SUCCESS(rv, rv); + + if (mReferrer) { + nsAutoCString referrer; + rv = mReferrer->GetSpec(referrer); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("referrer"), referrer); + } else { + rv = stmt->BindNullByName(NS_LITERAL_CSTRING("referrer")); + } + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("entityID"), mEntityID); + NS_ENSURE_SUCCESS(rv, rv); + + int64_t currBytes; + (void)GetAmountTransferred(&currBytes); + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("currBytes"), currBytes); + NS_ENSURE_SUCCESS(rv, rv); + + int64_t maxBytes; + (void)GetSize(&maxBytes); + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("maxBytes"), maxBytes); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("autoResume"), mAutoResume); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), mID); + NS_ENSURE_SUCCESS(rv, rv); + + return stmt->Execute(); +} + +nsresult +nsDownload::FailDownload(nsresult aStatus, const char16_t *aMessage) +{ + // Grab the bundle before potentially losing our member variables + nsCOMPtr<nsIStringBundle> bundle = mDownloadManager->mBundle; + + (void)SetState(nsIDownloadManager::DOWNLOAD_FAILED); + + // Get title for alert. + nsXPIDLString title; + nsresult rv = bundle->GetStringFromName( + u"downloadErrorAlertTitle", getter_Copies(title)); + NS_ENSURE_SUCCESS(rv, rv); + + // Get a generic message if we weren't supplied one + nsXPIDLString message; + message = aMessage; + if (message.IsEmpty()) { + rv = bundle->GetStringFromName( + u"downloadErrorGeneric", getter_Copies(message)); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Get Download Manager window to be parent of alert + nsCOMPtr<nsIWindowMediator> wm = + do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<mozIDOMWindowProxy> dmWindow; + rv = wm->GetMostRecentWindow(u"Download:Manager", + getter_AddRefs(dmWindow)); + NS_ENSURE_SUCCESS(rv, rv); + + // Show alert + nsCOMPtr<nsIPromptService> prompter = + do_GetService("@mozilla.org/embedcomp/prompt-service;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + return prompter->Alert(dmWindow, title, message); +} diff --git a/components/downloads/src/nsDownloadManager.h b/components/downloads/src/nsDownloadManager.h new file mode 100644 index 000000000..566e3560a --- /dev/null +++ b/components/downloads/src/nsDownloadManager.h @@ -0,0 +1,454 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef downloadmanager___h___ +#define downloadmanager___h___ + +#if defined(XP_WIN) +#define DOWNLOAD_SCANNER +#endif + +#include "nsIDownload.h" +#include "nsIDownloadManager.h" +#include "nsIDownloadProgressListener.h" +#include "nsIFile.h" +#include "nsIMIMEInfo.h" +#include "nsINavHistoryService.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIStringBundle.h" +#include "nsISupportsPrimitives.h" +#include "nsWeakReference.h" +#include "nsITimer.h" +#include "nsString.h" + +#include "mozIDOMWindow.h" +#include "mozStorageHelper.h" +#include "nsAutoPtr.h" +#include "nsCOMArray.h" + +typedef int16_t DownloadState; +typedef int16_t DownloadType; + +class nsIArray; +class nsDownload; + +#ifdef DOWNLOAD_SCANNER +#include "nsDownloadScanner.h" +#endif + +class nsDownloadManager final : public nsIDownloadManager, + public nsINavHistoryObserver, + public nsIObserver, + public nsSupportsWeakReference +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIDOWNLOADMANAGER + NS_DECL_NSINAVHISTORYOBSERVER + NS_DECL_NSIOBSERVER + + nsresult Init(); + + static nsDownloadManager *GetSingleton(); + + nsDownloadManager() +#ifdef DOWNLOAD_SCANNER + : mScanner(nullptr) +#endif + { + } + +protected: + virtual ~nsDownloadManager(); + + nsresult InitDB(); + nsresult InitFileDB(); + void CloseAllDBs(); + void CloseDB(mozIStorageConnection* aDBConn, + mozIStorageStatement* aUpdateStmt, + mozIStorageStatement* aGetIdsStmt); + nsresult InitPrivateDB(); + already_AddRefed<mozIStorageConnection> GetFileDBConnection(nsIFile *dbFile) const; + already_AddRefed<mozIStorageConnection> GetPrivateDBConnection() const; + nsresult CreateTable(mozIStorageConnection* aDBConn); + + /** + * Fix up the database after a crash such as dealing with previously-active + * downloads. Call this before RestoreActiveDownloads to get the downloads + * fixed here to be auto-resumed. + */ + nsresult RestoreDatabaseState(); + + /** + * Paused downloads that survive across sessions are considered active, so + * rebuild the list of these downloads. + */ + nsresult RestoreActiveDownloads(); + + nsresult GetDownloadFromDB(const nsACString& aGUID, nsDownload **retVal); + nsresult GetDownloadFromDB(uint32_t aID, nsDownload **retVal); + nsresult GetDownloadFromDB(mozIStorageConnection* aDBConn, + mozIStorageStatement* stmt, + nsDownload **retVal); + + /** + * Specially track the active downloads so that we don't need to check + * every download to see if they're in progress. + */ + nsresult AddToCurrentDownloads(nsDownload *aDl); + + void SendEvent(nsDownload *aDownload, const char *aTopic); + + /** + * Adds a download with the specified information to the DB. + * + * @return The id of the download, or 0 if there was an error. + */ + int64_t AddDownloadToDB(const nsAString &aName, + const nsACString &aSource, + const nsACString &aTarget, + const nsAString &aTempPath, + int64_t aStartTime, + int64_t aEndTime, + const nsACString &aMimeType, + const nsACString &aPreferredApp, + nsHandlerInfoAction aPreferredAction, + bool aPrivate, + nsACString &aNewGUID); + + void NotifyListenersOnDownloadStateChange(int16_t aOldState, + nsDownload *aDownload); + void NotifyListenersOnProgressChange(nsIWebProgress *aProgress, + nsIRequest *aRequest, + int64_t aCurSelfProgress, + int64_t aMaxSelfProgress, + int64_t aCurTotalProgress, + int64_t aMaxTotalProgress, + nsDownload *aDownload); + void NotifyListenersOnStateChange(nsIWebProgress *aProgress, + nsIRequest *aRequest, + uint32_t aStateFlags, + nsresult aStatus, + nsDownload *aDownload); + + nsDownload *FindDownload(const nsACString& aGUID); + nsDownload *FindDownload(uint32_t aID); + + /** + * First try to resume the download, and if that fails, retry it. + * + * @param aDl The download to resume and/or retry. + */ + nsresult ResumeRetry(nsDownload *aDl); + + /** + * Pause all active downloads and remember if they should try to auto-resume + * when the download manager starts again. + * + * @param aSetResume Indicate if the downloads that get paused should be set + * as auto-resume. + */ + nsresult PauseAllDownloads(bool aSetResume); + + /** + * Resume all paused downloads unless we're only supposed to do the automatic + * ones; in that case, try to retry them as well if resuming doesn't work. + * + * @param aResumeAll If true, all downloads will be resumed; otherwise, only + * those that are marked as auto-resume will resume. + */ + nsresult ResumeAllDownloads(bool aResumeAll); + + /** + * Stop tracking the active downloads. Only use this when we're about to quit + * the download manager because we destroy our list of active downloads to + * break the dlmgr<->dl cycle. Active downloads that aren't real-paused will + * be canceled. + */ + nsresult RemoveAllDownloads(); + + /** + * Find all downloads from a source URI and delete them. + * + * @param aURI + * The source URI to remove downloads + */ + nsresult RemoveDownloadsForURI(nsIURI *aURI); + + /** + * Callback used for resuming downloads after getting a wake notification. + * + * @param aTimer + * Timer object fired after some delay after a wake notification + * @param aClosure + * nsDownloadManager object used to resume downloads + */ + static void ResumeOnWakeCallback(nsITimer *aTimer, void *aClosure); + nsCOMPtr<nsITimer> mResumeOnWakeTimer; + + void ConfirmCancelDownloads(int32_t aCount, + nsISupportsPRBool *aCancelDownloads, + const char16_t *aTitle, + const char16_t *aCancelMessageMultiple, + const char16_t *aCancelMessageSingle, + const char16_t *aDontCancelButton); + + int32_t GetRetentionBehavior(); + + /** + * Type to indicate possible behaviors for active downloads across sessions. + * + * Possible values are: + * QUIT_AND_RESUME - downloads should be auto-resumed + * QUIT_AND_PAUSE - downloads should be paused + * QUIT_AND_CANCEL - downloads should be cancelled + */ + enum QuitBehavior { + QUIT_AND_RESUME = 0, + QUIT_AND_PAUSE = 1, + QUIT_AND_CANCEL = 2 + }; + + /** + * Indicates user-set behavior for active downloads across sessions, + * + * @return value of user-set pref for active download behavior + */ + enum QuitBehavior GetQuitBehavior(); + + void OnEnterPrivateBrowsingMode(); + void OnLeavePrivateBrowsingMode(); + + nsresult RetryDownload(const nsACString& aGUID); + nsresult RetryDownload(nsDownload* dl); + + nsresult RemoveDownload(const nsACString& aGUID); + + nsresult NotifyDownloadRemoval(nsDownload* aRemoved); + + // Virus scanner for windows +#ifdef DOWNLOAD_SCANNER +private: + nsDownloadScanner* mScanner; +#endif + +private: + nsresult CleanUp(mozIStorageConnection* aDBConn); + nsresult InitStatements(mozIStorageConnection* aDBConn, + mozIStorageStatement** aUpdateStatement, + mozIStorageStatement** aGetIdsStatement); + nsresult RemoveAllDownloads(nsCOMArray<nsDownload>& aDownloads); + nsresult PauseAllDownloads(nsCOMArray<nsDownload>& aDownloads, bool aSetResume); + nsresult ResumeAllDownloads(nsCOMArray<nsDownload>& aDownloads, bool aResumeAll); + nsresult RemoveDownloadsForURI(mozIStorageStatement* aStatement, nsIURI *aURI); + + bool mUseJSTransfer; + nsCOMArray<nsIDownloadProgressListener> mListeners; + nsCOMArray<nsIDownloadProgressListener> mPrivacyAwareListeners; + nsCOMPtr<nsIStringBundle> mBundle; + nsCOMPtr<mozIStorageConnection> mDBConn; + nsCOMPtr<mozIStorageConnection> mPrivateDBConn; + nsCOMArray<nsDownload> mCurrentDownloads; + nsCOMArray<nsDownload> mCurrentPrivateDownloads; + nsCOMPtr<nsIObserverService> mObserverService; + nsCOMPtr<mozIStorageStatement> mUpdateDownloadStatement; + nsCOMPtr<mozIStorageStatement> mUpdatePrivateDownloadStatement; + nsCOMPtr<mozIStorageStatement> mGetIdsForURIStatement; + nsCOMPtr<mozIStorageStatement> mGetPrivateIdsForURIStatement; + nsAutoPtr<mozStorageTransaction> mHistoryTransaction; + + static nsDownloadManager *gDownloadManagerService; + + friend class nsDownload; +}; + +class nsDownload final : public nsIDownload +{ +public: + NS_DECL_NSIWEBPROGRESSLISTENER + NS_DECL_NSIWEBPROGRESSLISTENER2 + NS_DECL_NSITRANSFER + NS_DECL_NSIDOWNLOAD + NS_DECL_ISUPPORTS + + nsDownload(); + + /** + * This method MUST be called when changing states on a download. It will + * notify the download listener when a change happens. This also updates the + * database, by calling UpdateDB(). + */ + nsresult SetState(DownloadState aState); + +protected: + virtual ~nsDownload(); + + /** + * Finish up the download by breaking reference cycles and clearing unneeded + * data. Additionally, the download removes itself from the download + * manager's list of current downloads. + * + * NOTE: This method removes the cycle created when starting the download, so + * make sure to use kungFuDeathGrip if you want to access member variables. + */ + void Finalize(); + + /** + * For finished resumed downloads that came in from exthandler, perform the + * action that would have been done if the download wasn't resumed. + */ + nsresult ExecuteDesiredAction(); + + /** + * Move the temporary file to the final destination by removing the existing + * dummy target and renaming the temporary. + */ + nsresult MoveTempToTarget(); + + /** + * Set the target file permissions to be appropriate. + */ + nsresult FixTargetPermissions(); + + /** + * Update the start time which also implies the last update time is the same. + */ + void SetStartTime(int64_t aStartTime); + + /** + * Update the amount of bytes transferred and max bytes; and recalculate the + * download percent. + */ + void SetProgressBytes(int64_t aCurrBytes, int64_t aMaxBytes); + + /** + * All this does is cancel the connection that the download is using. It does + * not remove it from the download manager. + */ + nsresult CancelTransfer(); + + /** + * Download is not transferring? + */ + bool IsPaused(); + + /** + * Download can continue from the middle of a transfer? + */ + bool IsResumable(); + + /** + * Download was resumed? + */ + bool WasResumed(); + + /** + * Indicates if the download should try to automatically resume or not. + */ + bool ShouldAutoResume(); + + /** + * Download is in a state to stop and complete the download? + */ + bool IsFinishable(); + + /** + * Download is totally done transferring and all? + */ + bool IsFinished(); + + /** + * Update the DB with the current state of the download including time, + * download state and other values not known when first creating the + * download DB entry. + */ + nsresult UpdateDB(); + + /** + * Fail a download because of a failure status and prompt the provided + * message or use a generic download failure message if nullptr. + */ + nsresult FailDownload(nsresult aStatus, const char16_t *aMessage); + + /** + * Opens the downloaded file with the appropriate application, which is + * either the OS default, MIME type default, or the one selected by the user. + * + * This also adds the temporary file to the "To be deleted on Exit" list, if + * the corresponding user preference is set (except on OS X). + * + * This function was adopted from nsExternalAppHandler::OpenWithApplication + * (uriloader/exthandler/nsExternalHelperAppService.cpp). + */ + nsresult OpenWithApplication(); + + nsDownloadManager *mDownloadManager; + nsCOMPtr<nsIURI> mTarget; + +private: + nsString mDisplayName; + nsCString mEntityID; + nsCString mGUID; + + nsCOMPtr<nsIURI> mSource; + nsCOMPtr<nsIURI> mReferrer; + nsCOMPtr<nsICancelable> mCancelable; + nsCOMPtr<nsIRequest> mRequest; + nsCOMPtr<nsIFile> mTempFile; + nsCOMPtr<nsIMIMEInfo> mMIMEInfo; + + DownloadState mDownloadState; + + uint32_t mID; + int32_t mPercentComplete; + + /** + * These bytes are based on the position of where the request started, so 0 + * doesn't necessarily mean we have nothing. Use GetAmountTransferred and + * GetSize for the real transferred amount and size. + */ + int64_t mCurrBytes; + int64_t mMaxBytes; + + PRTime mStartTime; + PRTime mLastUpdate; + int64_t mResumedAt; + double mSpeed; + + bool mHasMultipleFiles; + bool mPrivate; + + /** + * Track various states of the download trying to auto-resume when starting + * the download manager or restoring from a crash. + * + * DONT_RESUME: Don't automatically resume the download + * AUTO_RESUME: Automaically resume the download + */ + enum AutoResume { DONT_RESUME, AUTO_RESUME }; + AutoResume mAutoResume; + + /** + * Stores the SHA-256 hash associated with the downloaded file. + */ + nsCString mHash; + + /** + * Stores the certificate chains in an nsIArray of nsIX509CertList of + * nsIX509Cert, if this binary is signed. + */ + nsCOMPtr<nsIArray> mSignatureInfo; + + /** + * Stores the redirects that led to this download in an nsIArray of + * nsIPrincipal. + */ + nsCOMPtr<nsIArray> mRedirects; + + friend class nsDownloadManager; +}; + +#endif diff --git a/components/downloads/src/nsDownloadManagerUI.js b/components/downloads/src/nsDownloadManagerUI.js new file mode 100644 index 000000000..11e241403 --- /dev/null +++ b/components/downloads/src/nsDownloadManagerUI.js @@ -0,0 +1,107 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +// Constants + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const DOWNLOAD_MANAGER_URL = "chrome://mozapps/content/downloads/downloads.xul"; +const PREF_FLASH_COUNT = "browser.download.manager.flashCount"; + +// nsDownloadManagerUI class + +function nsDownloadManagerUI() {} + +nsDownloadManagerUI.prototype = { + classID: Components.ID("7dfdf0d1-aff6-4a34-bad1-d0fe74601642"), + + // nsIDownloadManagerUI + + show: function show(aWindowContext, aDownload, aReason, aUsePrivateUI) + { + if (!aReason) + aReason = Ci.nsIDownloadManagerUI.REASON_USER_INTERACTED; + + // First we see if it is already visible + let window = this.recentWindow; + if (window) { + window.focus(); + + // If we are being asked to show again, with a user interaction reason, + // set the appropriate variable. + if (aReason == Ci.nsIDownloadManagerUI.REASON_USER_INTERACTED) + window.gUserInteracted = true; + return; + } + + let parent = null; + // We try to get a window to use as the parent here. If we don't have one, + // the download manager will close immediately after opening if the pref + // browser.download.manager.closeWhenDone is set to true. + try { + if (aWindowContext) + parent = aWindowContext.getInterface(Ci.nsIDOMWindow); + } catch (e) { /* it's OK to not have a parent window */ } + + // We pass the download manager and the nsIDownload we want selected (if any) + var params = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); + params.appendElement(aDownload, false); + + // Pass in the reason as well + let reason = Cc["@mozilla.org/supports-PRInt16;1"]. + createInstance(Ci.nsISupportsPRInt16); + reason.data = aReason; + params.appendElement(reason, false); + + var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. + getService(Ci.nsIWindowWatcher); + ww.openWindow(parent, + DOWNLOAD_MANAGER_URL, + "Download:Manager", + "chrome,dialog=no,resizable", + params); + }, + + get visible() { + return (null != this.recentWindow); + }, + + getAttention: function getAttention() + { + if (!this.visible) + throw Cr.NS_ERROR_UNEXPECTED; + + var prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + // This preference may not be set, so defaulting to two. + let flashCount = 2; + try { + flashCount = prefs.getIntPref(PREF_FLASH_COUNT); + } catch (e) { } + + var win = this.recentWindow.QueryInterface(Ci.nsIDOMChromeWindow); + win.getAttentionWithCycleCount(flashCount); + }, + + // nsDownloadManagerUI + + get recentWindow() { + var wm = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); + return wm.getMostRecentWindow("Download:Manager"); + }, + + // nsISupports + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDownloadManagerUI]) +}; + +// Module + +var components = [nsDownloadManagerUI]; +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); + diff --git a/components/downloads/src/nsDownloadProxy.h b/components/downloads/src/nsDownloadProxy.h new file mode 100644 index 000000000..ca48c9dad --- /dev/null +++ b/components/downloads/src/nsDownloadProxy.h @@ -0,0 +1,179 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef downloadproxy___h___ +#define downloadproxy___h___ + +#include "nsIDownloadManager.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsIMIMEInfo.h" +#include "nsIFileURL.h" +#include "nsIDownloadManagerUI.h" + +#define PREF_BDM_SHOWWHENSTARTING "browser.download.manager.showWhenStarting" +#define PREF_BDM_FOCUSWHENSTARTING "browser.download.manager.focusWhenStarting" + +// This class only exists because nsDownload cannot inherit from nsITransfer +// directly. The reason for this is that nsDownloadManager (incorrectly) keeps +// an nsCOMArray of nsDownloads, and nsCOMArray is only intended for use with +// abstract classes. Using a concrete class that multiply inherits from classes +// deriving from nsISupports will throw ambiguous base class errors. +class nsDownloadProxy : public nsITransfer +{ +protected: + + virtual ~nsDownloadProxy() { } + +public: + + nsDownloadProxy() { } + + NS_DECL_ISUPPORTS + + NS_IMETHOD Init(nsIURI* aSource, + nsIURI* aTarget, + const nsAString& aDisplayName, + nsIMIMEInfo *aMIMEInfo, + PRTime aStartTime, + nsIFile* aTempFile, + nsICancelable* aCancelable, + bool aIsPrivate) override { + nsresult rv; + nsCOMPtr<nsIDownloadManager> dm = do_GetService("@mozilla.org/download-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = dm->AddDownload(nsIDownloadManager::DOWNLOAD_TYPE_DOWNLOAD, aSource, + aTarget, aDisplayName, aMIMEInfo, aStartTime, + aTempFile, aCancelable, aIsPrivate, + getter_AddRefs(mInner)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrefService> prefs = do_GetService("@mozilla.org/preferences-service;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIPrefBranch> branch = do_QueryInterface(prefs); + + bool showDM = true; + if (branch) + branch->GetBoolPref(PREF_BDM_SHOWWHENSTARTING, &showDM); + + if (showDM) { + nsCOMPtr<nsIDownloadManagerUI> dmui = + do_GetService("@mozilla.org/download-manager-ui;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + bool visible; + rv = dmui->GetVisible(&visible); + NS_ENSURE_SUCCESS(rv, rv); + + bool focusWhenStarting = true; + if (branch) + (void)branch->GetBoolPref(PREF_BDM_FOCUSWHENSTARTING, &focusWhenStarting); + + if (visible && !focusWhenStarting) + return NS_OK; + + return dmui->Show(nullptr, mInner, nsIDownloadManagerUI::REASON_NEW_DOWNLOAD, aIsPrivate); + } + return rv; + } + + NS_IMETHOD OnStateChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aStateFlags, + nsresult aStatus) override + { + NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED); + return mInner->OnStateChange(aWebProgress, aRequest, aStateFlags, aStatus); + } + + NS_IMETHOD OnStatusChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, nsresult aStatus, + const char16_t *aMessage) override + { + NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED); + return mInner->OnStatusChange(aWebProgress, aRequest, aStatus, aMessage); + } + + NS_IMETHOD OnLocationChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, nsIURI *aLocation, + uint32_t aFlags) override + { + NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED); + return mInner->OnLocationChange(aWebProgress, aRequest, aLocation, aFlags); + } + + NS_IMETHOD OnProgressChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, + int32_t aCurSelfProgress, + int32_t aMaxSelfProgress, + int32_t aCurTotalProgress, + int32_t aMaxTotalProgress) override + { + NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED); + return mInner->OnProgressChange(aWebProgress, aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress); + } + + NS_IMETHOD OnProgressChange64(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, + int64_t aCurSelfProgress, + int64_t aMaxSelfProgress, + int64_t aCurTotalProgress, + int64_t aMaxTotalProgress) override + { + NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED); + return mInner->OnProgressChange64(aWebProgress, aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress); + } + + NS_IMETHOD OnRefreshAttempted(nsIWebProgress *aWebProgress, + nsIURI *aUri, + int32_t aDelay, + bool aSameUri, + bool *allowRefresh) override + { + *allowRefresh = true; + return NS_OK; + } + + NS_IMETHOD OnSecurityChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, uint32_t aState) override + { + NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED); + return mInner->OnSecurityChange(aWebProgress, aRequest, aState); + } + + NS_IMETHOD SetSha256Hash(const nsACString& aHash) override + { + NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED); + return mInner->SetSha256Hash(aHash); + } + + NS_IMETHOD SetSignatureInfo(nsIArray* aSignatureInfo) override + { + NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED); + return mInner->SetSignatureInfo(aSignatureInfo); + } + + NS_IMETHOD SetRedirects(nsIArray* aRedirects) override + { + NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED); + return mInner->SetRedirects(aRedirects); + } + +private: + nsCOMPtr<nsIDownload> mInner; +}; + +NS_IMPL_ISUPPORTS(nsDownloadProxy, nsITransfer, + nsIWebProgressListener, nsIWebProgressListener2) + +#endif diff --git a/components/downloads/src/nsDownloadScanner.cpp b/components/downloads/src/nsDownloadScanner.cpp new file mode 100644 index 000000000..1ef5b3660 --- /dev/null +++ b/components/downloads/src/nsDownloadScanner.cpp @@ -0,0 +1,728 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: se cin sw=2 ts=2 et : */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsDownloadScanner.h" +#include <comcat.h> +#include <process.h> +#include "nsDownloadManager.h" +#include "nsIXULAppInfo.h" +#include "nsXULAppAPI.h" +#include "nsIPrefService.h" +#include "nsNetUtil.h" +#include "prtime.h" +#include "nsDeque.h" +#include "nsIFileURL.h" +#include "nsIPrefBranch.h" +#include "nsXPCOMCIDInternal.h" + +/** + * Code overview + * + * Download scanner attempts to make use of one of two different virus + * scanning interfaces available on Windows - IOfficeAntiVirus (Windows + * 95/NT 4 and IE 5) and IAttachmentExecute (XPSP2 and up). The latter + * interface supports calling IOfficeAntiVirus internally, while also + * adding support for XPSP2+ ADS forks which define security related + * prompting on downloaded content. + * + * Both interfaces are synchronous and can take a while, so it is not a + * good idea to call either from the main thread. Some antivirus scanners can + * take a long time to scan or the call might block while the scanner shows + * its UI so if the user were to download many files that finished around the + * same time, they would have to wait a while if the scanning were done on + * exactly one other thread. Since the overhead of creating a thread is + * relatively small compared to the time it takes to download a file and scan + * it, a new thread is spawned for each download that is to be scanned. Since + * most of the mozilla codebase is not threadsafe, all the information needed + * for the scanner is gathered in the main thread in nsDownloadScanner::Scan::Start. + * The only function of nsDownloadScanner::Scan which is invoked on another + * thread is DoScan. + * + * Watchdog overview + * + * The watchdog is used internally by the scanner. It maintains a queue of + * current download scans. In a separate thread, it dequeues the oldest scan + * and waits on that scan's thread with a timeout given by WATCHDOG_TIMEOUT + * (default is 30 seconds). If the wait times out, then the watchdog notifies + * the Scan that it has timed out. If the scan really has timed out, then the + * Scan object will dispatch its run method to the main thread; this will + * release the watchdog thread's addref on the Scan. If it has not timed out + * (i.e. the Scan just finished in time), then the watchdog dispatches a + * ReleaseDispatcher to release its ref of the Scan on the main thread. + * + * In order to minimize execution time, there are two events used to notify the + * watchdog thread of a non-empty queue and a quit event. Every blocking wait + * that the watchdog thread does waits on the quit event; this lets the thread + * quickly exit when shutting down. Also, the download scan queue will be empty + * most of the time; rather than use a spin loop, a simple event is triggered + * by the main thread when a new scan is added to an empty queue. When the + * watchdog thread knows that it has run out of elements in the queue, it will + * wait on the new item event. + * + * Memory/resource leaks due to timeout: + * In the event of a timeout, the thread must remain alive; terminating it may + * very well cause the antivirus scanner to crash or be put into an + * inconsistent state; COM resources may also not be cleaned up. The downside + * is that we need to leave the thread running; suspending it may lead to a + * deadlock. Because the scan call may be ongoing, it may be dependent on the + * memory referenced by the MSOAVINFO structure, so we cannot free mName, mPath + * or mOrigin; this means that we cannot free the Scan object since doing so + * will deallocate that memory. Note that mDownload is set to null upon timeout + * or completion, so the download itself is never leaked. If the scan does + * eventually complete, then the all the memory and resources will be freed. + * It is possible, however extremely rare, that in the event of a timeout, the + * mStateSync critical section will leak its event; this will happen only if + * the scanning thread, watchdog thread or main thread try to enter the + * critical section when one of the others is already in it. + * + * Reasoning for CheckAndSetState - there exists a race condition between the time when + * either the timeout or normal scan sets the state and when Scan::Run is + * executed on the main thread. Ex: mStatus could be set by Scan::DoScan* which + * then queues a dispatch on the main thread. Before that dispatch is executed, + * the timeout code fires and sets mStatus to AVSCAN_TIMEDOUT which then queues + * its dispatch to the main thread (the same function as DoScan*). Both + * dispatches run and both try to set the download state to AVSCAN_TIMEDOUT + * which is incorrect. + * + * There are 5 possible outcomes of the virus scan: + * AVSCAN_GOOD => the file is clean + * AVSCAN_BAD => the file has a virus + * AVSCAN_UGLY => the file had a virus, but it was cleaned + * AVSCAN_FAILED => something else went wrong with the virus scanner. + * AVSCAN_TIMEDOUT => the scan (thread setup + execution) took too long + * + * Both the good and ugly states leave the user with a benign file, so they + * transition to the finished state. Bad files are sent to the blocked state. + * The failed and timedout states transition to finished downloads. + * + * Possible Future enhancements: + * * Create an interface for scanning files in general + * * Make this a service + * * Get antivirus scanner status via WMI/registry + */ + +// IAttachementExecute supports user definable settings for certain +// security related prompts. This defines a general GUID for use in +// all projects. Individual projects can define an individual guid +// if they want to. +#ifndef MOZ_VIRUS_SCANNER_PROMPT_GUID +#define MOZ_VIRUS_SCANNER_PROMPT_GUID \ + { 0xb50563d1, 0x16b6, 0x43c2, { 0xa6, 0x6a, 0xfa, 0xe6, 0xd2, 0x11, 0xf2, \ + 0xea } } +#endif +static const GUID GUID_MozillaVirusScannerPromptGeneric = + MOZ_VIRUS_SCANNER_PROMPT_GUID; + +// Initial timeout is 30 seconds +#define WATCHDOG_TIMEOUT (30*PR_USEC_PER_SEC) + +// Maximum length for URI's passed into IAE +#define MAX_IAEURILENGTH 1683 + +class nsDownloadScannerWatchdog +{ + typedef nsDownloadScanner::Scan Scan; +public: + nsDownloadScannerWatchdog(); + ~nsDownloadScannerWatchdog(); + + nsresult Init(); + nsresult Shutdown(); + + void Watch(Scan *scan); +private: + static unsigned int __stdcall WatchdogThread(void *p); + CRITICAL_SECTION mQueueSync; + nsDeque mScanQueue; + HANDLE mThread; + HANDLE mNewItemEvent; + HANDLE mQuitEvent; +}; + +nsDownloadScanner::nsDownloadScanner() : + mAESExists(false) +{ +} + +// This destructor appeases the compiler; it would otherwise complain about an +// incomplete type for nsDownloadWatchdog in the instantiation of +// nsAutoPtr::~nsAutoPtr +// Plus, it's a handy location to call nsDownloadScannerWatchdog::Shutdown from +nsDownloadScanner::~nsDownloadScanner() { + if (mWatchdog) + (void)mWatchdog->Shutdown(); +} + +nsresult +nsDownloadScanner::Init() +{ + // This CoInitialize/CoUninitialize pattern seems to be common in the Mozilla + // codebase. All other COM calls/objects are made on different threads. + nsresult rv = NS_OK; + CoInitialize(nullptr); + + if (!IsAESAvailable()) { + CoUninitialize(); + return NS_ERROR_NOT_AVAILABLE; + } + + mAESExists = true; + + // Initialize scanning + mWatchdog = new nsDownloadScannerWatchdog(); + if (mWatchdog) { + rv = mWatchdog->Init(); + if (FAILED(rv)) + mWatchdog = nullptr; + } else { + rv = NS_ERROR_OUT_OF_MEMORY; + } + + if (NS_FAILED(rv)) + return rv; + + return rv; +} + +bool +nsDownloadScanner::IsAESAvailable() +{ + // Try to instantiate IAE to see if it's available. + RefPtr<IAttachmentExecute> ae; + HRESULT hr; + hr = CoCreateInstance(CLSID_AttachmentServices, nullptr, CLSCTX_INPROC, + IID_IAttachmentExecute, getter_AddRefs(ae)); + if (FAILED(hr)) { + NS_WARNING("Could not instantiate attachment execution service\n"); + return false; + } + return true; +} + +// If IAttachementExecute is available, use the CheckPolicy call to find out +// if this download should be prevented due to Security Zone Policy settings. +AVCheckPolicyState +nsDownloadScanner::CheckPolicy(nsIURI *aSource, nsIURI *aTarget) +{ + nsresult rv; + + if (!mAESExists || !aSource || !aTarget) + return AVPOLICY_DOWNLOAD; + + nsAutoCString source; + rv = aSource->GetSpec(source); + if (NS_FAILED(rv)) + return AVPOLICY_DOWNLOAD; + + nsCOMPtr<nsIFileURL> fileUrl(do_QueryInterface(aTarget)); + if (!fileUrl) + return AVPOLICY_DOWNLOAD; + + nsCOMPtr<nsIFile> theFile; + nsAutoString aFileName; + if (NS_FAILED(fileUrl->GetFile(getter_AddRefs(theFile))) || + NS_FAILED(theFile->GetLeafName(aFileName))) + return AVPOLICY_DOWNLOAD; + + // IAttachementExecute prohibits src data: schemes by default but we + // support them. If this is a data src, skip off doing a policy check. + // (The file will still be scanned once it lands on the local system.) + bool isDataScheme(false); + nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(aSource); + if (innerURI) + (void)innerURI->SchemeIs("data", &isDataScheme); + if (isDataScheme) + return AVPOLICY_DOWNLOAD; + + RefPtr<IAttachmentExecute> ae; + HRESULT hr; + hr = CoCreateInstance(CLSID_AttachmentServices, nullptr, CLSCTX_INPROC, + IID_IAttachmentExecute, getter_AddRefs(ae)); + if (FAILED(hr)) + return AVPOLICY_DOWNLOAD; + + (void)ae->SetClientGuid(GUID_MozillaVirusScannerPromptGeneric); + (void)ae->SetSource(NS_ConvertUTF8toUTF16(source).get()); + (void)ae->SetFileName(aFileName.get()); + + // Any failure means the file download/exec will be blocked by the system. + // S_OK or S_FALSE imply it's ok. + hr = ae->CheckPolicy(); + + if (hr == S_OK) + return AVPOLICY_DOWNLOAD; + + if (hr == S_FALSE) + return AVPOLICY_PROMPT; + + if (hr == E_INVALIDARG) + return AVPOLICY_PROMPT; + + return AVPOLICY_BLOCKED; +} + +#ifndef THREAD_MODE_BACKGROUND_BEGIN +#define THREAD_MODE_BACKGROUND_BEGIN 0x00010000 +#endif + +#ifndef THREAD_MODE_BACKGROUND_END +#define THREAD_MODE_BACKGROUND_END 0x00020000 +#endif + +unsigned int __stdcall +nsDownloadScanner::ScannerThreadFunction(void *p) +{ + HANDLE currentThread = GetCurrentThread(); + NS_ASSERTION(!NS_IsMainThread(), "Antivirus scan should not be run on the main thread"); + nsDownloadScanner::Scan *scan = static_cast<nsDownloadScanner::Scan*>(p); + if (!SetThreadPriority(currentThread, THREAD_MODE_BACKGROUND_BEGIN)) + (void)SetThreadPriority(currentThread, THREAD_PRIORITY_IDLE); + scan->DoScan(); + (void)SetThreadPriority(currentThread, THREAD_MODE_BACKGROUND_END); + _endthreadex(0); + return 0; +} + +// The sole purpose of this class is to release an object on the main thread +// It assumes that its creator will addref it and it will release itself on +// the main thread too +class ReleaseDispatcher : public mozilla::Runnable { +public: + ReleaseDispatcher(nsISupports *ptr) + : mPtr(ptr) {} + NS_IMETHOD Run(); +private: + nsISupports *mPtr; +}; + +nsresult ReleaseDispatcher::Run() { + NS_ASSERTION(NS_IsMainThread(), "Antivirus scan release dispatch should be run on the main thread"); + NS_RELEASE(mPtr); + NS_RELEASE_THIS(); + return NS_OK; +} + +nsDownloadScanner::Scan::Scan(nsDownloadScanner *scanner, nsDownload *download) + : mDLScanner(scanner), mThread(nullptr), + mDownload(download), mStatus(AVSCAN_NOTSTARTED), + mSkipSource(false) +{ + InitializeCriticalSection(&mStateSync); +} + +nsDownloadScanner::Scan::~Scan() { + DeleteCriticalSection(&mStateSync); +} + +nsresult +nsDownloadScanner::Scan::Start() +{ + mStartTime = PR_Now(); + + mThread = (HANDLE)_beginthreadex(nullptr, 0, ScannerThreadFunction, + this, CREATE_SUSPENDED, nullptr); + if (!mThread) + return NS_ERROR_OUT_OF_MEMORY; + + nsresult rv = NS_OK; + + // Get the path to the file on disk + nsCOMPtr<nsIFile> file; + rv = mDownload->GetTargetFile(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + rv = file->GetPath(mPath); + NS_ENSURE_SUCCESS(rv, rv); + + // Grab the app name + nsCOMPtr<nsIXULAppInfo> appinfo = + do_GetService(XULAPPINFO_SERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString name; + rv = appinfo->GetName(name); + NS_ENSURE_SUCCESS(rv, rv); + CopyUTF8toUTF16(name, mName); + + // Get the origin + nsCOMPtr<nsIURI> uri; + rv = mDownload->GetSource(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString origin; + rv = uri->GetSpec(origin); + NS_ENSURE_SUCCESS(rv, rv); + + // Certain virus interfaces do not like extremely long uris. + // Chop off the path and cgi data and just pass the base domain. + if (origin.Length() > MAX_IAEURILENGTH) { + rv = uri->GetPrePath(origin); + NS_ENSURE_SUCCESS(rv, rv); + } + + CopyUTF8toUTF16(origin, mOrigin); + + // We count https/ftp/http as an http download + bool isHttp(false), isFtp(false), isHttps(false); + nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(uri); + if (!innerURI) innerURI = uri; + (void)innerURI->SchemeIs("http", &isHttp); + (void)innerURI->SchemeIs("ftp", &isFtp); + (void)innerURI->SchemeIs("https", &isHttps); + mIsHttpDownload = isHttp || isFtp || isHttps; + + // IAttachementExecute prohibits src data: schemes by default but we + // support them. Mark the download if it's a data scheme, so we + // can skip off supplying the src to IAttachementExecute when we scan + // the resulting file. + (void)innerURI->SchemeIs("data", &mSkipSource); + + // ResumeThread returns the previous suspend count + if (1 != ::ResumeThread(mThread)) { + CloseHandle(mThread); + return NS_ERROR_UNEXPECTED; + } + return NS_OK; +} + +nsresult +nsDownloadScanner::Scan::Run() +{ + NS_ASSERTION(NS_IsMainThread(), "Antivirus scan dispatch should be run on the main thread"); + + // Cleanup our thread + if (mStatus != AVSCAN_TIMEDOUT) + WaitForSingleObject(mThread, INFINITE); + CloseHandle(mThread); + + DownloadState downloadState = 0; + EnterCriticalSection(&mStateSync); + switch (mStatus) { + case AVSCAN_BAD: + downloadState = nsIDownloadManager::DOWNLOAD_DIRTY; + break; + default: + case AVSCAN_FAILED: + case AVSCAN_GOOD: + case AVSCAN_UGLY: + case AVSCAN_TIMEDOUT: + downloadState = nsIDownloadManager::DOWNLOAD_FINISHED; + break; + } + LeaveCriticalSection(&mStateSync); + // Download will be null if we already timed out + if (mDownload) + (void)mDownload->SetState(downloadState); + + // Clean up some other variables + // In the event of a timeout, our destructor won't be called + mDownload = nullptr; + + NS_RELEASE_THIS(); + return NS_OK; +} + +static DWORD +ExceptionFilterFunction(DWORD exceptionCode) { + switch(exceptionCode) { + case EXCEPTION_ACCESS_VIOLATION: + case EXCEPTION_ILLEGAL_INSTRUCTION: + case EXCEPTION_IN_PAGE_ERROR: + case EXCEPTION_PRIV_INSTRUCTION: + case EXCEPTION_STACK_OVERFLOW: + return EXCEPTION_EXECUTE_HANDLER; + default: + return EXCEPTION_CONTINUE_SEARCH; + } +} + +bool +nsDownloadScanner::Scan::DoScanAES() +{ + // This warning is for the destructor of ae which will not be invoked in the + // event of a win32 exception +#pragma warning(disable: 4509) + HRESULT hr; + RefPtr<IAttachmentExecute> ae; + MOZ_SEH_TRY { + hr = CoCreateInstance(CLSID_AttachmentServices, nullptr, CLSCTX_ALL, + IID_IAttachmentExecute, getter_AddRefs(ae)); + } MOZ_SEH_EXCEPT(ExceptionFilterFunction(GetExceptionCode())) { + return CheckAndSetState(AVSCAN_NOTSTARTED,AVSCAN_FAILED); + } + + // If we (somehow) already timed out, then don't bother scanning + if (CheckAndSetState(AVSCAN_SCANNING, AVSCAN_NOTSTARTED)) { + AVScanState newState; + if (SUCCEEDED(hr)) { + bool gotException = false; + MOZ_SEH_TRY { + (void)ae->SetClientGuid(GUID_MozillaVirusScannerPromptGeneric); + (void)ae->SetLocalPath(mPath.get()); + // Provide the src for everything but data: schemes. + if (!mSkipSource) + (void)ae->SetSource(mOrigin.get()); + + // Save() will invoke the scanner + hr = ae->Save(); + } MOZ_SEH_EXCEPT(ExceptionFilterFunction(GetExceptionCode())) { + gotException = true; + } + + MOZ_SEH_TRY { + ae = nullptr; + } MOZ_SEH_EXCEPT(ExceptionFilterFunction(GetExceptionCode())) { + gotException = true; + } + + if(gotException) { + newState = AVSCAN_FAILED; + } + else if (SUCCEEDED(hr)) { // Passed the scan + newState = AVSCAN_GOOD; + } + else if (HRESULT_CODE(hr) == ERROR_FILE_NOT_FOUND) { + NS_WARNING("Downloaded file disappeared before it could be scanned"); + newState = AVSCAN_FAILED; + } + else if (hr == E_INVALIDARG) { + NS_WARNING("IAttachementExecute returned invalid argument error"); + newState = AVSCAN_FAILED; + } + else { + newState = AVSCAN_UGLY; + } + } + else { + newState = AVSCAN_FAILED; + } + return CheckAndSetState(newState, AVSCAN_SCANNING); + } + return false; +} +#pragma warning(default: 4509) + +void +nsDownloadScanner::Scan::DoScan() +{ + CoInitialize(nullptr); + + if (DoScanAES()) { + // We need to do a few more things on the main thread + NS_DispatchToMainThread(this); + } else { + // We timed out, so just release + ReleaseDispatcher* releaser = new ReleaseDispatcher(this); + if(releaser) { + NS_ADDREF(releaser); + NS_DispatchToMainThread(releaser); + } + } + + MOZ_SEH_TRY { + CoUninitialize(); + } MOZ_SEH_EXCEPT(ExceptionFilterFunction(GetExceptionCode())) { + // Not much we can do at this point... + } +} + +HANDLE +nsDownloadScanner::Scan::GetWaitableThreadHandle() const +{ + HANDLE targetHandle = INVALID_HANDLE_VALUE; + (void)DuplicateHandle(GetCurrentProcess(), mThread, + GetCurrentProcess(), &targetHandle, + SYNCHRONIZE, // Only allow clients to wait on this handle + FALSE, // cannot be inherited by child processes + 0); + return targetHandle; +} + +bool +nsDownloadScanner::Scan::NotifyTimeout() +{ + bool didTimeout = CheckAndSetState(AVSCAN_TIMEDOUT, AVSCAN_SCANNING) || + CheckAndSetState(AVSCAN_TIMEDOUT, AVSCAN_NOTSTARTED); + if (didTimeout) { + // We need to do a few more things on the main thread + NS_DispatchToMainThread(this); + } + return didTimeout; +} + +bool +nsDownloadScanner::Scan::CheckAndSetState(AVScanState newState, AVScanState expectedState) { + bool gotExpectedState = false; + EnterCriticalSection(&mStateSync); + if((gotExpectedState = (mStatus == expectedState))) + mStatus = newState; + LeaveCriticalSection(&mStateSync); + return gotExpectedState; +} + +nsresult +nsDownloadScanner::ScanDownload(nsDownload *download) +{ + if (!mAESExists) + return NS_ERROR_NOT_AVAILABLE; + + // No ref ptr, see comment below + Scan *scan = new Scan(this, download); + if (!scan) + return NS_ERROR_OUT_OF_MEMORY; + + NS_ADDREF(scan); + + nsresult rv = scan->Start(); + + // Note that we only release upon error. On success, the scan is passed off + // to a new thread. It is eventually released in Scan::Run on the main thread. + if (NS_FAILED(rv)) + NS_RELEASE(scan); + else + // Notify the watchdog + mWatchdog->Watch(scan); + + return rv; +} + +nsDownloadScannerWatchdog::nsDownloadScannerWatchdog() + : mNewItemEvent(nullptr), mQuitEvent(nullptr) { + InitializeCriticalSection(&mQueueSync); +} +nsDownloadScannerWatchdog::~nsDownloadScannerWatchdog() { + DeleteCriticalSection(&mQueueSync); +} + +nsresult +nsDownloadScannerWatchdog::Init() { + // Both events are auto-reset + mNewItemEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); + if (INVALID_HANDLE_VALUE == mNewItemEvent) + return NS_ERROR_OUT_OF_MEMORY; + mQuitEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); + if (INVALID_HANDLE_VALUE == mQuitEvent) { + (void)CloseHandle(mNewItemEvent); + return NS_ERROR_OUT_OF_MEMORY; + } + + // This thread is always running, however it will be asleep + // for most of the dlmgr's lifetime + mThread = (HANDLE)_beginthreadex(nullptr, 0, WatchdogThread, + this, 0, nullptr); + if (!mThread) { + (void)CloseHandle(mNewItemEvent); + (void)CloseHandle(mQuitEvent); + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +nsresult +nsDownloadScannerWatchdog::Shutdown() { + // Tell the watchdog thread to quite + (void)SetEvent(mQuitEvent); + (void)WaitForSingleObject(mThread, INFINITE); + (void)CloseHandle(mThread); + // Manually clear and release the queued scans + while (mScanQueue.GetSize() != 0) { + Scan *scan = reinterpret_cast<Scan*>(mScanQueue.Pop()); + NS_RELEASE(scan); + } + (void)CloseHandle(mNewItemEvent); + (void)CloseHandle(mQuitEvent); + return NS_OK; +} + +void +nsDownloadScannerWatchdog::Watch(Scan *scan) { + bool wasEmpty; + // Note that there is no release in this method + // The scan will be released by the watchdog ALWAYS on the main thread + // when either the watchdog thread processes the scan or the watchdog + // is shut down + NS_ADDREF(scan); + EnterCriticalSection(&mQueueSync); + wasEmpty = mScanQueue.GetSize()==0; + mScanQueue.Push(scan); + LeaveCriticalSection(&mQueueSync); + // If the queue was empty, then the watchdog thread is/will be asleep + if (wasEmpty) + (void)SetEvent(mNewItemEvent); +} + +unsigned int +__stdcall +nsDownloadScannerWatchdog::WatchdogThread(void *p) { + NS_ASSERTION(!NS_IsMainThread(), "Antivirus scan watchdog should not be run on the main thread"); + nsDownloadScannerWatchdog *watchdog = (nsDownloadScannerWatchdog*)p; + HANDLE waitHandles[3] = {watchdog->mNewItemEvent, watchdog->mQuitEvent, INVALID_HANDLE_VALUE}; + DWORD waitStatus; + DWORD queueItemsLeft = 0; + // Loop until quit event or error + while (0 != queueItemsLeft || + ((WAIT_OBJECT_0 + 1) != + (waitStatus = + WaitForMultipleObjects(2, waitHandles, FALSE, INFINITE)) && + waitStatus != WAIT_FAILED)) { + Scan *scan = nullptr; + PRTime startTime, expectedEndTime, now; + DWORD waitTime; + + // Pop scan from queue + EnterCriticalSection(&watchdog->mQueueSync); + scan = reinterpret_cast<Scan*>(watchdog->mScanQueue.Pop()); + queueItemsLeft = watchdog->mScanQueue.GetSize(); + LeaveCriticalSection(&watchdog->mQueueSync); + + // Calculate expected end time + startTime = scan->GetStartTime(); + expectedEndTime = WATCHDOG_TIMEOUT + startTime; + now = PR_Now(); + // PRTime is not guaranteed to be a signed integral type (afaik), but + // currently it is + if (now > expectedEndTime) { + waitTime = 0; + } else { + // This is a positive value, and we know that it will not overflow + // (bounded by WATCHDOG_TIMEOUT) + // waitTime is in milliseconds, nspr uses microseconds + waitTime = static_cast<DWORD>((expectedEndTime - now)/PR_USEC_PER_MSEC); + } + HANDLE hThread = waitHandles[2] = scan->GetWaitableThreadHandle(); + + // Wait for the thread (obj 1) or quit event (obj 0) + waitStatus = WaitForMultipleObjects(2, (waitHandles+1), FALSE, waitTime); + CloseHandle(hThread); + + ReleaseDispatcher* releaser = new ReleaseDispatcher(scan); + if(!releaser) + continue; + NS_ADDREF(releaser); + // Got quit event or error + if (waitStatus == WAIT_FAILED || waitStatus == WAIT_OBJECT_0) { + NS_DispatchToMainThread(releaser); + break; + // Thread exited normally + } else if (waitStatus == (WAIT_OBJECT_0+1)) { + NS_DispatchToMainThread(releaser); + continue; + // Timeout case + } else { + NS_ASSERTION(waitStatus == WAIT_TIMEOUT, "Unexpected wait status in dlmgr watchdog thread"); + if (!scan->NotifyTimeout()) { + // If we didn't time out, then release the thread + NS_DispatchToMainThread(releaser); + } else { + // NotifyTimeout did a dispatch which will release the scan, so we + // don't need to release the scan + NS_RELEASE(releaser); + } + } + } + _endthreadex(0); + return 0; +} diff --git a/components/downloads/src/nsDownloadScanner.h b/components/downloads/src/nsDownloadScanner.h new file mode 100644 index 000000000..3301489fe --- /dev/null +++ b/components/downloads/src/nsDownloadScanner.h @@ -0,0 +1,121 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* vim: se cin sw=2 ts=2 et : */ + +#ifndef nsDownloadScanner_h_ +#define nsDownloadScanner_h_ + +#ifdef WIN32_LEAN_AND_MEAN +#undef WIN32_LEAN_AND_MEAN +#endif +#include <windows.h> +#define AVVENDOR +#include <objidl.h> +#include <msoav.h> +#include <shlobj.h> + +#include "nsAutoPtr.h" +#include "nsThreadUtils.h" +#include "nsTArray.h" +#include "nsIObserver.h" +#include "nsIURI.h" + +enum AVScanState +{ + AVSCAN_NOTSTARTED = 0, + AVSCAN_SCANNING, + AVSCAN_GOOD, + AVSCAN_BAD, + AVSCAN_UGLY, + AVSCAN_FAILED, + AVSCAN_TIMEDOUT +}; + +enum AVCheckPolicyState +{ + AVPOLICY_DOWNLOAD, + AVPOLICY_PROMPT, + AVPOLICY_BLOCKED +}; + +// See nsDownloadScanner.cpp for declaration and definition +class nsDownloadScannerWatchdog; +class nsDownload; + +class nsDownloadScanner +{ +public: + nsDownloadScanner(); + ~nsDownloadScanner(); + nsresult Init(); + nsresult ScanDownload(nsDownload *download); + AVCheckPolicyState CheckPolicy(nsIURI *aSource, nsIURI *aTarget); + +private: + bool mAESExists; + nsTArray<CLSID> mScanCLSID; + bool IsAESAvailable(); + bool EnumerateOAVProviders(); + + nsAutoPtr<nsDownloadScannerWatchdog> mWatchdog; + + static unsigned int __stdcall ScannerThreadFunction(void *p); + class Scan : public mozilla::Runnable + { + public: + Scan(nsDownloadScanner *scanner, nsDownload *download); + ~Scan(); + nsresult Start(); + + // Returns the time that Start was called + PRTime GetStartTime() const { return mStartTime; } + // Returns a copy of the thread handle that can be waited on, but not + // terminated + // The caller is responsible for closing the handle + // If the thread has terminated, then this will return the pseudo-handle + // INVALID_HANDLE_VALUE + HANDLE GetWaitableThreadHandle() const; + + // Called on a secondary thread to notify the scan that it has timed out + // this is used only by the watchdog thread + bool NotifyTimeout(); + + private: + nsDownloadScanner *mDLScanner; + PRTime mStartTime; + HANDLE mThread; + RefPtr<nsDownload> mDownload; + // Guards mStatus + CRITICAL_SECTION mStateSync; + AVScanState mStatus; + nsString mPath; + nsString mName; + nsString mOrigin; + // Also true if it is an ftp download + bool mIsHttpDownload; + bool mSkipSource; + + /* @summary Sets the Scan's state to newState if the current state is + expectedState + * @param newState The new state of the scan + * @param expectedState The state that the caller expects the scan to be in + * @return If the old state matched expectedState + */ + bool CheckAndSetState(AVScanState newState, AVScanState expectedState); + + NS_IMETHOD Run(); + + void DoScan(); + bool DoScanAES(); + bool DoScanOAV(); + + friend unsigned int __stdcall nsDownloadScanner::ScannerThreadFunction(void *); + }; + // Used to give access to Scan + friend class nsDownloadScannerWatchdog; +}; +#endif + diff --git a/components/downloads/src/nsHelperAppDlg.js b/components/downloads/src/nsHelperAppDlg.js new file mode 100644 index 000000000..0e5cfdaf0 --- /dev/null +++ b/components/downloads/src/nsHelperAppDlg.js @@ -0,0 +1,1138 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const {utils: Cu, interfaces: Ci, classes: Cc, results: Cr} = Components; +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "EnableDelayHelper", + "resource://gre/modules/SharedPromptUtils.jsm"); + +/////////////////////////////////////////////////////////////////////////////// +//// Helper Functions + +/** + * Determines if a given directory is able to be used to download to. + * + * @param aDirectory + * The directory to check. + * @return true if we can use the directory, false otherwise. + */ +function isUsableDirectory(aDirectory) +{ + return aDirectory.exists() && aDirectory.isDirectory() && + aDirectory.isWritable(); +} + +// Web progress listener so we can detect errors while mLauncher is +// streaming the data to a temporary file. +function nsUnknownContentTypeDialogProgressListener(aHelperAppDialog) { + this.helperAppDlg = aHelperAppDialog; +} + +nsUnknownContentTypeDialogProgressListener.prototype = { + // nsIWebProgressListener methods. + // Look for error notifications and display alert to user. + onStatusChange: function( aWebProgress, aRequest, aStatus, aMessage ) { + if ( aStatus != Components.results.NS_OK ) { + // Display error alert (using text supplied by back-end). + // FIXME this.dialog is undefined? + Services.prompt.alert( this.dialog, this.helperAppDlg.mTitle, aMessage ); + // Close the dialog. + this.helperAppDlg.onCancel(); + if ( this.helperAppDlg.mDialog ) { + this.helperAppDlg.mDialog.close(); + } + } + }, + + // Ignore onProgressChange, onProgressChange64, onStateChange, onLocationChange, onSecurityChange, and onRefreshAttempted notifications. + onProgressChange: function( aWebProgress, + aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress ) { + }, + + onProgressChange64: function( aWebProgress, + aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress ) { + }, + + + + onStateChange: function( aWebProgress, aRequest, aStateFlags, aStatus ) { + }, + + onLocationChange: function( aWebProgress, aRequest, aLocation, aFlags ) { + }, + + onSecurityChange: function( aWebProgress, aRequest, state ) { + }, + + onRefreshAttempted: function( aWebProgress, aURI, aDelay, aSameURI ) { + return true; + } +}; + +/////////////////////////////////////////////////////////////////////////////// +//// nsUnknownContentTypeDialog + +/* This file implements the nsIHelperAppLauncherDialog interface. + * + * The implementation consists of a JavaScript "class" named nsUnknownContentTypeDialog, + * comprised of: + * - a JS constructor function + * - a prototype providing all the interface methods and implementation stuff + * + * In addition, this file implements an nsIModule object that registers the + * nsUnknownContentTypeDialog component. + */ + +const PREF_BD_USEDOWNLOADDIR = "browser.download.useDownloadDir"; +const nsITimer = Components.interfaces.nsITimer; + +var downloadModule = {}; +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/DownloadLastDir.jsm", downloadModule); +Components.utils.import("resource://gre/modules/DownloadPaths.jsm"); +Components.utils.import("resource://gre/modules/DownloadUtils.jsm"); +Components.utils.import("resource://gre/modules/Downloads.jsm"); +Components.utils.import("resource://gre/modules/FileUtils.jsm"); +Components.utils.import("resource://gre/modules/Task.jsm"); + +/* ctor + */ +function nsUnknownContentTypeDialog() { + // Initialize data properties. + this.mLauncher = null; + this.mContext = null; + this.mReason = null; + this.chosenApp = null; + this.givenDefaultApp = false; + this.updateSelf = true; + this.mTitle = ""; +} + +nsUnknownContentTypeDialog.prototype = { + classID: Components.ID("{F68578EB-6EC2-4169-AE19-8C6243F0ABE1}"), + + nsIMIMEInfo : Components.interfaces.nsIMIMEInfo, + + QueryInterface: function (iid) { + if (!iid.equals(Components.interfaces.nsIHelperAppLauncherDialog) && + !iid.equals(Components.interfaces.nsITimerCallback) && + !iid.equals(Components.interfaces.nsISupports)) { + throw Components.results.NS_ERROR_NO_INTERFACE; + } + return this; + }, + + // ---------- nsIHelperAppLauncherDialog methods ---------- + + // show: Open XUL dialog using window watcher. Since the dialog is not + // modal, it needs to be a top level window and the way to open + // one of those is via that route). + show: function(aLauncher, aContext, aReason) { + this.mLauncher = aLauncher; + this.mContext = aContext; + this.mReason = aReason; + + // Cache some information in case this context goes away: + try { + let parent = aContext.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); + this._mDownloadDir = new downloadModule.DownloadLastDir(parent); + } catch (ex) { + Cu.reportError("Missing window information when showing nsIHelperAppLauncherDialog: " + ex); + } + + const nsITimer = Components.interfaces.nsITimer; + this._showTimer = Components.classes["@mozilla.org/timer;1"] + .createInstance(nsITimer); + this._showTimer.initWithCallback(this, 0, nsITimer.TYPE_ONE_SHOT); + }, + + // When opening from new tab, if tab closes while dialog is opening, + // (which is a race condition on the XUL file being cached and the timer + // in nsExternalHelperAppService), the dialog gets a blur and doesn't + // activate the OK button. So we wait a bit before doing opening it. + reallyShow: function() { + try { + let ir = this.mContext.QueryInterface(Components.interfaces.nsIInterfaceRequestor); + let docShell = ir.getInterface(Components.interfaces.nsIDocShell); + let rootWin = docShell.QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + let ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] + .getService(Components.interfaces.nsIWindowWatcher); + this.mDialog = ww.openWindow(rootWin, + "chrome://mozapps/content/downloads/unknownContentType.xul", + null, + "chrome,centerscreen,titlebar,dialog=yes,dependent", + null); + } catch (ex) { + // The containing window may have gone away. Break reference + // cycles and stop doing the download. + this.mLauncher.cancel(Components.results.NS_BINDING_ABORTED); + return; + } + + // Hook this object to the dialog. + this.mDialog.dialog = this; + + // Hook up utility functions. + this.getSpecialFolderKey = this.mDialog.getSpecialFolderKey; + + // Watch for error notifications. + var progressListener = new nsUnknownContentTypeDialogProgressListener(this); + this.mLauncher.setWebProgressListener(progressListener); + }, + + // + // displayBadPermissionAlert() + // + // Diplay an alert panel about the bad permission of folder/directory. + // + displayBadPermissionAlert: function () { + let bundle = + Services.strings.createBundle("chrome://mozapps/locale/downloads/unknownContentType.properties"); + + Services.prompt.alert(this.dialog, + bundle.GetStringFromName("badPermissions.title"), + bundle.GetStringFromName("badPermissions")); + }, + + promptForSaveToFileAsync: function(aLauncher, aContext, aDefaultFile, aSuggestedFileExtension, aForcePrompt) { + var result = null; + + this.mLauncher = aLauncher; + + let prefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + let bundle = + Services.strings + .createBundle("chrome://mozapps/locale/downloads/unknownContentType.properties"); + + let parent; + let gDownloadLastDir; + try { + parent = aContext.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); + } catch (ex) {} + + if (parent) { + gDownloadLastDir = new downloadModule.DownloadLastDir(parent); + } else { + // Use the cached download info, but pick an arbitrary parent window + // because the original one is definitely gone (and nsIFilePicker doesn't like + // a null parent): + gDownloadLastDir = this._mDownloadDir; + let windowsEnum = Services.wm.getEnumerator(""); + while (windowsEnum.hasMoreElements()) { + let someWin = windowsEnum.getNext(); + // We need to make sure we don't end up with this dialog, because otherwise + // that's going to go away when the user clicks "Save", and that breaks the + // windows file picker that's supposed to show up if we let the user choose + // where to save files... + if (someWin != this.mDialog) { + parent = someWin; + } + } + if (!parent) { + Cu.reportError("No candidate parent windows were found for the save filepicker." + + "This should never happen."); + } + } + + Task.spawn(function() { + if (!aForcePrompt) { + // Check to see if the user wishes to auto save to the default download + // folder without prompting. Note that preference might not be set. + let autodownload = prefs.getBoolPref(PREF_BD_USEDOWNLOADDIR, false); + + if (autodownload) { + // Retrieve the user's default download directory + let preferredDir = yield Downloads.getPreferredDownloadsDirectory(); + let defaultFolder = new FileUtils.File(preferredDir); + + try { + result = this.validateLeafName(defaultFolder, aDefaultFile, aSuggestedFileExtension); + } + catch (ex) { + // When the default download directory is write-protected, + // prompt the user for a different target file. + } + + // Check to make sure we have a valid directory, otherwise, prompt + if (result) { + // This path is taken when we have a writable default download directory. + aLauncher.saveDestinationAvailable(result); + return; + } + } + } + + // Use file picker to show dialog. + var nsIFilePicker = Components.interfaces.nsIFilePicker; + var picker = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker); + var windowTitle = bundle.GetStringFromName("saveDialogTitle"); + picker.init(parent, windowTitle, nsIFilePicker.modeSave); + picker.defaultString = aDefaultFile; + + if (aSuggestedFileExtension) { + // aSuggestedFileExtension includes the period, so strip it + picker.defaultExtension = aSuggestedFileExtension.substring(1); + } + else { + try { + picker.defaultExtension = this.mLauncher.MIMEInfo.primaryExtension; + } + catch (ex) { } + } + + var wildCardExtension = "*"; + if (aSuggestedFileExtension) { + wildCardExtension += aSuggestedFileExtension; + picker.appendFilter(this.mLauncher.MIMEInfo.description, wildCardExtension); + } + + picker.appendFilters( nsIFilePicker.filterAll ); + + // Default to lastDir if it is valid, otherwise use the user's default + // downloads directory. getPreferredDownloadsDirectory should always + // return a valid directory path, so we can safely default to it. + let preferredDir = yield Downloads.getPreferredDownloadsDirectory(); + picker.displayDirectory = new FileUtils.File(preferredDir); + + gDownloadLastDir.getFileAsync(aLauncher.source, function LastDirCallback(lastDir) { + if (lastDir && isUsableDirectory(lastDir)) + picker.displayDirectory = lastDir; + + if (picker.show() == nsIFilePicker.returnCancel) { + // null result means user cancelled. + aLauncher.saveDestinationAvailable(null); + return; + } + + // Be sure to save the directory the user chose through the Save As... + // dialog as the new browser.download.dir since the old one + // didn't exist. + result = picker.file; + + if (result) { + try { + // Remove the file so that it's not there when we ensure non-existence later; + // this is safe because for the file to exist, the user would have had to + // confirm that he wanted the file overwritten. + // Only remove file if final name exists + if (result.exists() && this.getFinalLeafName(result.leafName) == result.leafName) + result.remove(false); + } + catch (ex) { + // As it turns out, the failure to remove the file, for example due to + // permission error, will be handled below eventually somehow. + } + + var newDir = result.parent.QueryInterface(Components.interfaces.nsILocalFile); + + // Do not store the last save directory as a pref inside the private browsing mode + gDownloadLastDir.setFile(aLauncher.source, newDir); + + try { + result = this.validateLeafName(newDir, result.leafName, null); + } + catch (ex) { + // When the chosen download directory is write-protected, + // display an informative error message. + // In all cases, download will be stopped. + + if (ex.result == Components.results.NS_ERROR_FILE_ACCESS_DENIED) { + this.displayBadPermissionAlert(); + aLauncher.saveDestinationAvailable(null); + return; + } + + } + } + aLauncher.saveDestinationAvailable(result); + }.bind(this)); + }.bind(this)).then(null, Components.utils.reportError); + }, + + getFinalLeafName: function (aLeafName, aFileExt) + { + // Remove any leading periods, since we don't want to save hidden files + // automatically. + aLeafName = aLeafName.replace(/^\.+/, ""); + + if (aLeafName == "") + aLeafName = "unnamed" + (aFileExt ? "." + aFileExt : ""); + + return aLeafName; + }, + + /** + * Ensures that a local folder/file combination does not already exist in + * the file system (or finds such a combination with a reasonably similar + * leaf name), creates the corresponding file, and returns it. + * + * @param aLocalFolder + * the folder where the file resides + * @param aLeafName + * the string name of the file (may be empty if no name is known, + * in which case a name will be chosen) + * @param aFileExt + * the extension of the file, if one is known; this will be ignored + * if aLeafName is non-empty + * @return nsILocalFile + * the created file + * @throw an error such as permission doesn't allow creation of + * file, etc. + */ + validateLeafName: function (aLocalFolder, aLeafName, aFileExt) + { + if (!(aLocalFolder && isUsableDirectory(aLocalFolder))) { + throw new Components.Exception("Destination directory non-existing or permission error", + Components.results.NS_ERROR_FILE_ACCESS_DENIED); + } + + aLeafName = this.getFinalLeafName(aLeafName, aFileExt); + aLocalFolder.append(aLeafName); + + // The following assignment can throw an exception, but + // is now caught properly in the caller of validateLeafName. + var createdFile = DownloadPaths.createNiceUniqueFile(aLocalFolder); + +#ifdef XP_WIN + let ext; + try { + // We can fail here if there's no primary extension set + ext = "." + this.mLauncher.MIMEInfo.primaryExtension; + } catch (e) { } + + // Append a file extension if it's an executable that doesn't have one + // but make sure we actually have an extension to add + let leaf = createdFile.leafName; + if (ext && leaf.slice(-ext.length) != ext && createdFile.isExecutable()) { + createdFile.remove(false); + aLocalFolder.leafName = leaf + ext; + createdFile = DownloadPaths.createNiceUniqueFile(aLocalFolder); + } +#endif + + return createdFile; + }, + + // ---------- implementation methods ---------- + + // initDialog: Fill various dialog fields with initial content. + initDialog : function() { + // Put file name in window title. + var suggestedFileName = this.mLauncher.suggestedFileName; + + // Some URIs do not implement nsIURL, so we can't just QI. + var url = this.mLauncher.source; + if (url instanceof Components.interfaces.nsINestedURI) + url = url.innermostURI; + + var fname = ""; + var iconPath = "goat"; + this.mSourcePath = url.prePath; + if (url instanceof Components.interfaces.nsIURL) { + // A url, use file name from it. + fname = iconPath = url.fileName; + this.mSourcePath += url.directory; + } else { + // A generic uri, use path. + fname = url.path; + this.mSourcePath += url.path; + } + + if (suggestedFileName) + fname = iconPath = suggestedFileName; + + var displayName = fname.replace(/ +/g, " "); + + this.mTitle = this.dialogElement("strings").getFormattedString("title", [displayName]); + this.mDialog.document.title = this.mTitle; + + // Put content type, filename and location into intro. + this.initIntro(url, fname, displayName); + + var iconString = "moz-icon://" + iconPath + "?size=16&contentType=" + this.mLauncher.MIMEInfo.MIMEType; + this.dialogElement("contentTypeImage").setAttribute("src", iconString); + + // if always-save and is-executable and no-handler + // then set up simple ui + var mimeType = this.mLauncher.MIMEInfo.MIMEType; + var shouldntRememberChoice = (mimeType == "application/octet-stream" || + mimeType == "application/x-msdownload" || + this.mLauncher.targetFileIsExecutable); + if ((shouldntRememberChoice && !this.openWithDefaultOK()) || + Services.prefs.getBoolPref("browser.download.forbid_open_with")) { + // hide featured choice + this.dialogElement("normalBox").collapsed = true; + // show basic choice + this.dialogElement("basicBox").collapsed = false; + // change button labels and icons; use "save" icon for the accept + // button since it's the only action possible + let acceptButton = this.mDialog.document.documentElement + .getButton("accept"); + acceptButton.label = this.dialogElement("strings") + .getString("unknownAccept.label"); + acceptButton.setAttribute("icon", "save"); + this.mDialog.document.documentElement.getButton("cancel").label = this.dialogElement("strings").getString("unknownCancel.label"); + // hide other handler + this.dialogElement("openHandler").collapsed = true; + // set save as the selected option + this.dialogElement("mode").selectedItem = this.dialogElement("save"); + } + else { + this.initAppAndSaveToDiskValues(); + + // Initialize "always ask me" box. This should always be disabled + // and set to true for the ambiguous type application/octet-stream. + // We don't also check for application/x-msdownload here since we + // want users to be able to autodownload .exe files. + var rememberChoice = this.dialogElement("rememberChoice"); + + // Just because we have a content-type of application/octet-stream + // here doesn't actually mean that the content is of that type. Many + // servers default to sending text/plain for file types they don't know + // about. To account for this, the uriloader does some checking to see + // if a file sent as text/plain contains binary characters, and if so (*) + // it morphs the content-type into application/octet-stream so that + // the file can be properly handled. Since this is not generic binary + // data, rather, a data format that the system probably knows about, + // we don't want to use the content-type provided by this dialog's + // opener, as that's the generic application/octet-stream that the + // uriloader has passed, rather we want to ask the MIME Service. + // This is so we don't needlessly disable the "autohandle" checkbox. + + // commented out to close the opening brace in the if statement. + // var mimeService = Components.classes["@mozilla.org/mime;1"].getService(Components.interfaces.nsIMIMEService); + // var type = mimeService.getTypeFromURI(this.mLauncher.source); + // this.realMIMEInfo = mimeService.getFromTypeAndExtension(type, ""); + + // if (type == "application/octet-stream") { + if (shouldntRememberChoice) { + rememberChoice.checked = false; + rememberChoice.disabled = true; + } + else { + rememberChoice.checked = !this.mLauncher.MIMEInfo.alwaysAskBeforeHandling && + this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.handleInternally; + } + this.toggleRememberChoice(rememberChoice); + + // XXXben - menulist won't init properly, hack. + var openHandler = this.dialogElement("openHandler"); + openHandler.parentNode.removeChild(openHandler); + var openHandlerBox = this.dialogElement("openHandlerBox"); + openHandlerBox.appendChild(openHandler); + } + + this.mDialog.setTimeout("dialog.postShowCallback()", 0); + + this.delayHelper = new EnableDelayHelper({ + disableDialog: () => { + this.mDialog.document.documentElement.getButton("accept").disabled = true; + }, + enableDialog: () => { + this.mDialog.document.documentElement.getButton("accept").disabled = false; + }, + focusTarget: this.mDialog + }); + }, + + notify: function (aTimer) { + if (aTimer == this._showTimer) { + if (!this.mDialog) { + this.reallyShow(); + } + // The timer won't release us, so we have to release it. + this._showTimer = null; + } + else if (aTimer == this._saveToDiskTimer) { + // Since saveToDisk may open a file picker and therefore block this routine, + // we should only call it once the dialog is closed. + this.mLauncher.saveToDisk(null, false); + this._saveToDiskTimer = null; + } + }, + + postShowCallback: function () { + this.mDialog.sizeToContent(); + + // Set initial focus + this.dialogElement("mode").focus(); + }, + + // initIntro: + initIntro: function(url, filename, displayname) { + this.dialogElement( "location" ).value = displayname; + this.dialogElement( "location" ).setAttribute("realname", filename); + this.dialogElement( "location" ).setAttribute("tooltiptext", displayname); + + // if mSourcePath is a local file, then let's use the pretty path name + // instead of an ugly url... + var pathString; + if (url instanceof Components.interfaces.nsIFileURL) { + try { + // Getting .file might throw, or .parent could be null + pathString = url.file.parent.path; + } catch (ex) {} + } + + if (!pathString) { + // wasn't a fileURL + var tmpurl = url.clone(); // don't want to change the real url + try { + tmpurl.userPass = ""; + } catch (ex) {} + pathString = tmpurl.prePath; + } + + // Set the location text, which is separate from the intro text so it can be cropped + var location = this.dialogElement( "source" ); + location.value = pathString; + location.setAttribute("tooltiptext", this.mSourcePath); + + // Show the type of file. + var type = this.dialogElement("type"); + var mimeInfo = this.mLauncher.MIMEInfo; + + // 1. Try to use the pretty description of the type, if one is available. + var typeString = mimeInfo.description; + + if (typeString == "") { + // 2. If there is none, use the extension to identify the file, e.g. "ZIP file" + var primaryExtension = ""; + try { + primaryExtension = mimeInfo.primaryExtension; + } + catch (ex) { + } + if (primaryExtension != "") + typeString = this.dialogElement("strings").getFormattedString("fileType", [primaryExtension.toUpperCase()]); + // 3. If we can't even do that, just give up and show the MIME type. + else + typeString = mimeInfo.MIMEType; + } + // When the length is unknown, contentLength would be -1 + if (this.mLauncher.contentLength >= 0) { + let [size, unit] = DownloadUtils. + convertByteUnits(this.mLauncher.contentLength); + type.value = this.dialogElement("strings") + .getFormattedString("orderedFileSizeWithType", + [typeString, size, unit]); + } + else { + type.value = typeString; + } + }, + + // Returns true if opening the default application makes sense. + openWithDefaultOK: function() { + // The checking is different on Windows... +#ifdef XP_WIN + // Windows presents some special cases. + // We need to prevent use of "system default" when the file is + // executable (so the user doesn't launch nasty programs downloaded + // from the web), and, enable use of "system default" if it isn't + // executable (because we will prompt the user for the default app + // in that case). + + // Default is Ok if the file isn't executable (and vice-versa). + return !this.mLauncher.targetFileIsExecutable; +#else + // On other platforms, default is Ok if there is a default app. + // Note that nsIMIMEInfo providers need to ensure that this holds true + // on each platform. + return this.mLauncher.MIMEInfo.hasDefaultHandler; +#endif + }, + + // Set "default" application description field. + initDefaultApp: function() { + // Use description, if we can get one. + var desc = this.mLauncher.MIMEInfo.defaultDescription; + if (desc) { + var defaultApp = this.dialogElement("strings").getFormattedString("defaultApp", [desc]); + this.dialogElement("defaultHandler").label = defaultApp; + } + else { + this.dialogElement("modeDeck").setAttribute("selectedIndex", "1"); + // Hide the default handler item too, in case the user picks a + // custom handler at a later date which triggers the menulist to show. + this.dialogElement("defaultHandler").hidden = true; + } + }, + + // getPath: + getPath: function (aFile) { + return aFile.path; + }, + + // initAppAndSaveToDiskValues: + initAppAndSaveToDiskValues: function() { + var modeGroup = this.dialogElement("mode"); + + // We don't let users open .exe files or random binary data directly + // from the browser at the moment because of security concerns. + var openWithDefaultOK = this.openWithDefaultOK(); + var mimeType = this.mLauncher.MIMEInfo.MIMEType; + if (this.mLauncher.targetFileIsExecutable || ( + (mimeType == "application/octet-stream" || + mimeType == "application/x-msdownload") && + !openWithDefaultOK)) { + this.dialogElement("open").disabled = true; + var openHandler = this.dialogElement("openHandler"); + openHandler.disabled = true; + openHandler.selectedItem = null; + modeGroup.selectedItem = this.dialogElement("save"); + return; + } + + // Fill in helper app info, if there is any. + try { + this.chosenApp = + this.mLauncher.MIMEInfo.preferredApplicationHandler + .QueryInterface(Components.interfaces.nsILocalHandlerApp); + } catch (e) { + this.chosenApp = null; + } + // Initialize "default application" field. + this.initDefaultApp(); + + var otherHandler = this.dialogElement("otherHandler"); + + // Fill application name textbox. + if (this.chosenApp && this.chosenApp.executable && + this.chosenApp.executable.path) { + otherHandler.setAttribute("path", + this.getPath(this.chosenApp.executable)); + + otherHandler.label = this.getFileDisplayName(this.chosenApp.executable); + otherHandler.hidden = false; + } + + var openHandler = this.dialogElement("openHandler"); + openHandler.selectedIndex = 0; + var defaultOpenHandler = this.dialogElement("defaultHandler"); + + if (this.mLauncher.MIMEInfo.preferredAction == this.nsIMIMEInfo.useSystemDefault) { + // Open (using system default). + modeGroup.selectedItem = this.dialogElement("open"); + } else if (this.mLauncher.MIMEInfo.preferredAction == this.nsIMIMEInfo.useHelperApp) { + // Open with given helper app. + modeGroup.selectedItem = this.dialogElement("open"); + openHandler.selectedItem = (otherHandler && !otherHandler.hidden) ? + otherHandler : defaultOpenHandler; + } else { + // Save to disk. + modeGroup.selectedItem = this.dialogElement("save"); + } + + // If we don't have a "default app" then disable that choice. + if (!openWithDefaultOK) { + var isSelected = defaultOpenHandler.selected; + + // Disable that choice. + defaultOpenHandler.hidden = true; + // If that's the default, then switch to "save to disk." + if (isSelected) { + openHandler.selectedIndex = 1; + modeGroup.selectedItem = this.dialogElement("save"); + } + } + + otherHandler.nextSibling.hidden = otherHandler.nextSibling.nextSibling.hidden = false; + this.updateOKButton(); + }, + + // Returns the user-selected application + helperAppChoice: function() { + return this.chosenApp; + }, + + get saveToDisk() { + return this.dialogElement("save").selected; + }, + + get useOtherHandler() { + return this.dialogElement("open").selected && this.dialogElement("openHandler").selectedIndex == 1; + }, + + get useSystemDefault() { + return this.dialogElement("open").selected && this.dialogElement("openHandler").selectedIndex == 0; + }, + + toggleRememberChoice: function (aCheckbox) { + this.dialogElement("settingsChange").hidden = !aCheckbox.checked; + this.mDialog.sizeToContent(); + }, + + openHandlerCommand: function () { + var openHandler = this.dialogElement("openHandler"); + if (openHandler.selectedItem.id == "choose") + this.chooseApp(); + else + openHandler.setAttribute("lastSelectedItemID", openHandler.selectedItem.id); + }, + + updateOKButton: function() { + var ok = false; + if (this.dialogElement("save").selected) { + // This is always OK. + ok = true; + } + else if (this.dialogElement("open").selected) { + switch (this.dialogElement("openHandler").selectedIndex) { + case 0: + // No app need be specified in this case. + ok = true; + break; + case 1: + // only enable the OK button if we have a default app to use or if + // the user chose an app.... + ok = this.chosenApp || /\S/.test(this.dialogElement("otherHandler").getAttribute("path")); + break; + } + } + + // Enable Ok button if ok to press. + this.mDialog.document.documentElement.getButton("accept").disabled = !ok; + }, + + // Returns true iff the user-specified helper app has been modified. + appChanged: function() { + return this.helperAppChoice() != this.mLauncher.MIMEInfo.preferredApplicationHandler; + }, + + updateMIMEInfo: function() { + // Don't update mime type preferences when the preferred action is set to + // the internal handler -- this dialog is the result of the handler fallback + // (e.g. Content-Disposition was set as attachment) + var discardUpdate = this.mLauncher.MIMEInfo.preferredAction == this.nsIMIMEInfo.handleInternally && + !this.dialogElement("rememberChoice").checked; + + var needUpdate = false; + // If current selection differs from what's in the mime info object, + // then we need to update. + if (this.saveToDisk) { + needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.saveToDisk; + if (needUpdate) + this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.saveToDisk; + } + else if (this.useSystemDefault) { + needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.useSystemDefault; + if (needUpdate) + this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.useSystemDefault; + } + else { + // For "open with", we need to check both preferred action and whether the user chose + // a new app. + needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.useHelperApp || this.appChanged(); + if (needUpdate) { + this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.useHelperApp; + // App may have changed - Update application + var app = this.helperAppChoice(); + this.mLauncher.MIMEInfo.preferredApplicationHandler = app; + } + } + // We will also need to update if the "always ask" flag has changed. + needUpdate = needUpdate || this.mLauncher.MIMEInfo.alwaysAskBeforeHandling != (!this.dialogElement("rememberChoice").checked); + + // One last special case: If the input "always ask" flag was false, then we always + // update. In that case we are displaying the helper app dialog for the first + // time for this mime type and we need to store the user's action in the mimeTypes.rdf + // data source (whether that action has changed or not; if it didn't change, then we need + // to store the "always ask" flag so the helper app dialog will or won't display + // next time, per the user's selection). + needUpdate = needUpdate || !this.mLauncher.MIMEInfo.alwaysAskBeforeHandling; + + // Make sure mime info has updated setting for the "always ask" flag. + this.mLauncher.MIMEInfo.alwaysAskBeforeHandling = !this.dialogElement("rememberChoice").checked; + + return needUpdate && !discardUpdate; + }, + + // See if the user changed things, and if so, update the + // mimeTypes.rdf entry for this mime type. + updateHelperAppPref: function() { + var handlerInfo = this.mLauncher.MIMEInfo; + var hs = Cc["@mozilla.org/uriloader/handler-service;1"].getService(Ci.nsIHandlerService); + hs.store(handlerInfo); + }, + + // onOK: + onOK: function() { + // Verify typed app path, if necessary. + if (this.useOtherHandler) { + var helperApp = this.helperAppChoice(); + if (!helperApp || !helperApp.executable || + !helperApp.executable.exists()) { + // Show alert and try again. + var bundle = this.dialogElement("strings"); + var msg = bundle.getFormattedString("badApp", [this.dialogElement("otherHandler").getAttribute("path")]); + Services.prompt.alert(this.mDialog, bundle.getString("badApp.title"), msg); + + // Disable the OK button. + this.mDialog.document.documentElement.getButton("accept").disabled = true; + this.dialogElement("mode").focus(); + + // Clear chosen application. + this.chosenApp = null; + + // Leave dialog up. + return false; + } + } + + // Remove our web progress listener (a progress dialog will be + // taking over). + this.mLauncher.setWebProgressListener(null); + + // saveToDisk and launchWithApplication can return errors in + // certain circumstances (e.g. The user clicks cancel in the + // "Save to Disk" dialog. In those cases, we don't want to + // update the helper application preferences in the RDF file. + try { + var needUpdate = this.updateMIMEInfo(); + + if (this.dialogElement("save").selected) { + // If we're using a default download location, create a path + // for the file to be saved to to pass to |saveToDisk| - otherwise + // we must ask the user to pick a save name. + + /* + var prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch); + var targetFile = null; + try { + targetFile = prefs.getComplexValue("browser.download.defaultFolder", + Components.interfaces.nsILocalFile); + var leafName = this.dialogElement("location").getAttribute("realname"); + // Ensure that we don't overwrite any existing files here. + targetFile = this.validateLeafName(targetFile, leafName, null); + } + catch(e) { } + + this.mLauncher.saveToDisk(targetFile, false); + */ + + // see @notify + // we cannot use opener's setTimeout, see bug 420405 + this._saveToDiskTimer = Components.classes["@mozilla.org/timer;1"] + .createInstance(nsITimer); + this._saveToDiskTimer.initWithCallback(this, 0, + nsITimer.TYPE_ONE_SHOT); + } + else + this.mLauncher.launchWithApplication(null, false); + + // Update user pref for this mime type (if necessary). We do not + // store anything in the mime type preferences for the ambiguous + // type application/octet-stream. We do NOT do this for + // application/x-msdownload since we want users to be able to + // autodownload these to disk. + if (needUpdate && this.mLauncher.MIMEInfo.MIMEType != "application/octet-stream") + this.updateHelperAppPref(); + } catch(e) { } + + // Unhook dialog from this object. + this.mDialog.dialog = null; + + // Close up dialog by returning true. + return true; + }, + + // onCancel: + onCancel: function() { + // Remove our web progress listener. + this.mLauncher.setWebProgressListener(null); + + // Cancel app launcher. + try { + this.mLauncher.cancel(Components.results.NS_BINDING_ABORTED); + } catch(exception) { + } + + // Unhook dialog from this object. + this.mDialog.dialog = null; + + // Close up dialog by returning true. + return true; + }, + + // dialogElement: Convenience. + dialogElement: function(id) { + return this.mDialog.document.getElementById(id); + }, + + // Retrieve the pretty description from the file + getFileDisplayName: function getFileDisplayName(file) + { +#ifdef XP_WIN + if (file instanceof Components.interfaces.nsILocalFileWin) { + try { + return file.getVersionInfoField("FileDescription"); + } catch (e) {} + } +#endif + + return file.leafName; + }, + + finishChooseApp: function() { + if (this.chosenApp) { + // Show the "handler" menulist since we have a (user-specified) + // application now. + this.dialogElement("modeDeck").setAttribute("selectedIndex", "0"); + + // Update dialog. + var otherHandler = this.dialogElement("otherHandler"); + otherHandler.removeAttribute("hidden"); + otherHandler.setAttribute("path", this.getPath(this.chosenApp.executable)); + +#ifdef XP_WIN + otherHandler.label = this.getFileDisplayName(this.chosenApp.executable); +#else + otherHandler.label = this.chosenApp.name; +#endif + + this.dialogElement("openHandler").selectedIndex = 1; + this.dialogElement("openHandler").setAttribute("lastSelectedItemID", "otherHandler"); + + this.dialogElement("mode").selectedItem = this.dialogElement("open"); + } + else { + var openHandler = this.dialogElement("openHandler"); + var lastSelectedID = openHandler.getAttribute("lastSelectedItemID"); + if (!lastSelectedID) + lastSelectedID = "defaultHandler"; + openHandler.selectedItem = this.dialogElement(lastSelectedID); + } + }, + // chooseApp: Open file picker and prompt user for application. + chooseApp: function() { +#ifdef XP_WIN + // Protect against the lack of an extension + var fileExtension = ""; + try { + fileExtension = this.mLauncher.MIMEInfo.primaryExtension; + } catch(ex) { + } + + // Try to use the pretty description of the type, if one is available. + var typeString = this.mLauncher.MIMEInfo.description; + + if (!typeString) { + // If there is none, use the extension to + // identify the file, e.g. "ZIP file" + if (fileExtension) { + typeString = + this.dialogElement("strings"). + getFormattedString("fileType", [fileExtension.toUpperCase()]); + } else { + // If we can't even do that, just give up and show the MIME type. + typeString = this.mLauncher.MIMEInfo.MIMEType; + } + } + + var params = {}; + params.title = + this.dialogElement("strings").getString("chooseAppFilePickerTitle"); + params.description = typeString; + params.filename = this.mLauncher.suggestedFileName; + params.mimeInfo = this.mLauncher.MIMEInfo; + params.handlerApp = null; + + this.mDialog.openDialog("chrome://global/content/appPicker.xul", null, + "chrome,modal,centerscreen,titlebar,dialog=yes", + params); + + if (params.handlerApp && + params.handlerApp.executable && + params.handlerApp.executable.isFile()) { + // Remember the file they chose to run. + this.chosenApp = params.handlerApp; + } +#else // XP_WIN +#if MOZ_WIDGET_GTK == 3 + var nsIApplicationChooser = Components.interfaces.nsIApplicationChooser; + var appChooser = Components.classes["@mozilla.org/applicationchooser;1"] + .createInstance(nsIApplicationChooser); + appChooser.init(this.mDialog, this.dialogElement("strings").getString("chooseAppFilePickerTitle")); + var contentTypeDialogObj = this; + let appChooserCallback = function appChooserCallback_done(aResult) { + if (aResult) { + contentTypeDialogObj.chosenApp = aResult.QueryInterface(Components.interfaces.nsILocalHandlerApp); + } + contentTypeDialogObj.finishChooseApp(); + }; + appChooser.open(this.mLauncher.MIMEInfo.MIMEType, appChooserCallback); + // The finishChooseApp is called from appChooserCallback + return; +#else // MOZ_WIDGET_GTK == 3 + var nsIFilePicker = Components.interfaces.nsIFilePicker; + var fp = Components.classes["@mozilla.org/filepicker;1"] + .createInstance(nsIFilePicker); + fp.init(this.mDialog, + this.dialogElement("strings").getString("chooseAppFilePickerTitle"), + nsIFilePicker.modeOpen); + + fp.appendFilters(nsIFilePicker.filterApps); + + if (fp.show() == nsIFilePicker.returnOK && fp.file) { + // Remember the file they chose to run. + var localHandlerApp = + Components.classes["@mozilla.org/uriloader/local-handler-app;1"]. + createInstance(Components.interfaces.nsILocalHandlerApp); + localHandlerApp.executable = fp.file; + this.chosenApp = localHandlerApp; + } +#endif // MOZ_WIDGET_GTK == 3 +#endif // XP_WIN + this.finishChooseApp(); + }, + + // Turn this on to get debugging messages. + debug: false, + + // Dump text (if debug is on). + dump: function( text ) { + if ( this.debug ) { + dump( text ); + } + }, + + // dumpObj: + dumpObj: function( spec ) { + var val = "<undefined>"; + try { + val = eval( "this."+spec ).toString(); + } catch( exception ) { + } + this.dump( spec + "=" + val + "\n" ); + }, + + // dumpObjectProperties + dumpObjectProperties: function( desc, obj ) { + for( prop in obj ) { + this.dump( desc + "." + prop + "=" ); + var val = "<undefined>"; + try { + val = obj[ prop ]; + } catch ( exception ) { + } + this.dump( val + "\n" ); + } + } +} + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([nsUnknownContentTypeDialog]); |