import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

@ -0,0 +1,18 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
with Files('*'):
BUG_COMPONENT = ('Toolkit', 'Download Manager')
DIRS += ['public', 'src']
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
TEST_HARNESS_FILES.xpcshell.toolkit.components.jsdownloads.test.data += [
'test/data/empty.txt',
'test/data/source.txt',
]

View file

@ -0,0 +1,9 @@
# 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/.
XPIDL_MODULE = 'jsdownloads'
XPIDL_SOURCES += [
'mozIDownloadPlatform.idl',
]

View file

@ -0,0 +1,61 @@
/* 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 nsIURI;
interface nsIFile;
[scriptable, uuid(9f556e4a-d9b3-46c3-9f8f-d0db1ac6c8c1)]
interface mozIDownloadPlatform : nsISupports
{
/**
* Perform platform specific operations when a download is done.
*
* Windows:
* Add the download to the recent documents list
* Set the file to be indexed for searching
* Mac:
* Bounce the downloads dock icon
* GTK:
* Add the download to the recent documents list
* Save the source uri in the downloaded file's metadata
* Android:
* Scan media
*
* @param aSource
* Source URI of the download
* @param aReferrer
* Referrer URI of the download
* @param aTarget
* Downloaded file
* @param aContentType
* The source's content type
* @param aIsPrivate
* True for private downloads
* @return none
*/
void downloadDone(in nsIURI aSource, in nsIURI aReferrer, in nsIFile aTarget,
in ACString aContentType, in boolean aIsPrivate);
/**
* Security Zone constants. Used by mapUrlToZone().
*/
const unsigned long ZONE_MY_COMPUTER = 0;
const unsigned long ZONE_INTRANET = 1;
const unsigned long ZONE_TRUSTED = 2;
const unsigned long ZONE_INTERNET = 3;
const unsigned long ZONE_RESTRICTED = 4;
/**
* Proxy for IInternetSecurityManager::MapUrlToZone().
*
* Windows only.
*
* @param aURL
* URI of the download
* @return Security Zone corresponding to aURL.
*/
unsigned long mapUrlToZone(in AString aURL);
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,193 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = [
"DownloadImport",
];
// Globals
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, "Downloads",
"resource://gre/modules/Downloads.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm")
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
"resource://gre/modules/Sqlite.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
/**
* These values come from the previous interface
* nsIDownloadManager, which has now been deprecated.
* These are the only types of download states that
* we will import.
*/
const DOWNLOAD_NOTSTARTED = -1;
const DOWNLOAD_DOWNLOADING = 0;
const DOWNLOAD_PAUSED = 4;
const DOWNLOAD_QUEUED = 5;
// DownloadImport
/**
* Provides an object that has a method to import downloads
* from the previous SQLite storage format.
*
* @param aList A DownloadList where each successfully
* imported download will be added.
* @param aPath The path to the database file.
*/
this.DownloadImport = function (aList, aPath)
{
this.list = aList;
this.path = aPath;
}
this.DownloadImport.prototype = {
/**
* Imports unfinished downloads from the previous SQLite storage
* format (supporting schemas 7 and up), to the new Download object
* format. Each imported download will be added to the DownloadList
*
* @return {Promise}
* @resolves When the operation has completed (i.e., every download
* from the previous database has been read and added to
* the DownloadList)
*/
import: function () {
return Task.spawn(function* task_DI_import() {
let connection = yield Sqlite.openConnection({ path: this.path });
try {
let schemaVersion = yield connection.getSchemaVersion();
// We don't support schemas older than version 7 (from 2007)
// - Version 7 added the columns mimeType, preferredApplication
// and preferredAction in 2007
// - Version 8 added the column autoResume in 2007
// (if we encounter version 7 we will treat autoResume = false)
// - Version 9 is the last known version, which added a unique
// GUID text column that is not used here
if (schemaVersion < 7) {
throw new Error("Unable to import in-progress downloads because "
+ "the existing profile is too old.");
}
let rows = yield connection.execute("SELECT * FROM moz_downloads");
for (let row of rows) {
try {
// Get the DB row data
let source = row.getResultByName("source");
let target = row.getResultByName("target");
let tempPath = row.getResultByName("tempPath");
let startTime = row.getResultByName("startTime");
let state = row.getResultByName("state");
let referrer = row.getResultByName("referrer");
let maxBytes = row.getResultByName("maxBytes");
let mimeType = row.getResultByName("mimeType");
let preferredApplication = row.getResultByName("preferredApplication");
let preferredAction = row.getResultByName("preferredAction");
let entityID = row.getResultByName("entityID");
let autoResume = false;
try {
autoResume = (row.getResultByName("autoResume") == 1);
} catch (ex) {
// autoResume wasn't present in schema version 7
}
if (!source) {
throw new Error("Attempted to import a row with an empty " +
"source column.");
}
let resumeDownload = false;
switch (state) {
case DOWNLOAD_NOTSTARTED:
case DOWNLOAD_QUEUED:
case DOWNLOAD_DOWNLOADING:
resumeDownload = true;
break;
case DOWNLOAD_PAUSED:
resumeDownload = autoResume;
break;
default:
// We won't import downloads in other states
continue;
}
// Transform the data
let targetPath = NetUtil.newURI(target)
.QueryInterface(Ci.nsIFileURL).file.path;
let launchWhenSucceeded = (preferredAction != Ci.nsIMIMEInfo.saveToDisk);
let downloadOptions = {
source: {
url: source,
referrer: referrer
},
target: {
path: targetPath,
partFilePath: tempPath,
},
saver: {
type: "copy",
entityID: entityID
},
startTime: new Date(startTime / 1000),
totalBytes: maxBytes,
hasPartialData: !!tempPath,
tryToKeepPartialData: true,
launchWhenSucceeded: launchWhenSucceeded,
contentType: mimeType,
launcherPath: preferredApplication
};
// Paused downloads that should not be auto-resumed are considered
// in a "canceled" state.
if (!resumeDownload) {
downloadOptions.canceled = true;
}
let download = yield Downloads.createDownload(downloadOptions);
yield this.list.add(download);
if (resumeDownload) {
download.start().catch(() => {});
} else {
yield download.refresh();
}
} catch (ex) {
Cu.reportError("Error importing download: " + ex);
}
}
} catch (ex) {
Cu.reportError(ex);
} finally {
yield connection.close();
}
}.bind(this));
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,309 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* 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 component implements the XPCOM interfaces required for integration with
* the legacy download components.
*
* New code is expected to use the "Downloads.jsm" module directly, without
* going through the interfaces implemented in this XPCOM component. These
* interfaces are only maintained for backwards compatibility with components
* that still work synchronously on the main thread.
*/
"use strict";
// Globals
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, "Downloads",
"resource://gre/modules/Downloads.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
// DownloadLegacyTransfer
/**
* nsITransfer implementation that provides a bridge to a Download object.
*
* Legacy downloads work differently than the JavaScript implementation. In the
* latter, the caller only provides the properties for the Download object and
* the entire process is handled by the "start" method. In the legacy
* implementation, the caller must create a separate object to execute the
* download, and then make the download visible to the user by hooking it up to
* an nsITransfer instance.
*
* Since nsITransfer instances may be created before the download system is
* initialized, and initialization as well as other operations are asynchronous,
* this implementation is able to delay all progress and status notifications it
* receives until the associated Download object is finally created.
*
* Conversely, the DownloadLegacySaver object can also receive execution and
* cancellation requests asynchronously, before or after it is connected to
* this nsITransfer instance. For that reason, those requests are communicated
* in a potentially deferred way, using promise objects.
*
* The component that executes the download implements nsICancelable to receive
* cancellation requests, but after cancellation it cannot be reused again.
*
* Since the components that execute the download may be different and they
* don't always give consistent results, this bridge takes care of enforcing the
* expectations, for example by ensuring the target file exists when the
* download is successful, even if the source has a size of zero bytes.
*/
function DownloadLegacyTransfer()
{
this._deferDownload = Promise.defer();
}
DownloadLegacyTransfer.prototype = {
classID: Components.ID("{1b4c85df-cbdd-4bb6-b04e-613caece083c}"),
// nsISupports
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
Ci.nsIWebProgressListener2,
Ci.nsITransfer]),
// nsIWebProgressListener
onStateChange: function DLT_onStateChange(aWebProgress, aRequest, aStateFlags,
aStatus)
{
if (!Components.isSuccessCode(aStatus)) {
this._componentFailed = true;
}
if ((aStateFlags & Ci.nsIWebProgressListener.STATE_START) &&
(aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
let blockedByParentalControls = false;
// If it is a failed download, aRequest.responseStatus doesn't exist.
// (missing file on the server, network failure to download)
try {
// If the request's response has been blocked by Windows Parental Controls
// with an HTTP 450 error code, we must cancel the request synchronously.
blockedByParentalControls = aRequest instanceof Ci.nsIHttpChannel &&
aRequest.responseStatus == 450;
} catch (e) {
if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
aRequest.cancel(Cr.NS_BINDING_ABORTED);
}
}
if (blockedByParentalControls) {
aRequest.cancel(Cr.NS_BINDING_ABORTED);
}
// The main request has just started. Wait for the associated Download
// object to be available before notifying.
this._deferDownload.promise.then(download => {
// If the request was blocked, now that we have the download object we
// should set a flag that can be retrieved later when handling the
// cancellation so that the proper error can be thrown.
if (blockedByParentalControls) {
download._blockedByParentalControls = true;
}
download.saver.onTransferStarted(
aRequest,
this._cancelable instanceof Ci.nsIHelperAppLauncher);
// To handle asynchronous cancellation properly, we should hook up the
// handler only after we have been notified that the main request
// started. We will wait until the main request stopped before
// notifying that the download has been canceled. Since the request has
// not completed yet, deferCanceled is guaranteed to be set.
return download.saver.deferCanceled.promise.then(() => {
// Only cancel if the object executing the download is still running.
if (this._cancelable && !this._componentFailed) {
this._cancelable.cancel(Cr.NS_ERROR_ABORT);
if (this._cancelable instanceof Ci.nsIWebBrowserPersist) {
// This component will not send the STATE_STOP notification.
download.saver.onTransferFinished(aRequest, Cr.NS_ERROR_ABORT);
this._cancelable = null;
}
}
});
}).then(null, Cu.reportError);
} else if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) &&
(aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
// The last file has been received, or the download failed. Wait for the
// associated Download object to be available before notifying.
this._deferDownload.promise.then(download => {
// At this point, the hash has been set and we need to copy it to the
// DownloadSaver.
if (Components.isSuccessCode(aStatus)) {
download.saver.setSha256Hash(this._sha256Hash);
download.saver.setSignatureInfo(this._signatureInfo);
download.saver.setRedirects(this._redirects);
}
download.saver.onTransferFinished(aRequest, aStatus);
}).then(null, Cu.reportError);
// Release the reference to the component executing the download.
this._cancelable = null;
}
},
onProgressChange: function DLT_onProgressChange(aWebProgress, aRequest,
aCurSelfProgress,
aMaxSelfProgress,
aCurTotalProgress,
aMaxTotalProgress)
{
this.onProgressChange64(aWebProgress, aRequest, aCurSelfProgress,
aMaxSelfProgress, aCurTotalProgress,
aMaxTotalProgress);
},
onLocationChange: function () { },
onStatusChange: function DLT_onStatusChange(aWebProgress, aRequest, aStatus,
aMessage)
{
// The status change may optionally be received in addition to the state
// change, but if no network request actually started, it is possible that
// we only receive a status change with an error status code.
if (!Components.isSuccessCode(aStatus)) {
this._componentFailed = true;
// Wait for the associated Download object to be available.
this._deferDownload.promise.then(function DLT_OSC_onDownload(aDownload) {
aDownload.saver.onTransferFinished(aRequest, aStatus);
}).then(null, Cu.reportError);
}
},
onSecurityChange: function () { },
// nsIWebProgressListener2
onProgressChange64: function DLT_onProgressChange64(aWebProgress, aRequest,
aCurSelfProgress,
aMaxSelfProgress,
aCurTotalProgress,
aMaxTotalProgress)
{
// Wait for the associated Download object to be available.
this._deferDownload.promise.then(function DLT_OPC64_onDownload(aDownload) {
aDownload.saver.onProgressBytes(aCurTotalProgress, aMaxTotalProgress);
}).then(null, Cu.reportError);
},
onRefreshAttempted: function DLT_onRefreshAttempted(aWebProgress, aRefreshURI,
aMillis, aSameURI)
{
// Indicate that refreshes and redirects are allowed by default. However,
// note that download components don't usually call this method at all.
return true;
},
// nsITransfer
init: function DLT_init(aSource, aTarget, aDisplayName, aMIMEInfo, aStartTime,
aTempFile, aCancelable, aIsPrivate)
{
this._cancelable = aCancelable;
let launchWhenSucceeded = false, contentType = null, launcherPath = null;
if (aMIMEInfo instanceof Ci.nsIMIMEInfo) {
launchWhenSucceeded =
aMIMEInfo.preferredAction != Ci.nsIMIMEInfo.saveToDisk;
contentType = aMIMEInfo.type;
let appHandler = aMIMEInfo.preferredApplicationHandler;
if (aMIMEInfo.preferredAction == Ci.nsIMIMEInfo.useHelperApp &&
appHandler instanceof Ci.nsILocalHandlerApp) {
launcherPath = appHandler.executable.path;
}
}
// Create a new Download object associated to a DownloadLegacySaver, and
// wait for it to be available. This operation may cause the entire
// download system to initialize before the object is created.
Downloads.createDownload({
source: { url: aSource.spec, isPrivate: aIsPrivate },
target: { path: aTarget.QueryInterface(Ci.nsIFileURL).file.path,
partFilePath: aTempFile && aTempFile.path },
saver: "legacy",
launchWhenSucceeded: launchWhenSucceeded,
contentType: contentType,
launcherPath: launcherPath
}).then(function DLT_I_onDownload(aDownload) {
// Legacy components keep partial data when they use a ".part" file.
if (aTempFile) {
aDownload.tryToKeepPartialData = true;
}
// Start the download before allowing it to be controlled. Ignore errors.
aDownload.start().catch(() => {});
// Start processing all the other events received through nsITransfer.
this._deferDownload.resolve(aDownload);
// Add the download to the list, allowing it to be seen and canceled.
return Downloads.getList(Downloads.ALL).then(list => list.add(aDownload));
}.bind(this)).then(null, Cu.reportError);
},
setSha256Hash: function (hash)
{
this._sha256Hash = hash;
},
setSignatureInfo: function (signatureInfo)
{
this._signatureInfo = signatureInfo;
},
setRedirects: function (redirects)
{
this._redirects = redirects;
},
// Private methods and properties
/**
* This deferred object contains a promise that is resolved with the Download
* object associated with this nsITransfer instance, when it is available.
*/
_deferDownload: null,
/**
* Reference to the component that is executing the download. This component
* allows cancellation through its nsICancelable interface.
*/
_cancelable: null,
/**
* Indicates that the component that executes the download has notified a
* failure condition. In this case, we should never use the component methods
* that cancel the download.
*/
_componentFailed: false,
/**
* Save the SHA-256 hash in raw bytes of the downloaded file.
*/
_sha256Hash: null,
/**
* Save the signature info in a serialized protobuf of the downloaded file.
*/
_signatureInfo: null,
};
// Module
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DownloadLegacyTransfer]);

View file

@ -0,0 +1,559 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* This file includes the following constructors and global objects:
*
* DownloadList
* Represents a collection of Download objects that can be viewed and managed by
* the user interface, and persisted across sessions.
*
* DownloadCombinedList
* Provides a unified, unordered list combining public and private downloads.
*
* DownloadSummary
* Provides an aggregated view on the contents of a DownloadList.
*/
"use strict";
this.EXPORTED_SYMBOLS = [
"DownloadList",
"DownloadCombinedList",
"DownloadSummary",
];
// Globals
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, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
// DownloadList
/**
* Represents a collection of Download objects that can be viewed and managed by
* the user interface, and persisted across sessions.
*/
this.DownloadList = function ()
{
this._downloads = [];
this._views = new Set();
}
this.DownloadList.prototype = {
/**
* Array of Download objects currently in the list.
*/
_downloads: null,
/**
* Retrieves a snapshot of the downloads that are currently in the list. The
* returned array does not change when downloads are added or removed, though
* the Download objects it contains are still updated in real time.
*
* @return {Promise}
* @resolves An array of Download objects.
* @rejects JavaScript exception.
*/
getAll: function DL_getAll() {
return Promise.resolve(Array.slice(this._downloads, 0));
},
/**
* Adds a new download to the end of the items list.
*
* @note When a download is added to the list, its "onchange" event is
* registered by the list, thus it cannot be used to monitor the
* download. To receive change notifications for downloads that are
* added to the list, use the addView method to register for
* onDownloadChanged notifications.
*
* @param aDownload
* The Download object to add.
*
* @return {Promise}
* @resolves When the download has been added.
* @rejects JavaScript exception.
*/
add: function DL_add(aDownload) {
this._downloads.push(aDownload);
aDownload.onchange = this._change.bind(this, aDownload);
this._notifyAllViews("onDownloadAdded", aDownload);
return Promise.resolve();
},
/**
* Removes a download from the list. If the download was already removed,
* this method has no effect.
*
* This method does not change the state of the download, to allow adding it
* to another list, or control it directly. If you want to dispose of the
* download object, you should cancel it afterwards, and remove any partially
* downloaded data if needed.
*
* @param aDownload
* The Download object to remove.
*
* @return {Promise}
* @resolves When the download has been removed.
* @rejects JavaScript exception.
*/
remove: function DL_remove(aDownload) {
let index = this._downloads.indexOf(aDownload);
if (index != -1) {
this._downloads.splice(index, 1);
aDownload.onchange = null;
this._notifyAllViews("onDownloadRemoved", aDownload);
}
return Promise.resolve();
},
/**
* This function is called when "onchange" events of downloads occur.
*
* @param aDownload
* The Download object that changed.
*/
_change: function DL_change(aDownload) {
this._notifyAllViews("onDownloadChanged", aDownload);
},
/**
* Set of currently registered views.
*/
_views: null,
/**
* Adds a view that will be notified of changes to downloads. The newly added
* view will receive onDownloadAdded notifications for all the downloads that
* are already in the list.
*
* @param aView
* The view object to add. The following methods may be defined:
* {
* onDownloadAdded: function (aDownload) {
* // Called after aDownload is added to the end of the list.
* },
* onDownloadChanged: function (aDownload) {
* // Called after the properties of aDownload change.
* },
* onDownloadRemoved: function (aDownload) {
* // Called after aDownload is removed from the list.
* },
* }
*
* @return {Promise}
* @resolves When the view has been registered and all the onDownloadAdded
* notifications for the existing downloads have been sent.
* @rejects JavaScript exception.
*/
addView: function DL_addView(aView)
{
this._views.add(aView);
if ("onDownloadAdded" in aView) {
for (let download of this._downloads) {
try {
aView.onDownloadAdded(download);
} catch (ex) {
Cu.reportError(ex);
}
}
}
return Promise.resolve();
},
/**
* Removes a view that was previously added using addView.
*
* @param aView
* The view object to remove.
*
* @return {Promise}
* @resolves When the view has been removed. At this point, the removed view
* will not receive any more notifications.
* @rejects JavaScript exception.
*/
removeView: function DL_removeView(aView)
{
this._views.delete(aView);
return Promise.resolve();
},
/**
* Notifies all the views of a download addition, change, or removal.
*
* @param aMethodName
* String containing the name of the method to call on the view.
* @param aDownload
* The Download object that changed.
*/
_notifyAllViews: function (aMethodName, aDownload) {
for (let view of this._views) {
try {
if (aMethodName in view) {
view[aMethodName](aDownload);
}
} catch (ex) {
Cu.reportError(ex);
}
}
},
/**
* Removes downloads from the list that have finished, have failed, or have
* been canceled without keeping partial data. A filter function may be
* specified to remove only a subset of those downloads.
*
* This method finalizes each removed download, ensuring that any partially
* downloaded data associated with it is also removed.
*
* @param aFilterFn
* The filter function is called with each download as its only
* argument, and should return true to remove the download and false
* to keep it. This parameter may be null or omitted to have no
* additional filter.
*/
removeFinished: function DL_removeFinished(aFilterFn) {
Task.spawn(function* () {
let list = yield this.getAll();
for (let download of list) {
// Remove downloads that have been canceled, even if the cancellation
// operation hasn't completed yet so we don't check "stopped" here.
// Failed downloads with partial data are also removed.
if (download.stopped && (!download.hasPartialData || download.error) &&
(!aFilterFn || aFilterFn(download))) {
// Remove the download first, so that the views don't get the change
// notifications that may occur during finalization.
yield this.remove(download);
// Ensure that the download is stopped and no partial data is kept.
// This works even if the download state has changed meanwhile. We
// don't need to wait for the procedure to be complete before
// processing the other downloads in the list.
download.finalize(true).then(null, Cu.reportError);
}
}
}.bind(this)).then(null, Cu.reportError);
},
};
// DownloadCombinedList
/**
* Provides a unified, unordered list combining public and private downloads.
*
* Download objects added to this list are also added to one of the two
* underlying lists, based on their "source.isPrivate" property. Views on this
* list will receive notifications for both public and private downloads.
*
* @param aPublicList
* Underlying DownloadList containing public downloads.
* @param aPrivateList
* Underlying DownloadList containing private downloads.
*/
this.DownloadCombinedList = function (aPublicList, aPrivateList)
{
DownloadList.call(this);
this._publicList = aPublicList;
this._privateList = aPrivateList;
aPublicList.addView(this).then(null, Cu.reportError);
aPrivateList.addView(this).then(null, Cu.reportError);
}
this.DownloadCombinedList.prototype = {
__proto__: DownloadList.prototype,
/**
* Underlying DownloadList containing public downloads.
*/
_publicList: null,
/**
* Underlying DownloadList containing private downloads.
*/
_privateList: null,
/**
* Adds a new download to the end of the items list.
*
* @note When a download is added to the list, its "onchange" event is
* registered by the list, thus it cannot be used to monitor the
* download. To receive change notifications for downloads that are
* added to the list, use the addView method to register for
* onDownloadChanged notifications.
*
* @param aDownload
* The Download object to add.
*
* @return {Promise}
* @resolves When the download has been added.
* @rejects JavaScript exception.
*/
add: function (aDownload)
{
if (aDownload.source.isPrivate) {
return this._privateList.add(aDownload);
}
return this._publicList.add(aDownload);
},
/**
* Removes a download from the list. If the download was already removed,
* this method has no effect.
*
* This method does not change the state of the download, to allow adding it
* to another list, or control it directly. If you want to dispose of the
* download object, you should cancel it afterwards, and remove any partially
* downloaded data if needed.
*
* @param aDownload
* The Download object to remove.
*
* @return {Promise}
* @resolves When the download has been removed.
* @rejects JavaScript exception.
*/
remove: function (aDownload)
{
if (aDownload.source.isPrivate) {
return this._privateList.remove(aDownload);
}
return this._publicList.remove(aDownload);
},
// DownloadList view
onDownloadAdded: function (aDownload)
{
this._downloads.push(aDownload);
this._notifyAllViews("onDownloadAdded", aDownload);
},
onDownloadChanged: function (aDownload)
{
this._notifyAllViews("onDownloadChanged", aDownload);
},
onDownloadRemoved: function (aDownload)
{
let index = this._downloads.indexOf(aDownload);
if (index != -1) {
this._downloads.splice(index, 1);
}
this._notifyAllViews("onDownloadRemoved", aDownload);
},
};
// DownloadSummary
/**
* Provides an aggregated view on the contents of a DownloadList.
*/
this.DownloadSummary = function ()
{
this._downloads = [];
this._views = new Set();
}
this.DownloadSummary.prototype = {
/**
* Array of Download objects that are currently part of the summary.
*/
_downloads: null,
/**
* Underlying DownloadList whose contents should be summarized.
*/
_list: null,
/**
* This method may be called once to bind this object to a DownloadList.
*
* Views on the summarized data can be registered before this object is bound
* to an actual list. This allows the summary to be used without requiring
* the initialization of the DownloadList first.
*
* @param aList
* Underlying DownloadList whose contents should be summarized.
*
* @return {Promise}
* @resolves When the view on the underlying list has been registered.
* @rejects JavaScript exception.
*/
bindToList: function (aList)
{
if (this._list) {
throw new Error("bindToList may be called only once.");
}
return aList.addView(this).then(() => {
// Set the list reference only after addView has returned, so that we don't
// send a notification to our views for each download that is added.
this._list = aList;
this._onListChanged();
});
},
/**
* Set of currently registered views.
*/
_views: null,
/**
* Adds a view that will be notified of changes to the summary. The newly
* added view will receive an initial onSummaryChanged notification.
*
* @param aView
* The view object to add. The following methods may be defined:
* {
* onSummaryChanged: function () {
* // Called after any property of the summary has changed.
* },
* }
*
* @return {Promise}
* @resolves When the view has been registered and the onSummaryChanged
* notification has been sent.
* @rejects JavaScript exception.
*/
addView: function (aView)
{
this._views.add(aView);
if ("onSummaryChanged" in aView) {
try {
aView.onSummaryChanged();
} catch (ex) {
Cu.reportError(ex);
}
}
return Promise.resolve();
},
/**
* Removes a view that was previously added using addView.
*
* @param aView
* The view object to remove.
*
* @return {Promise}
* @resolves When the view has been removed. At this point, the removed view
* will not receive any more notifications.
* @rejects JavaScript exception.
*/
removeView: function (aView)
{
this._views.delete(aView);
return Promise.resolve();
},
/**
* Indicates whether all the downloads are currently stopped.
*/
allHaveStopped: true,
/**
* Indicates the total number of bytes to be transferred before completing all
* the downloads that are currently in progress.
*
* For downloads that do not have a known final size, the number of bytes
* currently transferred is reported as part of this property.
*
* This is zero if no downloads are currently in progress.
*/
progressTotalBytes: 0,
/**
* Number of bytes currently transferred as part of all the downloads that are
* currently in progress.
*
* This is zero if no downloads are currently in progress.
*/
progressCurrentBytes: 0,
/**
* This function is called when any change in the list of downloads occurs,
* and will recalculate the summary and notify the views in case the
* aggregated properties are different.
*/
_onListChanged: function () {
let allHaveStopped = true;
let progressTotalBytes = 0;
let progressCurrentBytes = 0;
// Recalculate the aggregated state. See the description of the individual
// properties for an explanation of the summarization logic.
for (let download of this._downloads) {
if (!download.stopped) {
allHaveStopped = false;
progressTotalBytes += download.hasProgress ? download.totalBytes
: download.currentBytes;
progressCurrentBytes += download.currentBytes;
}
}
// Exit now if the properties did not change.
if (this.allHaveStopped == allHaveStopped &&
this.progressTotalBytes == progressTotalBytes &&
this.progressCurrentBytes == progressCurrentBytes) {
return;
}
this.allHaveStopped = allHaveStopped;
this.progressTotalBytes = progressTotalBytes;
this.progressCurrentBytes = progressCurrentBytes;
// Notify all the views that our properties changed.
for (let view of this._views) {
try {
if ("onSummaryChanged" in view) {
view.onSummaryChanged();
}
} catch (ex) {
Cu.reportError(ex);
}
}
},
// DownloadList view
onDownloadAdded: function (aDownload)
{
this._downloads.push(aDownload);
if (this._list) {
this._onListChanged();
}
},
onDownloadChanged: function (aDownload)
{
this._onListChanged();
},
onDownloadRemoved: function (aDownload)
{
let index = this._downloads.indexOf(aDownload);
if (index != -1) {
this._downloads.splice(index, 1);
}
this._onListChanged();
},
};

View file

@ -0,0 +1,275 @@
/* 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 "DownloadPlatform.h"
#include "nsAutoPtr.h"
#include "nsNetUtil.h"
#include "nsString.h"
#include "nsINestedURI.h"
#include "nsIProtocolHandler.h"
#include "nsIURI.h"
#include "nsIFile.h"
#include "nsIObserverService.h"
#include "nsISupportsPrimitives.h"
#include "nsDirectoryServiceDefs.h"
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"
#define PREF_BDM_ADDTORECENTDOCS "browser.download.manager.addToRecentDocs"
#ifdef XP_WIN
#include <shlobj.h>
#include <urlmon.h>
#include "nsILocalFileWin.h"
#endif
#ifdef XP_MACOSX
#include <CoreFoundation/CoreFoundation.h>
#include "../../../../xpcom/io/CocoaFileUtils.h"
#endif
#ifdef MOZ_WIDGET_ANDROID
#include "FennecJNIWrappers.h"
#endif
#ifdef MOZ_WIDGET_GTK
#include <gtk/gtk.h>
#endif
using namespace mozilla;
DownloadPlatform *DownloadPlatform::gDownloadPlatformService = nullptr;
NS_IMPL_ISUPPORTS(DownloadPlatform, mozIDownloadPlatform);
DownloadPlatform* DownloadPlatform::GetDownloadPlatform()
{
if (!gDownloadPlatformService) {
gDownloadPlatformService = new DownloadPlatform();
}
NS_ADDREF(gDownloadPlatformService);
#if defined(MOZ_WIDGET_GTK)
g_type_init();
#endif
return gDownloadPlatformService;
}
#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
#ifdef XP_MACOSX
// Caller is responsible for freeing any result (CF Create Rule)
CFURLRef CreateCFURLFromNSIURI(nsIURI *aURI) {
nsAutoCString spec;
if (aURI) {
aURI->GetSpec(spec);
}
CFStringRef urlStr = ::CFStringCreateWithCString(kCFAllocatorDefault,
spec.get(),
kCFStringEncodingUTF8);
if (!urlStr) {
return NULL;
}
CFURLRef url = ::CFURLCreateWithString(kCFAllocatorDefault,
urlStr,
NULL);
::CFRelease(urlStr);
return url;
}
#endif
nsresult DownloadPlatform::DownloadDone(nsIURI* aSource, nsIURI* aReferrer, nsIFile* aTarget,
const nsACString& aContentType, bool aIsPrivate)
{
#if defined(XP_WIN) || defined(XP_MACOSX) || defined(MOZ_WIDGET_ANDROID) \
|| defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_GONK)
nsAutoString path;
if (aTarget && NS_SUCCEEDED(aTarget->GetPath(path))) {
#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_ANDROID)
// On Windows and Gtk, add the download to the system's "recent documents"
// list, with a pref to disable.
{
bool addToRecentDocs = Preferences::GetBool(PREF_BDM_ADDTORECENTDOCS);
#ifdef MOZ_WIDGET_ANDROID
if (jni::IsFennec() && addToRecentDocs) {
java::DownloadsIntegration::ScanMedia(path, aContentType);
}
#else
if (addToRecentDocs && !aIsPrivate) {
#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
}
#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;
nsresult rv = aSource->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
}
#endif
#ifdef XP_MACOSX
// On OS X, make the downloads stack bounce.
CFStringRef observedObject = ::CFStringCreateWithCString(kCFAllocatorDefault,
NS_ConvertUTF16toUTF8(path).get(),
kCFStringEncodingUTF8);
CFNotificationCenterRef center = ::CFNotificationCenterGetDistributedCenter();
::CFNotificationCenterPostNotification(center, CFSTR("com.apple.DownloadFileFinished"),
observedObject, nullptr, TRUE);
::CFRelease(observedObject);
// Add OS X origin and referrer file metadata
CFStringRef pathCFStr = NULL;
if (!path.IsEmpty()) {
pathCFStr = ::CFStringCreateWithCharacters(kCFAllocatorDefault,
(const UniChar*)path.get(),
path.Length());
}
if (pathCFStr) {
bool isFromWeb = IsURLPossiblyFromWeb(aSource);
CFURLRef sourceCFURL = CreateCFURLFromNSIURI(aSource);
CFURLRef referrerCFURL = CreateCFURLFromNSIURI(aReferrer);
CocoaFileUtils::AddOriginMetadataToFile(pathCFStr,
sourceCFURL,
referrerCFURL);
CocoaFileUtils::AddQuarantineMetadataToFile(pathCFStr,
sourceCFURL,
referrerCFURL,
isFromWeb);
::CFRelease(pathCFStr);
if (sourceCFURL) {
::CFRelease(sourceCFURL);
}
if (referrerCFURL) {
::CFRelease(referrerCFURL);
}
}
#endif
}
#endif
return NS_OK;
}
nsresult DownloadPlatform::MapUrlToZone(const nsAString& aURL,
uint32_t* aZone)
{
#ifdef XP_WIN
RefPtr<IInternetSecurityManager> inetSecMgr;
if (FAILED(CoCreateInstance(CLSID_InternetSecurityManager, NULL,
CLSCTX_ALL, IID_IInternetSecurityManager,
getter_AddRefs(inetSecMgr)))) {
return NS_ERROR_UNEXPECTED;
}
DWORD zone;
if (inetSecMgr->MapUrlToZone(PromiseFlatString(aURL).get(),
&zone, 0) != S_OK) {
return NS_ERROR_UNEXPECTED;
} else {
*aZone = zone;
}
return NS_OK;
#else
return NS_ERROR_NOT_IMPLEMENTED;
#endif
}
// Check if a URI is likely to be web-based, by checking its URI flags.
// If in doubt (e.g. if anything fails during the check) claims things
// are from the web.
bool DownloadPlatform::IsURLPossiblyFromWeb(nsIURI* aURI)
{
nsCOMPtr<nsIIOService> ios = do_GetIOService();
nsCOMPtr<nsIURI> uri = aURI;
if (!ios) {
return true;
}
while (uri) {
// We're not using nsIIOService::ProtocolHasFlags because it doesn't
// take per-URI flags into account. We're also not using
// NS_URIChainHasFlags because we're checking for *any* of 3 flags
// to be present on *all* of the nested URIs, which it can't do.
nsAutoCString scheme;
nsresult rv = uri->GetScheme(scheme);
if (NS_FAILED(rv)) {
return true;
}
nsCOMPtr<nsIProtocolHandler> ph;
rv = ios->GetProtocolHandler(scheme.get(), getter_AddRefs(ph));
if (NS_FAILED(rv)) {
return true;
}
uint32_t flags;
rv = ph->DoGetProtocolFlags(uri, &flags);
if (NS_FAILED(rv)) {
return true;
}
// If not dangerous to load, not a UI resource and not a local file,
// assume this is from the web:
if (!(flags & nsIProtocolHandler::URI_DANGEROUS_TO_LOAD) &&
!(flags & nsIProtocolHandler::URI_IS_UI_RESOURCE) &&
!(flags & nsIProtocolHandler::URI_IS_LOCAL_FILE)) {
return true;
}
// Otherwise, check if the URI is nested, and if so go through
// the loop again:
nsCOMPtr<nsINestedURI> nestedURI = do_QueryInterface(uri);
uri = nullptr;
if (nestedURI) {
rv = nestedURI->GetInnerURI(getter_AddRefs(uri));
if (NS_FAILED(rv)) {
return true;
}
}
}
return false;
}

View file

@ -0,0 +1,34 @@
/* 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 __DownloadPlatform_h__
#define __DownloadPlatform_h__
#include "mozIDownloadPlatform.h"
#include "nsCOMPtr.h"
class nsIURI;
class DownloadPlatform : public mozIDownloadPlatform
{
protected:
virtual ~DownloadPlatform() { }
public:
NS_DECL_ISUPPORTS
NS_DECL_MOZIDOWNLOADPLATFORM
DownloadPlatform() { }
static DownloadPlatform *gDownloadPlatformService;
static DownloadPlatform* GetDownloadPlatform();
private:
static bool IsURLPossiblyFromWeb(nsIURI* aURI);
};
#endif

View file

@ -0,0 +1,203 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Handles serialization of Download objects and persistence into a file, so
* that the state of downloads can be restored across sessions.
*
* The file is stored in JSON format, without indentation. With indentation
* applied, the file would look like this:
*
* {
* "list": [
* {
* "source": "http://www.example.com/download.txt",
* "target": "/home/user/Downloads/download.txt"
* },
* {
* "source": {
* "url": "http://www.example.com/download.txt",
* "referrer": "http://www.example.com/referrer.html"
* },
* "target": "/home/user/Downloads/download-2.txt"
* }
* ]
* }
*/
"use strict";
this.EXPORTED_SYMBOLS = [
"DownloadStore",
];
// Globals
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, "Downloads",
"resource://gre/modules/Downloads.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm")
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyGetter(this, "gTextDecoder", function () {
return new TextDecoder();
});
XPCOMUtils.defineLazyGetter(this, "gTextEncoder", function () {
return new TextEncoder();
});
// DownloadStore
/**
* Handles serialization of Download objects and persistence into a file, so
* that the state of downloads can be restored across sessions.
*
* @param aList
* DownloadList object to be populated or serialized.
* @param aPath
* String containing the file path where data should be saved.
*/
this.DownloadStore = function (aList, aPath)
{
this.list = aList;
this.path = aPath;
}
this.DownloadStore.prototype = {
/**
* DownloadList object to be populated or serialized.
*/
list: null,
/**
* String containing the file path where data should be saved.
*/
path: "",
/**
* This function is called with a Download object as its first argument, and
* should return true if the item should be saved.
*/
onsaveitem: () => true,
/**
* Loads persistent downloads from the file to the list.
*
* @return {Promise}
* @resolves When the operation finished successfully.
* @rejects JavaScript exception.
*/
load: function DS_load()
{
return Task.spawn(function* task_DS_load() {
let bytes;
try {
bytes = yield OS.File.read(this.path);
} catch (ex) {
if (!(ex instanceof OS.File.Error) || !ex.becauseNoSuchFile) {
throw ex;
}
// If the file does not exist, there are no downloads to load.
return;
}
let storeData = JSON.parse(gTextDecoder.decode(bytes));
// Create live downloads based on the static snapshot.
for (let downloadData of storeData.list) {
try {
let download = yield Downloads.createDownload(downloadData);
try {
if (!download.succeeded && !download.canceled && !download.error) {
// Try to restart the download if it was in progress during the
// previous session. Ignore errors.
download.start().catch(() => {});
} else {
// If the download was not in progress, try to update the current
// progress from disk. This is relevant in case we retained
// partially downloaded data.
yield download.refresh();
}
} finally {
// Add the download to the list if we succeeded in creating it,
// after we have updated its initial state.
yield this.list.add(download);
}
} catch (ex) {
// If an item is unrecognized, don't prevent others from being loaded.
Cu.reportError(ex);
}
}
}.bind(this));
},
/**
* Saves persistent downloads from the list to the file.
*
* If an error occurs, the previous file is not deleted.
*
* @return {Promise}
* @resolves When the operation finished successfully.
* @rejects JavaScript exception.
*/
save: function DS_save()
{
return Task.spawn(function* task_DS_save() {
let downloads = yield this.list.getAll();
// Take a static snapshot of the current state of all the downloads.
let storeData = { list: [] };
let atLeastOneDownload = false;
for (let download of downloads) {
try {
if (!this.onsaveitem(download)) {
continue;
}
let serializable = download.toSerializable();
if (!serializable) {
// This item cannot be persisted across sessions.
continue;
}
storeData.list.push(serializable);
atLeastOneDownload = true;
} catch (ex) {
// If an item cannot be converted to a serializable form, don't
// prevent others from being saved.
Cu.reportError(ex);
}
}
if (atLeastOneDownload) {
// Create or overwrite the file if there are downloads to save.
let bytes = gTextEncoder.encode(JSON.stringify(storeData));
yield OS.File.writeAtomic(this.path, bytes,
{ tmpPath: this.path + ".tmp" });
} else {
// Remove the file if there are no downloads to save at all.
try {
yield OS.File.remove(this.path);
} catch (ex) {
if (!(ex instanceof OS.File.Error) ||
!(ex.becauseNoSuchFile || ex.becauseAccessDenied)) {
throw ex;
}
// On Windows, we may get an access denied error instead of a no such
// file error if the file existed before, and was recently deleted.
}
}
}.bind(this));
},
};

View file

@ -0,0 +1,243 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Provides functions to handle status and messages in the user interface.
*/
"use strict";
this.EXPORTED_SYMBOLS = [
"DownloadUIHelper",
];
// Globals
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
const kStringBundleUrl =
"chrome://mozapps/locale/downloads/downloads.properties";
const kStringsRequiringFormatting = {
fileExecutableSecurityWarning: true,
cancelDownloadsOKTextMultiple: true,
quitCancelDownloadsAlertMsgMultiple: true,
quitCancelDownloadsAlertMsgMacMultiple: true,
offlineCancelDownloadsAlertMsgMultiple: true,
leavePrivateBrowsingWindowsCancelDownloadsAlertMsgMultiple2: true
};
// DownloadUIHelper
/**
* Provides functions to handle status and messages in the user interface.
*/
this.DownloadUIHelper = {
/**
* Returns an object that can be used to display prompts related to downloads.
*
* The prompts may be either anchored to a specified window, or anchored to
* the most recently active window, for example if the prompt is displayed in
* response to global notifications that are not associated with any window.
*
* @param aParent
* If specified, should reference the nsIDOMWindow to which the prompts
* should be attached. If omitted, the prompts will be attached to the
* most recently active window.
*
* @return A DownloadPrompter object.
*/
getPrompter: function (aParent)
{
return new DownloadPrompter(aParent || null);
},
};
/**
* Returns an object whose keys are the string names from the downloads string
* bundle, and whose values are either the translated strings or functions
* returning formatted strings.
*/
XPCOMUtils.defineLazyGetter(DownloadUIHelper, "strings", function () {
let strings = {};
let sb = Services.strings.createBundle(kStringBundleUrl);
let enumerator = sb.getSimpleEnumeration();
while (enumerator.hasMoreElements()) {
let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
let stringName = string.key;
if (stringName in kStringsRequiringFormatting) {
strings[stringName] = function () {
// Convert "arguments" to a real array before calling into XPCOM.
return sb.formatStringFromName(stringName,
Array.slice(arguments, 0),
arguments.length);
};
} else {
strings[stringName] = string.value;
}
}
return strings;
});
// DownloadPrompter
/**
* Allows displaying prompts related to downloads.
*
* @param aParent
* The nsIDOMWindow to which prompts should be attached, or null to
* attach prompts to the most recently active window.
*/
this.DownloadPrompter = function (aParent)
{
if (AppConstants.MOZ_B2G) {
// On B2G there is no prompter implementation.
this._prompter = null;
} else {
this._prompter = Services.ww.getNewPrompter(aParent);
}
}
this.DownloadPrompter.prototype = {
/**
* Constants with the different type of prompts.
*/
ON_QUIT: "prompt-on-quit",
ON_OFFLINE: "prompt-on-offline",
ON_LEAVE_PRIVATE_BROWSING: "prompt-on-leave-private-browsing",
/**
* nsIPrompt instance for displaying messages.
*/
_prompter: null,
/**
* Displays a warning message box that informs that the specified file is
* executable, and asks whether the user wants to launch it. The user is
* given the option of disabling future instances of this warning.
*
* @param aPath
* String containing the full path to the file to be opened.
*
* @return {Promise}
* @resolves Boolean indicating whether the launch operation can continue.
* @rejects JavaScript exception.
*/
confirmLaunchExecutable: function (aPath)
{
const kPrefAlertOnEXEOpen = "browser.download.manager.alertOnEXEOpen";
try {
// Always launch in case we have no prompter implementation.
if (!this._prompter) {
return Promise.resolve(true);
}
try {
if (!Services.prefs.getBoolPref(kPrefAlertOnEXEOpen)) {
return Promise.resolve(true);
}
} catch (ex) {
// If the preference does not exist, continue with the prompt.
}
let leafName = OS.Path.basename(aPath);
let s = DownloadUIHelper.strings;
let checkState = { value: false };
let shouldLaunch = this._prompter.confirmCheck(
s.fileExecutableSecurityWarningTitle,
s.fileExecutableSecurityWarning(leafName, leafName),
s.fileExecutableSecurityWarningDontAsk,
checkState);
if (shouldLaunch) {
Services.prefs.setBoolPref(kPrefAlertOnEXEOpen, !checkState.value);
}
return Promise.resolve(shouldLaunch);
} catch (ex) {
return Promise.reject(ex);
}
},
/**
* Displays a warning message box that informs that there are active
* downloads, and asks whether the user wants to cancel them or not.
*
* @param aDownloadsCount
* The current downloads count.
* @param aPromptType
* The type of prompt notification depending on the observer.
*
* @return False to cancel the downloads and continue, true to abort the
* operation.
*/
confirmCancelDownloads: function DP_confirmCancelDownload(aDownloadsCount,
aPromptType)
{
// Always continue in case we have no prompter implementation, or if there
// are no active downloads.
if (!this._prompter || aDownloadsCount <= 0) {
return false;
}
let s = DownloadUIHelper.strings;
let buttonFlags = (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
(Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1);
let okButton = aDownloadsCount > 1 ? s.cancelDownloadsOKTextMultiple(aDownloadsCount)
: s.cancelDownloadsOKText;
let title, message, cancelButton;
switch (aPromptType) {
case this.ON_QUIT:
title = s.quitCancelDownloadsAlertTitle;
if (AppConstants.platform != "macosx") {
message = aDownloadsCount > 1
? s.quitCancelDownloadsAlertMsgMultiple(aDownloadsCount)
: s.quitCancelDownloadsAlertMsg;
cancelButton = s.dontQuitButtonWin;
} else {
message = aDownloadsCount > 1
? s.quitCancelDownloadsAlertMsgMacMultiple(aDownloadsCount)
: s.quitCancelDownloadsAlertMsgMac;
cancelButton = s.dontQuitButtonMac;
}
break;
case this.ON_OFFLINE:
title = s.offlineCancelDownloadsAlertTitle;
message = aDownloadsCount > 1
? s.offlineCancelDownloadsAlertMsgMultiple(aDownloadsCount)
: s.offlineCancelDownloadsAlertMsg;
cancelButton = s.dontGoOfflineButton;
break;
case this.ON_LEAVE_PRIVATE_BROWSING:
title = s.leavePrivateBrowsingCancelDownloadsAlertTitle;
message = aDownloadsCount > 1
? s.leavePrivateBrowsingWindowsCancelDownloadsAlertMsgMultiple2(aDownloadsCount)
: s.leavePrivateBrowsingWindowsCancelDownloadsAlertMsg2;
cancelButton = s.dontLeavePrivateBrowsingButton2;
break;
}
let rv = this._prompter.confirmEx(title, message, buttonFlags, okButton,
cancelButton, null, null, {});
return (rv == 1);
}
};

View file

@ -0,0 +1,305 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Main entry point to get references to all the back-end objects.
*/
"use strict";
this.EXPORTED_SYMBOLS = [
"Downloads",
];
// Globals
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource://gre/modules/Integration.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/DownloadCore.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadCombinedList",
"resource://gre/modules/DownloadList.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadList",
"resource://gre/modules/DownloadList.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadSummary",
"resource://gre/modules/DownloadList.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper",
"resource://gre/modules/DownloadUIHelper.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
Integration.downloads.defineModuleGetter(this, "DownloadIntegration",
"resource://gre/modules/DownloadIntegration.jsm");
// Downloads
/**
* This object is exposed directly to the consumers of this JavaScript module,
* and provides the only entry point to get references to back-end objects.
*/
this.Downloads = {
/**
* Work on downloads that were not started from a private browsing window.
*/
get PUBLIC() {
return "{Downloads.PUBLIC}";
},
/**
* Work on downloads that were started from a private browsing window.
*/
get PRIVATE() {
return "{Downloads.PRIVATE}";
},
/**
* Work on both Downloads.PRIVATE and Downloads.PUBLIC downloads.
*/
get ALL() {
return "{Downloads.ALL}";
},
/**
* Creates a new Download object.
*
* @param aProperties
* Provides the initial properties for the newly created download.
* This matches the serializable representation of a Download object.
* Some of the most common properties in this object include:
* {
* source: String containing the URI for the download source.
* Alternatively, may be an nsIURI, a DownloadSource object,
* or an object with the following properties:
* {
* url: String containing the URI for the download source.
* isPrivate: Indicates whether the download originated from a
* private window. If omitted, the download is public.
* referrer: String containing the referrer URI of the download
* source. Can be omitted or null if no referrer should
* be sent or the download source is not HTTP.
* },
* target: String containing the path of the target file.
* Alternatively, may be an nsIFile, a DownloadTarget object,
* or an object with the following properties:
* {
* path: String containing the path of the target file.
* },
* saver: String representing the class of the download operation.
* If omitted, defaults to "copy". Alternatively, may be the
* serializable representation of a DownloadSaver object.
* }
*
* @return {Promise}
* @resolves The newly created Download object.
* @rejects JavaScript exception.
*/
createDownload: function D_createDownload(aProperties)
{
try {
return Promise.resolve(Download.fromSerializable(aProperties));
} catch (ex) {
return Promise.reject(ex);
}
},
/**
* Downloads data from a remote network location to a local file.
*
* This download method does not provide user interface, or the ability to
* cancel or restart the download programmatically. For that, you should
* obtain a reference to a Download object using the createDownload function.
*
* Since the download cannot be restarted, any partially downloaded data will
* not be kept in case the download fails.
*
* @param aSource
* String containing the URI for the download source. Alternatively,
* may be an nsIURI or a DownloadSource object.
* @param aTarget
* String containing the path of the target file. Alternatively, may
* be an nsIFile or a DownloadTarget object.
* @param aOptions
* An optional object used to control the behavior of this function.
* You may pass an object with a subset of the following fields:
* {
* isPrivate: Indicates whether the download originated from a
* private window.
* }
*
* @return {Promise}
* @resolves When the download has finished successfully.
* @rejects JavaScript exception if the download failed.
*/
fetch: function (aSource, aTarget, aOptions) {
return this.createDownload({
source: aSource,
target: aTarget,
}).then(function D_SD_onSuccess(aDownload) {
if (aOptions && ("isPrivate" in aOptions)) {
aDownload.source.isPrivate = aOptions.isPrivate;
}
return aDownload.start();
});
},
/**
* Retrieves the specified type of DownloadList object. There is one download
* list for each type, and this method always retrieves a reference to the
* same download list when called with the same argument.
*
* Calling this function may cause the list of public downloads to be reloaded
* from the previous session, if it wasn't loaded already.
*
* @param aType
* This can be Downloads.PUBLIC, Downloads.PRIVATE, or Downloads.ALL.
* Downloads added to the Downloads.PUBLIC and Downloads.PRIVATE lists
* are reflected in the Downloads.ALL list, and downloads added to the
* Downloads.ALL list are also added to either the Downloads.PUBLIC or
* the Downloads.PRIVATE list based on their properties.
*
* @return {Promise}
* @resolves The requested DownloadList or DownloadCombinedList object.
* @rejects JavaScript exception.
*/
getList: function (aType)
{
if (!this._promiseListsInitialized) {
this._promiseListsInitialized = Task.spawn(function* () {
let publicList = new DownloadList();
let privateList = new DownloadList();
let combinedList = new DownloadCombinedList(publicList, privateList);
try {
yield DownloadIntegration.addListObservers(publicList, false);
yield DownloadIntegration.addListObservers(privateList, true);
yield DownloadIntegration.initializePublicDownloadList(publicList);
} catch (ex) {
Cu.reportError(ex);
}
let publicSummary = yield this.getSummary(Downloads.PUBLIC);
let privateSummary = yield this.getSummary(Downloads.PRIVATE);
let combinedSummary = yield this.getSummary(Downloads.ALL);
yield publicSummary.bindToList(publicList);
yield privateSummary.bindToList(privateList);
yield combinedSummary.bindToList(combinedList);
this._lists[Downloads.PUBLIC] = publicList;
this._lists[Downloads.PRIVATE] = privateList;
this._lists[Downloads.ALL] = combinedList;
}.bind(this));
}
return this._promiseListsInitialized.then(() => this._lists[aType]);
},
/**
* Promise resolved when the initialization of the download lists has
* completed, or null if initialization has never been requested.
*/
_promiseListsInitialized: null,
/**
* After initialization, this object is populated with one key for each type
* of download list that can be returned (Downloads.PUBLIC, Downloads.PRIVATE,
* or Downloads.ALL). The values are the DownloadList objects.
*/
_lists: {},
/**
* Retrieves the specified type of DownloadSummary object. There is one
* download summary for each type, and this method always retrieves a
* reference to the same download summary when called with the same argument.
*
* Calling this function does not cause the list of public downloads to be
* reloaded from the previous session. The summary will behave as if no
* downloads are present until the getList method is called.
*
* @param aType
* This can be Downloads.PUBLIC, Downloads.PRIVATE, or Downloads.ALL.
*
* @return {Promise}
* @resolves The requested DownloadList or DownloadCombinedList object.
* @rejects JavaScript exception.
*/
getSummary: function (aType)
{
if (aType != Downloads.PUBLIC && aType != Downloads.PRIVATE &&
aType != Downloads.ALL) {
throw new Error("Invalid aType argument.");
}
if (!(aType in this._summaries)) {
this._summaries[aType] = new DownloadSummary();
}
return Promise.resolve(this._summaries[aType]);
},
/**
* This object is populated by the getSummary method with one key for each
* type of object that can be returned (Downloads.PUBLIC, Downloads.PRIVATE,
* or Downloads.ALL). The values are the DownloadSummary objects.
*/
_summaries: {},
/**
* Returns the system downloads directory asynchronously.
* Mac OSX:
* User downloads directory
* XP/2K:
* My Documents/Downloads
* Vista and others:
* User downloads directory
* Linux:
* XDG user dir spec, with a fallback to Home/Downloads
* Android:
* standard downloads directory i.e. /sdcard
*
* @return {Promise}
* @resolves The downloads directory string path.
*/
getSystemDownloadsDirectory: function D_getSystemDownloadsDirectory() {
return DownloadIntegration.getSystemDownloadsDirectory();
},
/**
* Returns the preferred downloads directory based on the user preferences
* in the current profile asynchronously.
*
* @return {Promise}
* @resolves The downloads directory string path.
*/
getPreferredDownloadsDirectory: function D_getPreferredDownloadsDirectory() {
return DownloadIntegration.getPreferredDownloadsDirectory();
},
/**
* Returns the temporary directory where downloads are placed before the
* final location is chosen, or while the document is opened temporarily
* with an external application. This may or may not be the system temporary
* directory, based on the platform asynchronously.
*
* @return {Promise}
* @resolves The downloads directory string path.
*/
getTemporaryDownloadsDirectory: function D_getTemporaryDownloadsDirectory() {
return DownloadIntegration.getTemporaryDownloadsDirectory();
},
/**
* Constructor for a DownloadError object. When you catch an exception during
* a download, you can use this to verify if "ex instanceof Downloads.Error",
* before reading the exception properties with the error details.
*/
Error: DownloadError,
};

View file

@ -0,0 +1,2 @@
component {1b4c85df-cbdd-4bb6-b04e-613caece083c} DownloadLegacy.js
contract @mozilla.org/transfer;1 {1b4c85df-cbdd-4bb6-b04e-613caece083c}

View file

@ -0,0 +1,31 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
SOURCES += [
'DownloadPlatform.cpp',
]
EXTRA_COMPONENTS += [
'DownloadLegacy.js',
'Downloads.manifest',
]
EXTRA_JS_MODULES += [
'DownloadCore.jsm',
'DownloadImport.jsm',
'DownloadList.jsm',
'Downloads.jsm',
'DownloadStore.jsm',
'DownloadUIHelper.jsm',
]
EXTRA_PP_JS_MODULES += [
'DownloadIntegration.jsm',
]
FINAL_LIBRARY = 'xul'
CXXFLAGS += CONFIG['TK_CFLAGS']

View file

@ -0,0 +1,7 @@
"use strict";
module.exports = {
"extends": [
"../../../../../testing/mochitest/browser.eslintrc.js"
]
};

View file

@ -0,0 +1,7 @@
[DEFAULT]
support-files =
head.js
testFile.html
[browser_DownloadPDFSaver.js]
skip-if = os != "win"

View file

@ -0,0 +1,97 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests the PDF download saver, and tests using a window as a
* source for the copy download saver.
*/
"use strict";
/**
* Helper function to make sure a window reference exists on the download source.
*/
function* test_download_windowRef(aTab, aDownload) {
ok(aDownload.source.windowRef, "Download source had a window reference");
ok(aDownload.source.windowRef instanceof Ci.xpcIJSWeakReference, "Download window reference is a weak ref");
is(aDownload.source.windowRef.get(), aTab.linkedBrowser.contentWindow, "Download window exists during test");
}
/**
* Helper function to check the state of a completed download.
*/
function* test_download_state_complete(aTab, aDownload, aPrivate, aCanceled) {
ok(aDownload.source, "Download has a source");
is(aDownload.source.url, aTab.linkedBrowser.contentWindow.location, "Download source has correct url");
is(aDownload.source.isPrivate, aPrivate, "Download source has correct private state");
ok(aDownload.stopped, "Download is stopped");
is(aCanceled, aDownload.canceled, "Download has correct canceled state");
is(!aCanceled, aDownload.succeeded, "Download has correct succeeded state");
is(aDownload.error, null, "Download error is not defined");
}
function* test_createDownload_common(aPrivate, aType) {
let win = yield BrowserTestUtils.openNewBrowserWindow({ private : aPrivate});
let tab = yield BrowserTestUtils.openNewForegroundTab(win.gBrowser, getRootDirectory(gTestPath) + "testFile.html");
let download = yield Downloads.createDownload({
source: tab.linkedBrowser.contentWindow,
target: { path: getTempFile(TEST_TARGET_FILE_NAME_PDF).path },
saver: { type: aType }
});
yield test_download_windowRef(tab, download);
yield download.start();
yield test_download_state_complete(tab, download, aPrivate, false);
if (aType == "pdf") {
let signature = yield OS.File.read(download.target.path,
{ bytes: 4, encoding: "us-ascii" });
is(signature, "%PDF", "File exists and signature matches");
} else {
ok((yield OS.File.exists(download.target.path)), "File exists");
}
win.gBrowser.removeTab(tab);
win.close()
}
add_task(function* test_createDownload_pdf_private() {
yield test_createDownload_common(true, "pdf");
});
add_task(function* test_createDownload_pdf_not_private() {
yield test_createDownload_common(false, "pdf");
});
// Even for the copy saver, using a window should produce valid results
add_task(function* test_createDownload_copy_private() {
yield test_createDownload_common(true, "copy");
});
add_task(function* test_createDownload_copy_not_private() {
yield test_createDownload_common(false, "copy");
});
add_task(function* test_cancel_pdf_download() {
let tab = gBrowser.addTab(getRootDirectory(gTestPath) + "testFile.html");
yield promiseBrowserLoaded(tab.linkedBrowser);
let download = yield Downloads.createDownload({
source: tab.linkedBrowser.contentWindow,
target: { path: getTempFile(TEST_TARGET_FILE_NAME_PDF).path },
saver: "pdf",
});
yield test_download_windowRef(tab, download);
download.start().catch(() => {});
// Immediately cancel the download to test that it is erased correctly.
yield download.cancel();
yield test_download_state_complete(tab, download, false, true);
let exists = yield OS.File.exists(download.target.path)
ok(!exists, "Target file does not exist");
gBrowser.removeTab(tab);
});

View file

@ -0,0 +1,87 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Provides infrastructure for automated download components tests.
*/
"use strict";
// Globals
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
var Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
"resource://gre/modules/DownloadPaths.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
"resource://gre/modules/Downloads.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
"resource://testing-common/httpd.js");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
const TEST_TARGET_FILE_NAME_PDF = "test-download.pdf";
// Support functions
// While the previous test file should have deleted all the temporary files it
// used, on Windows these might still be pending deletion on the physical file
// system. Thus, start from a new base number every time, to make a collision
// with a file that is still pending deletion highly unlikely.
var gFileCounter = Math.floor(Math.random() * 1000000);
/**
* Returns a reference to a temporary file, that is guaranteed not to exist, and
* to have never been created before.
*
* @param aLeafName
* Suggested leaf name for the file to be created.
*
* @return nsIFile pointing to a non-existent file in a temporary directory.
*
* @note It is not enough to delete the file if it exists, or to delete the file
* after calling nsIFile.createUnique, because on Windows the delete
* operation in the file system may still be pending, preventing a new
* file with the same name to be created.
*/
function getTempFile(aLeafName)
{
// Prepend a serial number to the extension in the suggested leaf name.
let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
let leafName = base + "-" + gFileCounter + ext;
gFileCounter++;
// Get a file reference under the temporary directory for this test file.
let file = FileUtils.getFile("TmpD", [leafName]);
ok(!file.exists(), "Temp file does not exist");
registerCleanupFunction(function () {
if (file.exists()) {
file.remove(false);
}
});
return file;
}
function promiseBrowserLoaded(browser) {
return new Promise(resolve => {
browser.addEventListener("load", function onLoad(event) {
if (event.target == browser.contentDocument) {
browser.removeEventListener("load", onLoad, true);
resolve();
}
}, true);
});
}

View file

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<title>Test Save as PDF</title>
</head>
<body>
<p>Save me as a PDF!</p>
</body>
</html>

View file

@ -0,0 +1,7 @@
"use strict";
module.exports = {
"extends": [
"../../../../../testing/xpcshell/xpcshell.eslintrc.js"
]
};

View file

@ -0,0 +1 @@
This test string is downloaded.

View file

@ -0,0 +1,7 @@
"use strict";
module.exports = {
"extends": [
"../../../../../testing/xpcshell/xpcshell.eslintrc.js"
]
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,843 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Provides infrastructure for automated download components tests.
*/
"use strict";
// Globals
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
var Cr = Components.results;
Cu.import("resource://gre/modules/Integration.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
"resource://gre/modules/DownloadPaths.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
"resource://gre/modules/Downloads.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
"resource://testing-common/httpd.js");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
"resource://testing-common/PlacesTestUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MockRegistrar",
"resource://testing-common/MockRegistrar.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService",
"@mozilla.org/uriloader/external-helper-app-service;1",
Ci.nsIExternalHelperAppService);
Integration.downloads.defineModuleGetter(this, "DownloadIntegration",
"resource://gre/modules/DownloadIntegration.jsm");
const ServerSocket = Components.Constructor(
"@mozilla.org/network/server-socket;1",
"nsIServerSocket",
"init");
const BinaryOutputStream = Components.Constructor(
"@mozilla.org/binaryoutputstream;1",
"nsIBinaryOutputStream",
"setOutputStream")
XPCOMUtils.defineLazyServiceGetter(this, "gMIMEService",
"@mozilla.org/mime;1",
"nsIMIMEService");
const TEST_TARGET_FILE_NAME = "test-download.txt";
const TEST_STORE_FILE_NAME = "test-downloads.json";
const TEST_REFERRER_URL = "http://www.example.com/referrer.html";
const TEST_DATA_SHORT = "This test string is downloaded.";
// Generate using gzipCompressString in TelemetryController.jsm.
const TEST_DATA_SHORT_GZIP_ENCODED_FIRST = [
31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 11, 201, 200, 44, 86, 40, 73, 45, 46, 81, 40, 46, 41, 202, 204
];
const TEST_DATA_SHORT_GZIP_ENCODED_SECOND = [
75, 87, 0, 114, 83, 242, 203, 243, 114, 242, 19, 83, 82, 83, 244, 0, 151, 222, 109, 43, 31, 0, 0, 0
];
const TEST_DATA_SHORT_GZIP_ENCODED =
TEST_DATA_SHORT_GZIP_ENCODED_FIRST.concat(TEST_DATA_SHORT_GZIP_ENCODED_SECOND);
/**
* All the tests are implemented with add_task, this starts them automatically.
*/
function run_test()
{
do_get_profile();
run_next_test();
}
// Support functions
/**
* HttpServer object initialized before tests start.
*/
var gHttpServer;
/**
* Given a file name, returns a string containing an URI that points to the file
* on the currently running instance of the test HTTP server.
*/
function httpUrl(aFileName) {
return "http://localhost:" + gHttpServer.identity.primaryPort + "/" +
aFileName;
}
// While the previous test file should have deleted all the temporary files it
// used, on Windows these might still be pending deletion on the physical file
// system. Thus, start from a new base number every time, to make a collision
// with a file that is still pending deletion highly unlikely.
var gFileCounter = Math.floor(Math.random() * 1000000);
/**
* Returns a reference to a temporary file, that is guaranteed not to exist, and
* to have never been created before.
*
* @param aLeafName
* Suggested leaf name for the file to be created.
*
* @return nsIFile pointing to a non-existent file in a temporary directory.
*
* @note It is not enough to delete the file if it exists, or to delete the file
* after calling nsIFile.createUnique, because on Windows the delete
* operation in the file system may still be pending, preventing a new
* file with the same name to be created.
*/
function getTempFile(aLeafName)
{
// Prepend a serial number to the extension in the suggested leaf name.
let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
let leafName = base + "-" + gFileCounter + ext;
gFileCounter++;
// Get a file reference under the temporary directory for this test file.
let file = FileUtils.getFile("TmpD", [leafName]);
do_check_false(file.exists());
do_register_cleanup(function () {
try {
file.remove(false)
} catch (e) {
if (!(e instanceof Components.Exception &&
(e.result == Cr.NS_ERROR_FILE_ACCESS_DENIED ||
e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST ||
e.result == Cr.NS_ERROR_FILE_NOT_FOUND))) {
throw e;
}
// On Windows, we may get an access denied error if the file existed before,
// and was recently deleted.
// Don't bother checking file.exists() as that may also cause an access
// denied error.
}
});
return file;
}
/**
* Waits for pending events to be processed.
*
* @return {Promise}
* @resolves When pending events have been processed.
* @rejects Never.
*/
function promiseExecuteSoon()
{
let deferred = Promise.defer();
do_execute_soon(deferred.resolve);
return deferred.promise;
}
/**
* Waits for a pending events to be processed after a timeout.
*
* @return {Promise}
* @resolves When pending events have been processed.
* @rejects Never.
*/
function promiseTimeout(aTime)
{
let deferred = Promise.defer();
do_timeout(aTime, deferred.resolve);
return deferred.promise;
}
/**
* Waits for a new history visit to be notified for the specified URI.
*
* @param aUrl
* String containing the URI that will be visited.
*
* @return {Promise}
* @resolves Array [aTime, aTransitionType] from nsINavHistoryObserver.onVisit.
* @rejects Never.
*/
function promiseWaitForVisit(aUrl)
{
let deferred = Promise.defer();
let uri = NetUtil.newURI(aUrl);
PlacesUtils.history.addObserver({
QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]),
onBeginUpdateBatch: function () {},
onEndUpdateBatch: function () {},
onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID,
aTransitionType, aGUID, aHidden) {
if (aURI.equals(uri)) {
PlacesUtils.history.removeObserver(this);
deferred.resolve([aTime, aTransitionType]);
}
},
onTitleChanged: function () {},
onDeleteURI: function () {},
onClearHistory: function () {},
onPageChanged: function () {},
onDeleteVisits: function () {},
}, false);
return deferred.promise;
}
/**
* Check browsing history to see whether the given URI has been visited.
*
* @param aUrl
* String containing the URI that will be visited.
*
* @return {Promise}
* @resolves Boolean indicating whether the URI has been visited.
* @rejects JavaScript exception.
*/
function promiseIsURIVisited(aUrl) {
let deferred = Promise.defer();
PlacesUtils.asyncHistory.isURIVisited(NetUtil.newURI(aUrl),
function (aURI, aIsVisited) {
deferred.resolve(aIsVisited);
});
return deferred.promise;
}
/**
* Creates a new Download object, setting a temporary file as the target.
*
* @param aSourceUrl
* String containing the URI for the download source, or null to use
* httpUrl("source.txt").
*
* @return {Promise}
* @resolves The newly created Download object.
* @rejects JavaScript exception.
*/
function promiseNewDownload(aSourceUrl) {
return Downloads.createDownload({
source: aSourceUrl || httpUrl("source.txt"),
target: getTempFile(TEST_TARGET_FILE_NAME),
});
}
/**
* Starts a new download using the nsIWebBrowserPersist interface, and controls
* it using the legacy nsITransfer interface.
*
* @param aSourceUrl
* String containing the URI for the download source, or null to use
* httpUrl("source.txt").
* @param aOptions
* An optional object used to control the behavior of this function.
* You may pass an object with a subset of the following fields:
* {
* isPrivate: Boolean indicating whether the download originated from a
* private window.
* targetFile: nsIFile for the target, or null to use a temporary file.
* outPersist: Receives a reference to the created nsIWebBrowserPersist
* instance.
* launchWhenSucceeded: Boolean indicating whether the target should
* be launched when it has completed successfully.
* launcherPath: String containing the path of the custom executable to
* use to launch the target of the download.
* }
*
* @return {Promise}
* @resolves The Download object created as a consequence of controlling the
* download through the legacy nsITransfer interface.
* @rejects Never. The current test fails in case of exceptions.
*/
function promiseStartLegacyDownload(aSourceUrl, aOptions) {
let sourceURI = NetUtil.newURI(aSourceUrl || httpUrl("source.txt"));
let targetFile = (aOptions && aOptions.targetFile)
|| getTempFile(TEST_TARGET_FILE_NAME);
let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
.createInstance(Ci.nsIWebBrowserPersist);
if (aOptions) {
aOptions.outPersist = persist;
}
let fileExtension = null, mimeInfo = null;
let match = sourceURI.path.match(/\.([^.\/]+)$/);
if (match) {
fileExtension = match[1];
}
if (fileExtension) {
try {
mimeInfo = gMIMEService.getFromTypeAndExtension(null, fileExtension);
mimeInfo.preferredAction = Ci.nsIMIMEInfo.saveToDisk;
} catch (ex) { }
}
if (aOptions && aOptions.launcherPath) {
do_check_true(mimeInfo != null);
let localHandlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]
.createInstance(Ci.nsILocalHandlerApp);
localHandlerApp.executable = new FileUtils.File(aOptions.launcherPath);
mimeInfo.preferredApplicationHandler = localHandlerApp;
mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
}
if (aOptions && aOptions.launchWhenSucceeded) {
do_check_true(mimeInfo != null);
mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
}
// Apply decoding if required by the "Content-Encoding" header.
persist.persistFlags &= ~Ci.nsIWebBrowserPersist.PERSIST_FLAGS_NO_CONVERSION;
persist.persistFlags |=
Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
let transfer = Cc["@mozilla.org/transfer;1"].createInstance(Ci.nsITransfer);
let deferred = Promise.defer();
Downloads.getList(Downloads.ALL).then(function (aList) {
// Temporarily register a view that will get notified when the download we
// are controlling becomes visible in the list of downloads.
aList.addView({
onDownloadAdded: function (aDownload) {
aList.removeView(this).then(null, do_report_unexpected_exception);
// Remove the download to keep the list empty for the next test. This
// also allows the caller to register the "onchange" event directly.
let promise = aList.remove(aDownload);
// When the download object is ready, make it available to the caller.
promise.then(() => deferred.resolve(aDownload),
do_report_unexpected_exception);
},
}).then(null, do_report_unexpected_exception);
let isPrivate = aOptions && aOptions.isPrivate;
// Initialize the components so they reference each other. This will cause
// the Download object to be created and added to the public downloads.
transfer.init(sourceURI, NetUtil.newURI(targetFile), null, mimeInfo, null,
null, persist, isPrivate);
persist.progressListener = transfer;
// Start the actual download process.
persist.savePrivacyAwareURI(sourceURI, null, null, 0, null, null, targetFile,
isPrivate);
}.bind(this)).then(null, do_report_unexpected_exception);
return deferred.promise;
}
/**
* Starts a new download using the nsIHelperAppService interface, and controls
* it using the legacy nsITransfer interface. The source of the download will
* be "interruptible_resumable.txt" and partially downloaded data will be kept.
*
* @param aSourceUrl
* String containing the URI for the download source, or null to use
* httpUrl("interruptible_resumable.txt").
*
* @return {Promise}
* @resolves The Download object created as a consequence of controlling the
* download through the legacy nsITransfer interface.
* @rejects Never. The current test fails in case of exceptions.
*/
function promiseStartExternalHelperAppServiceDownload(aSourceUrl) {
let sourceURI = NetUtil.newURI(aSourceUrl ||
httpUrl("interruptible_resumable.txt"));
let deferred = Promise.defer();
Downloads.getList(Downloads.PUBLIC).then(function (aList) {
// Temporarily register a view that will get notified when the download we
// are controlling becomes visible in the list of downloads.
aList.addView({
onDownloadAdded: function (aDownload) {
aList.removeView(this).then(null, do_report_unexpected_exception);
// Remove the download to keep the list empty for the next test. This
// also allows the caller to register the "onchange" event directly.
let promise = aList.remove(aDownload);
// When the download object is ready, make it available to the caller.
promise.then(() => deferred.resolve(aDownload),
do_report_unexpected_exception);
},
}).then(null, do_report_unexpected_exception);
let channel = NetUtil.newChannel({
uri: sourceURI,
loadUsingSystemPrincipal: true
});
// Start the actual download process.
channel.asyncOpen2({
contentListener: null,
onStartRequest: function (aRequest, aContext)
{
let requestChannel = aRequest.QueryInterface(Ci.nsIChannel);
this.contentListener = gExternalHelperAppService.doContent(
requestChannel.contentType, aRequest, null, true);
this.contentListener.onStartRequest(aRequest, aContext);
},
onStopRequest: function (aRequest, aContext, aStatusCode)
{
this.contentListener.onStopRequest(aRequest, aContext, aStatusCode);
},
onDataAvailable: function (aRequest, aContext, aInputStream, aOffset,
aCount)
{
this.contentListener.onDataAvailable(aRequest, aContext, aInputStream,
aOffset, aCount);
},
});
}.bind(this)).then(null, do_report_unexpected_exception);
return deferred.promise;
}
/**
* Waits for a download to reach half of its progress, in case it has not
* reached the expected progress already.
*
* @param aDownload
* The Download object to wait upon.
*
* @return {Promise}
* @resolves When the download has reached half of its progress.
* @rejects Never.
*/
function promiseDownloadMidway(aDownload) {
let deferred = Promise.defer();
// Wait for the download to reach half of its progress.
let onchange = function () {
if (!aDownload.stopped && !aDownload.canceled && aDownload.progress == 50) {
aDownload.onchange = null;
deferred.resolve();
}
};
// Register for the notification, but also call the function directly in
// case the download already reached the expected progress.
aDownload.onchange = onchange;
onchange();
return deferred.promise;
}
/**
* Waits for a download to finish, in case it has not finished already.
*
* @param aDownload
* The Download object to wait upon.
*
* @return {Promise}
* @resolves When the download has finished successfully.
* @rejects JavaScript exception if the download failed.
*/
function promiseDownloadStopped(aDownload) {
if (!aDownload.stopped) {
// The download is in progress, wait for the current attempt to finish and
// report any errors that may occur.
return aDownload.start();
}
if (aDownload.succeeded) {
return Promise.resolve();
}
// The download failed or was canceled.
return Promise.reject(aDownload.error || new Error("Download canceled."));
}
/**
* Returns a new public or private DownloadList object.
*
* @param aIsPrivate
* True for the private list, false or undefined for the public list.
*
* @return {Promise}
* @resolves The newly created DownloadList object.
* @rejects JavaScript exception.
*/
function promiseNewList(aIsPrivate)
{
// We need to clear all the internal state for the list and summary objects,
// since all the objects are interdependent internally.
Downloads._promiseListsInitialized = null;
Downloads._lists = {};
Downloads._summaries = {};
return Downloads.getList(aIsPrivate ? Downloads.PRIVATE : Downloads.PUBLIC);
}
/**
* Ensures that the given file contents are equal to the given string.
*
* @param aPath
* String containing the path of the file whose contents should be
* verified.
* @param aExpectedContents
* String containing the octets that are expected in the file.
*
* @return {Promise}
* @resolves When the operation completes.
* @rejects Never.
*/
function promiseVerifyContents(aPath, aExpectedContents)
{
return Task.spawn(function* () {
let file = new FileUtils.File(aPath);
if (!(yield OS.File.exists(aPath))) {
do_throw("File does not exist: " + aPath);
}
if ((yield OS.File.stat(aPath)).size == 0) {
do_throw("File is empty: " + aPath);
}
let deferred = Promise.defer();
NetUtil.asyncFetch(
{ uri: NetUtil.newURI(file), loadUsingSystemPrincipal: true },
function(aInputStream, aStatus) {
do_check_true(Components.isSuccessCode(aStatus));
let contents = NetUtil.readInputStreamToString(aInputStream,
aInputStream.available());
if (contents.length > TEST_DATA_SHORT.length * 2 ||
/[^\x20-\x7E]/.test(contents)) {
// Do not print the entire content string to the test log.
do_check_eq(contents.length, aExpectedContents.length);
do_check_true(contents == aExpectedContents);
} else {
// Print the string if it is short and made of printable characters.
do_check_eq(contents, aExpectedContents);
}
deferred.resolve();
});
yield deferred.promise;
});
}
/**
* Starts a socket listener that closes each incoming connection.
*
* @returns nsIServerSocket that listens for connections. Call its "close"
* method to stop listening and free the server port.
*/
function startFakeServer()
{
let serverSocket = new ServerSocket(-1, true, -1);
serverSocket.asyncListen({
onSocketAccepted: function (aServ, aTransport) {
aTransport.close(Cr.NS_BINDING_ABORTED);
},
onStopListening: function () { },
});
return serverSocket;
}
/**
* This is an internal reference that should not be used directly by tests.
*/
var _gDeferResponses = Promise.defer();
/**
* Ensures that all the interruptible requests started after this function is
* called won't complete until the continueResponses function is called.
*
* Normally, the internal HTTP server returns all the available data as soon as
* a request is received. In order for some requests to be served one part at a
* time, special interruptible handlers are registered on the HTTP server. This
* allows testing events or actions that need to happen in the middle of a
* download.
*
* For example, the handler accessible at the httpUri("interruptible.txt")
* address returns the TEST_DATA_SHORT text, then it may block until the
* continueResponses method is called. At this point, the handler sends the
* TEST_DATA_SHORT text again to complete the response.
*
* If an interruptible request is started before the function is called, it may
* or may not be blocked depending on the actual sequence of events.
*/
function mustInterruptResponses()
{
// If there are pending blocked requests, allow them to complete. This is
// done to prevent requests from being blocked forever, but should not affect
// the test logic, since previously started requests should not be monitored
// on the client side anymore.
_gDeferResponses.resolve();
do_print("Interruptible responses will be blocked midway.");
_gDeferResponses = Promise.defer();
}
/**
* Allows all the current and future interruptible requests to complete.
*/
function continueResponses()
{
do_print("Interruptible responses are now allowed to continue.");
_gDeferResponses.resolve();
}
/**
* Registers an interruptible response handler.
*
* @param aPath
* Path passed to nsIHttpServer.registerPathHandler.
* @param aFirstPartFn
* This function is called when the response is received, with the
* aRequest and aResponse arguments of the server.
* @param aSecondPartFn
* This function is called with the aRequest and aResponse arguments of
* the server, when the continueResponses function is called.
*/
function registerInterruptibleHandler(aPath, aFirstPartFn, aSecondPartFn)
{
gHttpServer.registerPathHandler(aPath, function (aRequest, aResponse) {
do_print("Interruptible request started.");
// Process the first part of the response.
aResponse.processAsync();
aFirstPartFn(aRequest, aResponse);
// Wait on the current deferred object, then finish the request.
_gDeferResponses.promise.then(function RIH_onSuccess() {
aSecondPartFn(aRequest, aResponse);
aResponse.finish();
do_print("Interruptible request finished.");
}).then(null, Cu.reportError);
});
}
/**
* Ensure the given date object is valid.
*
* @param aDate
* The date object to be checked. This value can be null.
*/
function isValidDate(aDate) {
return aDate && aDate.getTime && !isNaN(aDate.getTime());
}
/**
* Position of the first byte served by the "interruptible_resumable.txt"
* handler during the most recent response.
*/
var gMostRecentFirstBytePos;
// Initialization functions common to all tests
add_task(function test_common_initialize()
{
// Start the HTTP server.
gHttpServer = new HttpServer();
gHttpServer.registerDirectory("/", do_get_file("../data"));
gHttpServer.start(-1);
do_register_cleanup(() => {
return new Promise(resolve => {
// Ensure all the pending HTTP requests have a chance to finish.
continueResponses();
// Stop the HTTP server, calling resolve when it's done.
gHttpServer.stop(resolve);
});
});
// Cache locks might prevent concurrent requests to the same resource, and
// this may block tests that use the interruptible handlers.
Services.prefs.setBoolPref("browser.cache.disk.enable", false);
Services.prefs.setBoolPref("browser.cache.memory.enable", false);
do_register_cleanup(function () {
Services.prefs.clearUserPref("browser.cache.disk.enable");
Services.prefs.clearUserPref("browser.cache.memory.enable");
});
registerInterruptibleHandler("/interruptible.txt",
function firstPart(aRequest, aResponse) {
aResponse.setHeader("Content-Type", "text/plain", false);
aResponse.setHeader("Content-Length", "" + (TEST_DATA_SHORT.length * 2),
false);
aResponse.write(TEST_DATA_SHORT);
}, function secondPart(aRequest, aResponse) {
aResponse.write(TEST_DATA_SHORT);
});
registerInterruptibleHandler("/interruptible_resumable.txt",
function firstPart(aRequest, aResponse) {
aResponse.setHeader("Content-Type", "text/plain", false);
// Determine if only part of the data should be sent.
let data = TEST_DATA_SHORT + TEST_DATA_SHORT;
if (aRequest.hasHeader("Range")) {
var matches = aRequest.getHeader("Range")
.match(/^\s*bytes=(\d+)?-(\d+)?\s*$/);
var firstBytePos = (matches[1] === undefined) ? 0 : matches[1];
var lastBytePos = (matches[2] === undefined) ? data.length - 1
: matches[2];
if (firstBytePos >= data.length) {
aResponse.setStatusLine(aRequest.httpVersion, 416,
"Requested Range Not Satisfiable");
aResponse.setHeader("Content-Range", "*/" + data.length, false);
aResponse.finish();
return;
}
aResponse.setStatusLine(aRequest.httpVersion, 206, "Partial Content");
aResponse.setHeader("Content-Range", firstBytePos + "-" +
lastBytePos + "/" +
data.length, false);
data = data.substring(firstBytePos, lastBytePos + 1);
gMostRecentFirstBytePos = firstBytePos;
} else {
gMostRecentFirstBytePos = 0;
}
aResponse.setHeader("Content-Length", "" + data.length, false);
aResponse.write(data.substring(0, data.length / 2));
// Store the second part of the data on the response object, so that it
// can be used by the secondPart function.
aResponse.secondPartData = data.substring(data.length / 2);
}, function secondPart(aRequest, aResponse) {
aResponse.write(aResponse.secondPartData);
});
registerInterruptibleHandler("/interruptible_gzip.txt",
function firstPart(aRequest, aResponse) {
aResponse.setHeader("Content-Type", "text/plain", false);
aResponse.setHeader("Content-Encoding", "gzip", false);
aResponse.setHeader("Content-Length", "" + TEST_DATA_SHORT_GZIP_ENCODED.length);
let bos = new BinaryOutputStream(aResponse.bodyOutputStream);
bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED_FIRST,
TEST_DATA_SHORT_GZIP_ENCODED_FIRST.length);
}, function secondPart(aRequest, aResponse) {
let bos = new BinaryOutputStream(aResponse.bodyOutputStream);
bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED_SECOND,
TEST_DATA_SHORT_GZIP_ENCODED_SECOND.length);
});
gHttpServer.registerPathHandler("/shorter-than-content-length-http-1-1.txt",
function (aRequest, aResponse) {
aResponse.processAsync();
aResponse.setStatusLine("1.1", 200, "OK");
aResponse.setHeader("Content-Type", "text/plain", false);
aResponse.setHeader("Content-Length", "" + (TEST_DATA_SHORT.length * 2),
false);
aResponse.write(TEST_DATA_SHORT);
aResponse.finish();
});
// This URL will emulate being blocked by Windows Parental controls
gHttpServer.registerPathHandler("/parentalblocked.zip",
function (aRequest, aResponse) {
aResponse.setStatusLine(aRequest.httpVersion, 450,
"Blocked by Windows Parental Controls");
});
// During unit tests, most of the functions that require profile access or
// operating system features will be disabled. Individual tests may override
// them again to check for specific behaviors.
Integration.downloads.register(base => ({
__proto__: base,
loadPublicDownloadListFromStore: () => Promise.resolve(),
shouldKeepBlockedData: () => Promise.resolve(false),
shouldBlockForParentalControls: () => Promise.resolve(false),
shouldBlockForRuntimePermissions: () => Promise.resolve(false),
shouldBlockForReputationCheck: () => Promise.resolve({
shouldBlock: false,
verdict: "",
}),
confirmLaunchExecutable: () => Promise.resolve(),
launchFile: () => Promise.resolve(),
showContainingDirectory: () => Promise.resolve(),
// This flag allows re-enabling the default observers during their tests.
allowObservers: false,
addListObservers() {
return this.allowObservers ? super.addListObservers(...arguments)
: Promise.resolve();
},
// This flag allows re-enabling the download directory logic for its tests.
_allowDirectories: false,
set allowDirectories(value) {
this._allowDirectories = value;
// We have to invalidate the previously computed directory path.
this._downloadsDirectory = null;
},
_getDirectory(name) {
return super._getDirectory(this._allowDirectories ? name : "TmpD");
},
}));
// Make sure that downloads started using nsIExternalHelperAppService are
// saved to disk without asking for a destination interactively.
let mock = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]),
promptForSaveToFileAsync(aLauncher,
aWindowContext,
aDefaultFileName,
aSuggestedFileExtension,
aForcePrompt) {
// The dialog should create the empty placeholder file.
let file = getTempFile(TEST_TARGET_FILE_NAME);
file.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
aLauncher.saveDestinationAvailable(file);
},
};
let cid = MockRegistrar.register("@mozilla.org/helperapplauncherdialog;1", mock);
do_register_cleanup(() => {
MockRegistrar.unregister(cid);
});
});

View file

@ -0,0 +1,87 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests the main download interfaces using DownloadCopySaver.
*/
"use strict";
XPCOMUtils.defineLazyModuleGetter(this, "DownloadError",
"resource://gre/modules/DownloadCore.jsm");
// Execution of common tests
var gUseLegacySaver = false;
var scriptFile = do_get_file("common_test_Download.js");
Services.scriptloader.loadSubScript(NetUtil.newURI(scriptFile).spec);
// Tests
/**
* Tests the DownloadError object.
*/
add_task(function test_DownloadError()
{
let error = new DownloadError({ result: Cr.NS_ERROR_NOT_RESUMABLE,
message: "Not resumable."});
do_check_eq(error.result, Cr.NS_ERROR_NOT_RESUMABLE);
do_check_eq(error.message, "Not resumable.");
do_check_false(error.becauseSourceFailed);
do_check_false(error.becauseTargetFailed);
do_check_false(error.becauseBlocked);
do_check_false(error.becauseBlockedByParentalControls);
error = new DownloadError({ message: "Unknown error."});
do_check_eq(error.result, Cr.NS_ERROR_FAILURE);
do_check_eq(error.message, "Unknown error.");
error = new DownloadError({ result: Cr.NS_ERROR_NOT_RESUMABLE });
do_check_eq(error.result, Cr.NS_ERROR_NOT_RESUMABLE);
do_check_true(error.message.indexOf("Exception") > 0);
// becauseSourceFailed will be set, but not the unknown property.
error = new DownloadError({ message: "Unknown error.",
becauseSourceFailed: true,
becauseUnknown: true });
do_check_true(error.becauseSourceFailed);
do_check_false("becauseUnknown" in error);
error = new DownloadError({ result: Cr.NS_ERROR_MALFORMED_URI,
inferCause: true });
do_check_eq(error.result, Cr.NS_ERROR_MALFORMED_URI);
do_check_true(error.becauseSourceFailed);
do_check_false(error.becauseTargetFailed);
do_check_false(error.becauseBlocked);
do_check_false(error.becauseBlockedByParentalControls);
// This test does not set inferCause, so becauseSourceFailed will not be set.
error = new DownloadError({ result: Cr.NS_ERROR_MALFORMED_URI });
do_check_eq(error.result, Cr.NS_ERROR_MALFORMED_URI);
do_check_false(error.becauseSourceFailed);
error = new DownloadError({ result: Cr.NS_ERROR_FILE_INVALID_PATH,
inferCause: true });
do_check_eq(error.result, Cr.NS_ERROR_FILE_INVALID_PATH);
do_check_false(error.becauseSourceFailed);
do_check_true(error.becauseTargetFailed);
do_check_false(error.becauseBlocked);
do_check_false(error.becauseBlockedByParentalControls);
error = new DownloadError({ becauseBlocked: true });
do_check_eq(error.message, "Download blocked.");
do_check_false(error.becauseSourceFailed);
do_check_false(error.becauseTargetFailed);
do_check_true(error.becauseBlocked);
do_check_false(error.becauseBlockedByParentalControls);
error = new DownloadError({ becauseBlockedByParentalControls: true });
do_check_eq(error.message, "Download blocked.");
do_check_false(error.becauseSourceFailed);
do_check_false(error.becauseTargetFailed);
do_check_true(error.becauseBlocked);
do_check_true(error.becauseBlockedByParentalControls);
});

View file

@ -0,0 +1,701 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests the DownloadImport object.
*/
"use strict";
// Globals
XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
"resource://gre/modules/Sqlite.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadImport",
"resource://gre/modules/DownloadImport.jsm");
// Importable states
const DOWNLOAD_NOTSTARTED = -1;
const DOWNLOAD_DOWNLOADING = 0;
const DOWNLOAD_PAUSED = 4;
const DOWNLOAD_QUEUED = 5;
// Non importable states
const DOWNLOAD_FAILED = 2;
const DOWNLOAD_CANCELED = 3;
const DOWNLOAD_BLOCKED_PARENTAL = 6;
const DOWNLOAD_SCANNING = 7;
const DOWNLOAD_DIRTY = 8;
const DOWNLOAD_BLOCKED_POLICY = 9;
// The TEST_DATA_TAINTED const is a version of TEST_DATA_SHORT in which the
// beginning of the data was changed (with the TEST_DATA_REPLACEMENT value).
// We use this to test that the entityID is properly imported and the download
// can be resumed from where it was paused.
// For simplification purposes, the test requires that TEST_DATA_SHORT and
// TEST_DATA_TAINTED have the same length.
const TEST_DATA_REPLACEMENT = "-changed- ";
const TEST_DATA_TAINTED = TEST_DATA_REPLACEMENT +
TEST_DATA_SHORT.substr(TEST_DATA_REPLACEMENT.length);
const TEST_DATA_LENGTH = TEST_DATA_SHORT.length;
// The length of the partial file that we'll write to disk as an existing
// ongoing download.
const TEST_DATA_PARTIAL_LENGTH = TEST_DATA_REPLACEMENT.length;
// The value of the "maxBytes" column stored in the DB about the downloads.
// It's intentionally different than TEST_DATA_LENGTH to test that each value
// is seen when expected.
const MAXBYTES_IN_DB = TEST_DATA_LENGTH - 10;
var gDownloadsRowToImport;
var gDownloadsRowNonImportable;
/**
* Creates a database with an empty moz_downloads table and leaves an
* open connection to it.
*
* @param aPath
* String containing the path of the database file to be created.
* @param aSchemaVersion
* Number with the version of the database schema to set.
*
* @return {Promise}
* @resolves The open connection to the database.
* @rejects If an error occurred during the database creation.
*/
function promiseEmptyDatabaseConnection({aPath, aSchemaVersion}) {
return Task.spawn(function* () {
let connection = yield Sqlite.openConnection({ path: aPath });
yield connection.execute("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)");
yield connection.setSchemaVersion(aSchemaVersion);
return connection;
});
}
/**
* Inserts a new entry in the database with the given columns' values.
*
* @param aConnection
* The database connection.
* @param aDownloadRow
* An object representing the values for each column of the row
* being inserted.
*
* @return {Promise}
* @resolves When the operation completes.
* @rejects If there's an error inserting the row.
*/
function promiseInsertRow(aConnection, aDownloadRow) {
// We can't use the aDownloadRow obj directly in the execute statement
// because the obj bind code in Sqlite.jsm doesn't allow objects
// with extra properties beyond those being binded. So we might as well
// use an array as it is simpler.
let values = [
aDownloadRow.source, aDownloadRow.target, aDownloadRow.tempPath,
aDownloadRow.startTime.getTime() * 1000, aDownloadRow.state,
aDownloadRow.referrer, aDownloadRow.entityID, aDownloadRow.maxBytes,
aDownloadRow.mimeType, aDownloadRow.preferredApplication,
aDownloadRow.preferredAction, aDownloadRow.autoResume
];
return aConnection.execute("INSERT INTO moz_downloads ("
+ "name, source, target, tempPath, startTime,"
+ "endTime, state, referrer, entityID, currBytes,"
+ "maxBytes, mimeType, preferredApplication,"
+ "preferredAction, autoResume, guid)"
+ "VALUES ("
+ "'', ?, ?, ?, ?, " // name,
+ "0, ?, ?, ?, 0, " // endTime, currBytes
+ " ?, ?, ?, " //
+ " ?, ?, '')", // and guid are not imported
values);
}
/**
* Retrieves the number of rows in the moz_downloads table of the
* database.
*
* @param aConnection
* The database connection.
*
* @return {Promise}
* @resolves With the number of rows.
* @rejects Never.
*/
function promiseTableCount(aConnection) {
return aConnection.execute("SELECT COUNT(*) FROM moz_downloads")
.then(res => res[0].getResultByName("COUNT(*)"))
.then(null, Cu.reportError);
}
/**
* Briefly opens a network channel to a given URL to retrieve
* the entityID of this url, as generated by the network code.
*
* @param aUrl
* The URL to retrieve the entityID.
*
* @return {Promise}
* @resolves The EntityID of the given URL.
* @rejects When there's a problem accessing the URL.
*/
function promiseEntityID(aUrl) {
let deferred = Promise.defer();
let entityID = "";
let channel = NetUtil.newChannel({
uri: NetUtil.newURI(aUrl),
loadUsingSystemPrincipal: true
});
channel.asyncOpen2({
onStartRequest: function (aRequest) {
if (aRequest instanceof Ci.nsIResumableChannel) {
entityID = aRequest.entityID;
}
aRequest.cancel(Cr.NS_BINDING_ABORTED);
},
onStopRequest: function (aRequest, aContext, aStatusCode) {
if (aStatusCode == Cr.NS_BINDING_ABORTED) {
deferred.resolve(entityID);
} else {
deferred.reject("Unexpected status code received");
}
},
onDataAvailable: function () {}
});
return deferred.promise;
}
/**
* Gets a file path to a temporary writeable download target, in the
* correct format as expected to be stored in the downloads database,
* which is file:///absolute/path/to/file
*
* @param aLeafName
* A hint leaf name for the file.
*
* @return String The path to the download target.
*/
function getDownloadTarget(aLeafName) {
return NetUtil.newURI(getTempFile(aLeafName)).spec;
}
/**
* Generates a temporary partial file to use as an in-progress
* download. The file is written to disk with a part of the total expected
* download content pre-written.
*
* @param aLeafName
* A hint leaf name for the file.
* @param aTainted
* A boolean value. When true, the partial content of the file
* will be different from the expected content of the original source
* file. See the declaration of TEST_DATA_TAINTED for more information.
*
* @return {Promise}
* @resolves When the operation completes, and returns a string with the path
* to the generated file.
* @rejects If there's an error writing the file.
*/
function getPartialFile(aLeafName, aTainted = false) {
let tempDownload = getTempFile(aLeafName);
let partialContent = aTainted
? TEST_DATA_TAINTED.substr(0, TEST_DATA_PARTIAL_LENGTH)
: TEST_DATA_SHORT.substr(0, TEST_DATA_PARTIAL_LENGTH);
return OS.File.writeAtomic(tempDownload.path, partialContent,
{ tmpPath: tempDownload.path + ".tmp",
flush: true })
.then(() => tempDownload.path);
}
/**
* Generates a Date object to be used as the startTime for the download rows
* in the DB. A date that is obviously different from the current time is
* generated to make sure this stored data and a `new Date()` can't collide.
*
* @param aOffset
* A offset from the base generated date is used to differentiate each
* row in the database.
*
* @return A Date object.
*/
function getStartTime(aOffset) {
return new Date(1000000 + (aOffset * 10000));
}
/**
* Performs various checks on an imported Download object to make sure
* all properties are properly set as expected from the import procedure.
*
* @param aDownload
* The Download object to be checked.
* @param aDownloadRow
* An object that represents a row from the original database table,
* with extra properties describing expected values that are not
* explictly part of the database.
*
* @return {Promise}
* @resolves When the operation completes
* @rejects Never
*/
function checkDownload(aDownload, aDownloadRow) {
return Task.spawn(function*() {
do_check_eq(aDownload.source.url, aDownloadRow.source);
do_check_eq(aDownload.source.referrer, aDownloadRow.referrer);
do_check_eq(aDownload.target.path,
NetUtil.newURI(aDownloadRow.target)
.QueryInterface(Ci.nsIFileURL).file.path);
do_check_eq(aDownload.target.partFilePath, aDownloadRow.tempPath);
if (aDownloadRow.expectedResume) {
do_check_true(!aDownload.stopped || aDownload.succeeded);
yield promiseDownloadStopped(aDownload);
do_check_true(aDownload.succeeded);
do_check_eq(aDownload.progress, 100);
// If the download has resumed, a new startTime will be set.
// By calling toJSON we're also testing that startTime is a Date object.
do_check_neq(aDownload.startTime.toJSON(),
aDownloadRow.startTime.toJSON());
} else {
do_check_false(aDownload.succeeded);
do_check_eq(aDownload.startTime.toJSON(),
aDownloadRow.startTime.toJSON());
}
do_check_eq(aDownload.stopped, true);
let serializedSaver = aDownload.saver.toSerializable();
if (typeof(serializedSaver) == "object") {
do_check_eq(serializedSaver.type, "copy");
} else {
do_check_eq(serializedSaver, "copy");
}
if (aDownloadRow.entityID) {
do_check_eq(aDownload.saver.entityID, aDownloadRow.entityID);
}
do_check_eq(aDownload.currentBytes, aDownloadRow.expectedCurrentBytes);
do_check_eq(aDownload.totalBytes, aDownloadRow.expectedTotalBytes);
if (aDownloadRow.expectedContent) {
let fileToCheck = aDownloadRow.expectedResume
? aDownload.target.path
: aDownload.target.partFilePath;
yield promiseVerifyContents(fileToCheck, aDownloadRow.expectedContent);
}
do_check_eq(aDownload.contentType, aDownloadRow.expectedContentType);
do_check_eq(aDownload.launcherPath, aDownloadRow.preferredApplication);
do_check_eq(aDownload.launchWhenSucceeded,
aDownloadRow.preferredAction != Ci.nsIMIMEInfo.saveToDisk);
});
}
// Preparation tasks
/**
* Prepares the list of downloads to be added to the database that should
* be imported by the import procedure.
*/
add_task(function* prepareDownloadsToImport() {
let sourceUrl = httpUrl("source.txt");
let sourceEntityId = yield promiseEntityID(sourceUrl);
gDownloadsRowToImport = [
// Paused download with autoResume and a partial file. By
// setting the correct entityID the download can resume from
// where it stopped, and to test that this works properly we
// intentionally set different data in the beginning of the
// partial file to make sure it was not replaced.
{
source: sourceUrl,
target: getDownloadTarget("inprogress1.txt"),
tempPath: yield getPartialFile("inprogress1.txt.part", true),
startTime: getStartTime(1),
state: DOWNLOAD_PAUSED,
referrer: httpUrl("referrer1"),
entityID: sourceEntityId,
maxBytes: MAXBYTES_IN_DB,
mimeType: "mimeType1",
preferredAction: Ci.nsIMIMEInfo.saveToDisk,
preferredApplication: "prerredApplication1",
autoResume: 1,
// Even though the information stored in the DB said
// maxBytes was MAXBYTES_IN_DB, the download turned out to be
// a different length. Here we make sure the totalBytes property
// was correctly set with the actual value. The same consideration
// applies to the contentType.
expectedCurrentBytes: TEST_DATA_LENGTH,
expectedTotalBytes: TEST_DATA_LENGTH,
expectedResume: true,
expectedContentType: "text/plain",
expectedContent: TEST_DATA_TAINTED,
},
// Paused download with autoResume and a partial file,
// but missing entityID. This means that the download will
// start from beginning, and the entire original content of the
// source file should replace the different data that was stored
// in the partial file.
{
source: sourceUrl,
target: getDownloadTarget("inprogress2.txt"),
tempPath: yield getPartialFile("inprogress2.txt.part", true),
startTime: getStartTime(2),
state: DOWNLOAD_PAUSED,
referrer: httpUrl("referrer2"),
entityID: "",
maxBytes: MAXBYTES_IN_DB,
mimeType: "mimeType2",
preferredAction: Ci.nsIMIMEInfo.saveToDisk,
preferredApplication: "prerredApplication2",
autoResume: 1,
expectedCurrentBytes: TEST_DATA_LENGTH,
expectedTotalBytes: TEST_DATA_LENGTH,
expectedResume: true,
expectedContentType: "text/plain",
expectedContent: TEST_DATA_SHORT
},
// Paused download with no autoResume and a partial file.
{
source: sourceUrl,
target: getDownloadTarget("inprogress3.txt"),
tempPath: yield getPartialFile("inprogress3.txt.part"),
startTime: getStartTime(3),
state: DOWNLOAD_PAUSED,
referrer: httpUrl("referrer3"),
entityID: "",
maxBytes: MAXBYTES_IN_DB,
mimeType: "mimeType3",
preferredAction: Ci.nsIMIMEInfo.saveToDisk,
preferredApplication: "prerredApplication3",
autoResume: 0,
// Since this download has not been resumed, the actual data
// about its total size and content type is not known.
// Therefore, we're going by the information imported from the DB.
expectedCurrentBytes: TEST_DATA_PARTIAL_LENGTH,
expectedTotalBytes: MAXBYTES_IN_DB,
expectedResume: false,
expectedContentType: "mimeType3",
expectedContent: TEST_DATA_SHORT.substr(0, TEST_DATA_PARTIAL_LENGTH),
},
// Paused download with autoResume and no partial file.
{
source: sourceUrl,
target: getDownloadTarget("inprogress4.txt"),
tempPath: "",
startTime: getStartTime(4),
state: DOWNLOAD_PAUSED,
referrer: httpUrl("referrer4"),
entityID: "",
maxBytes: MAXBYTES_IN_DB,
mimeType: "text/plain",
preferredAction: Ci.nsIMIMEInfo.useHelperApp,
preferredApplication: "prerredApplication4",
autoResume: 1,
expectedCurrentBytes: TEST_DATA_LENGTH,
expectedTotalBytes: TEST_DATA_LENGTH,
expectedResume: true,
expectedContentType: "text/plain",
expectedContent: TEST_DATA_SHORT
},
// Paused download with no autoResume and no partial file.
{
source: sourceUrl,
target: getDownloadTarget("inprogress5.txt"),
tempPath: "",
startTime: getStartTime(5),
state: DOWNLOAD_PAUSED,
referrer: httpUrl("referrer4"),
entityID: "",
maxBytes: MAXBYTES_IN_DB,
mimeType: "text/plain",
preferredAction: Ci.nsIMIMEInfo.useSystemDefault,
preferredApplication: "prerredApplication5",
autoResume: 0,
expectedCurrentBytes: 0,
expectedTotalBytes: MAXBYTES_IN_DB,
expectedResume: false,
expectedContentType: "text/plain",
},
// Queued download with no autoResume and no partial file.
// Even though autoResume=0, queued downloads always autoResume.
{
source: sourceUrl,
target: getDownloadTarget("inprogress6.txt"),
tempPath: "",
startTime: getStartTime(6),
state: DOWNLOAD_QUEUED,
referrer: httpUrl("referrer6"),
entityID: "",
maxBytes: MAXBYTES_IN_DB,
mimeType: "text/plain",
preferredAction: Ci.nsIMIMEInfo.useHelperApp,
preferredApplication: "prerredApplication6",
autoResume: 0,
expectedCurrentBytes: TEST_DATA_LENGTH,
expectedTotalBytes: TEST_DATA_LENGTH,
expectedResume: true,
expectedContentType: "text/plain",
expectedContent: TEST_DATA_SHORT
},
// Notstarted download with no autoResume and no partial file.
// Even though autoResume=0, notstarted downloads always autoResume.
{
source: sourceUrl,
target: getDownloadTarget("inprogress7.txt"),
tempPath: "",
startTime: getStartTime(7),
state: DOWNLOAD_NOTSTARTED,
referrer: httpUrl("referrer7"),
entityID: "",
maxBytes: MAXBYTES_IN_DB,
mimeType: "text/plain",
preferredAction: Ci.nsIMIMEInfo.useHelperApp,
preferredApplication: "prerredApplication7",
autoResume: 0,
expectedCurrentBytes: TEST_DATA_LENGTH,
expectedTotalBytes: TEST_DATA_LENGTH,
expectedResume: true,
expectedContentType: "text/plain",
expectedContent: TEST_DATA_SHORT
},
// Downloading download with no autoResume and a partial file.
// Even though autoResume=0, downloading downloads always autoResume.
{
source: sourceUrl,
target: getDownloadTarget("inprogress8.txt"),
tempPath: yield getPartialFile("inprogress8.txt.part", true),
startTime: getStartTime(8),
state: DOWNLOAD_DOWNLOADING,
referrer: httpUrl("referrer8"),
entityID: sourceEntityId,
maxBytes: MAXBYTES_IN_DB,
mimeType: "text/plain",
preferredAction: Ci.nsIMIMEInfo.saveToDisk,
preferredApplication: "prerredApplication8",
autoResume: 0,
expectedCurrentBytes: TEST_DATA_LENGTH,
expectedTotalBytes: TEST_DATA_LENGTH,
expectedResume: true,
expectedContentType: "text/plain",
expectedContent: TEST_DATA_TAINTED
},
];
});
/**
* Prepares the list of downloads to be added to the database that should
* *not* be imported by the import procedure.
*/
add_task(function* prepareNonImportableDownloads()
{
gDownloadsRowNonImportable = [
// Download with no source (should never happen in normal circumstances).
{
source: "",
target: "nonimportable1.txt",
tempPath: "",
startTime: getStartTime(1),
state: DOWNLOAD_PAUSED,
referrer: "",
entityID: "",
maxBytes: MAXBYTES_IN_DB,
mimeType: "mimeType1",
preferredAction: Ci.nsIMIMEInfo.saveToDisk,
preferredApplication: "prerredApplication1",
autoResume: 1
},
// state = DOWNLOAD_FAILED
{
source: httpUrl("source.txt"),
target: "nonimportable2.txt",
tempPath: "",
startTime: getStartTime(2),
state: DOWNLOAD_FAILED,
referrer: "",
entityID: "",
maxBytes: MAXBYTES_IN_DB,
mimeType: "mimeType2",
preferredAction: Ci.nsIMIMEInfo.saveToDisk,
preferredApplication: "prerredApplication2",
autoResume: 1
},
// state = DOWNLOAD_CANCELED
{
source: httpUrl("source.txt"),
target: "nonimportable3.txt",
tempPath: "",
startTime: getStartTime(3),
state: DOWNLOAD_CANCELED,
referrer: "",
entityID: "",
maxBytes: MAXBYTES_IN_DB,
mimeType: "mimeType3",
preferredAction: Ci.nsIMIMEInfo.saveToDisk,
preferredApplication: "prerredApplication3",
autoResume: 1
},
// state = DOWNLOAD_BLOCKED_PARENTAL
{
source: httpUrl("source.txt"),
target: "nonimportable4.txt",
tempPath: "",
startTime: getStartTime(4),
state: DOWNLOAD_BLOCKED_PARENTAL,
referrer: "",
entityID: "",
maxBytes: MAXBYTES_IN_DB,
mimeType: "mimeType4",
preferredAction: Ci.nsIMIMEInfo.saveToDisk,
preferredApplication: "prerredApplication4",
autoResume: 1
},
// state = DOWNLOAD_SCANNING
{
source: httpUrl("source.txt"),
target: "nonimportable5.txt",
tempPath: "",
startTime: getStartTime(5),
state: DOWNLOAD_SCANNING,
referrer: "",
entityID: "",
maxBytes: MAXBYTES_IN_DB,
mimeType: "mimeType5",
preferredAction: Ci.nsIMIMEInfo.saveToDisk,
preferredApplication: "prerredApplication5",
autoResume: 1
},
// state = DOWNLOAD_DIRTY
{
source: httpUrl("source.txt"),
target: "nonimportable6.txt",
tempPath: "",
startTime: getStartTime(6),
state: DOWNLOAD_DIRTY,
referrer: "",
entityID: "",
maxBytes: MAXBYTES_IN_DB,
mimeType: "mimeType6",
preferredAction: Ci.nsIMIMEInfo.saveToDisk,
preferredApplication: "prerredApplication6",
autoResume: 1
},
// state = DOWNLOAD_BLOCKED_POLICY
{
source: httpUrl("source.txt"),
target: "nonimportable7.txt",
tempPath: "",
startTime: getStartTime(7),
state: DOWNLOAD_BLOCKED_POLICY,
referrer: "",
entityID: "",
maxBytes: MAXBYTES_IN_DB,
mimeType: "mimeType7",
preferredAction: Ci.nsIMIMEInfo.saveToDisk,
preferredApplication: "prerredApplication7",
autoResume: 1
},
];
});
// Test
/**
* Creates a temporary Sqlite database with download data and perform an
* import of that data to the new Downloads API to verify that the import
* worked correctly.
*/
add_task(function* test_downloadImport()
{
let connection = null;
let downloadsSqlite = getTempFile("downloads.sqlite").path;
try {
// Set up the database.
connection = yield promiseEmptyDatabaseConnection({
aPath: downloadsSqlite,
aSchemaVersion: 9
});
// Insert both the importable and non-importable
// downloads together.
for (let downloadRow of gDownloadsRowToImport) {
yield promiseInsertRow(connection, downloadRow);
}
for (let downloadRow of gDownloadsRowNonImportable) {
yield promiseInsertRow(connection, downloadRow);
}
// Check that every item was inserted.
do_check_eq((yield promiseTableCount(connection)),
gDownloadsRowToImport.length +
gDownloadsRowNonImportable.length);
} finally {
// Close the connection so that DownloadImport can open it.
yield connection.close();
}
// Import items.
let list = yield promiseNewList(false);
yield new DownloadImport(list, downloadsSqlite).import();
let items = yield list.getAll();
do_check_eq(items.length, gDownloadsRowToImport.length);
for (let i = 0; i < gDownloadsRowToImport.length; i++) {
yield checkDownload(items[i], gDownloadsRowToImport[i]);
}
})

View file

@ -0,0 +1,432 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests the DownloadIntegration object.
*/
"use strict";
// Globals
/**
* Notifies the prompt observers and verify the expected downloads count.
*
* @param aIsPrivate
* Flag to know is test private observers.
* @param aExpectedCount
* the expected downloads count for quit and offline observers.
* @param aExpectedPBCount
* the expected downloads count for private browsing observer.
*/
function notifyPromptObservers(aIsPrivate, aExpectedCount, aExpectedPBCount) {
let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].
createInstance(Ci.nsISupportsPRBool);
// Notify quit application requested observer.
DownloadIntegration._testPromptDownloads = -1;
Services.obs.notifyObservers(cancelQuit, "quit-application-requested", null);
do_check_eq(DownloadIntegration._testPromptDownloads, aExpectedCount);
// Notify offline requested observer.
DownloadIntegration._testPromptDownloads = -1;
Services.obs.notifyObservers(cancelQuit, "offline-requested", null);
do_check_eq(DownloadIntegration._testPromptDownloads, aExpectedCount);
if (aIsPrivate) {
// Notify last private browsing requested observer.
DownloadIntegration._testPromptDownloads = -1;
Services.obs.notifyObservers(cancelQuit, "last-pb-context-exiting", null);
do_check_eq(DownloadIntegration._testPromptDownloads, aExpectedPBCount);
}
delete DownloadIntegration._testPromptDownloads;
}
// Tests
/**
* Allows re-enabling the real download directory logic during one test.
*/
function allowDirectoriesInTest() {
DownloadIntegration.allowDirectories = true;
function cleanup() {
DownloadIntegration.allowDirectories = false;
}
do_register_cleanup(cleanup);
return cleanup;
}
XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() {
return Services.strings.
createBundle("chrome://mozapps/locale/downloads/downloads.properties");
});
/**
* Tests that getSystemDownloadsDirectory returns an existing directory or
* creates a new directory depending on the platform. Instead of the real
* directory, this test is executed in the temporary directory so we can safely
* delete the created folder to check whether it is created again.
*/
add_task(function* test_getSystemDownloadsDirectory_exists_or_creates()
{
let tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
let downloadDir;
// OSX / Linux / Windows but not XP/2k
if (Services.appinfo.OS == "Darwin" ||
Services.appinfo.OS == "Linux" ||
(Services.appinfo.OS == "WINNT" &&
parseFloat(Services.sysinfo.getProperty("version")) >= 6)) {
downloadDir = yield DownloadIntegration.getSystemDownloadsDirectory();
do_check_eq(downloadDir, tempDir.path);
do_check_true(yield OS.File.exists(downloadDir));
let info = yield OS.File.stat(downloadDir);
do_check_true(info.isDir);
} else {
let targetPath = OS.Path.join(tempDir.path,
gStringBundle.GetStringFromName("downloadsFolder"));
try {
yield OS.File.removeEmptyDir(targetPath);
} catch (e) {}
downloadDir = yield DownloadIntegration.getSystemDownloadsDirectory();
do_check_eq(downloadDir, targetPath);
do_check_true(yield OS.File.exists(downloadDir));
let info = yield OS.File.stat(downloadDir);
do_check_true(info.isDir);
yield OS.File.removeEmptyDir(targetPath);
}
});
/**
* Tests that the real directory returned by getSystemDownloadsDirectory is not
* the one that is used during unit tests. Since this is the actual downloads
* directory of the operating system, we don't try to delete it afterwards.
*/
add_task(function* test_getSystemDownloadsDirectory_real()
{
let fakeDownloadDir = yield DownloadIntegration.getSystemDownloadsDirectory();
let cleanup = allowDirectoriesInTest();
let realDownloadDir = yield DownloadIntegration.getSystemDownloadsDirectory();
cleanup();
do_check_neq(fakeDownloadDir, realDownloadDir);
});
/**
* Tests that the getPreferredDownloadsDirectory returns a valid download
* directory string path.
*/
add_task(function* test_getPreferredDownloadsDirectory()
{
let cleanupDirectories = allowDirectoriesInTest();
let folderListPrefName = "browser.download.folderList";
let dirPrefName = "browser.download.dir";
function cleanupPrefs() {
Services.prefs.clearUserPref(folderListPrefName);
Services.prefs.clearUserPref(dirPrefName);
}
do_register_cleanup(cleanupPrefs);
// Should return the system downloads directory.
Services.prefs.setIntPref(folderListPrefName, 1);
let systemDir = yield DownloadIntegration.getSystemDownloadsDirectory();
let downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
do_check_neq(downloadDir, "");
do_check_eq(downloadDir, systemDir);
// Should return the desktop directory.
Services.prefs.setIntPref(folderListPrefName, 0);
downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
do_check_neq(downloadDir, "");
do_check_eq(downloadDir, Services.dirsvc.get("Desk", Ci.nsIFile).path);
// Should return the system downloads directory because the dir preference
// is not set.
Services.prefs.setIntPref(folderListPrefName, 2);
downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
do_check_neq(downloadDir, "");
do_check_eq(downloadDir, systemDir);
// Should return the directory which is listed in the dir preference.
let time = (new Date()).getTime();
let tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
tempDir.append(time);
Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, tempDir);
downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
do_check_neq(downloadDir, "");
do_check_eq(downloadDir, tempDir.path);
do_check_true(yield OS.File.exists(downloadDir));
yield OS.File.removeEmptyDir(tempDir.path);
// Should return the system downloads directory beacause the path is invalid
// in the dir preference.
tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
tempDir.append("dir_not_exist");
tempDir.append(time);
Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, tempDir);
downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
do_check_eq(downloadDir, systemDir);
// Should return the system downloads directory because the folderList
// preference is invalid
Services.prefs.setIntPref(folderListPrefName, 999);
downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
do_check_eq(downloadDir, systemDir);
cleanupPrefs();
cleanupDirectories();
});
/**
* Tests that the getTemporaryDownloadsDirectory returns a valid download
* directory string path.
*/
add_task(function* test_getTemporaryDownloadsDirectory()
{
let cleanup = allowDirectoriesInTest();
let downloadDir = yield DownloadIntegration.getTemporaryDownloadsDirectory();
do_check_neq(downloadDir, "");
if ("nsILocalFileMac" in Ci) {
let preferredDownloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
do_check_eq(downloadDir, preferredDownloadDir);
} else {
let tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
do_check_eq(downloadDir, tempDir.path);
}
cleanup();
});
// Tests DownloadObserver
/**
* Re-enables the default observers for the following tests.
*
* This takes effect the first time a DownloadList object is created, and lasts
* until this test file has completed.
*/
add_task(function* test_observers_setup()
{
DownloadIntegration.allowObservers = true;
do_register_cleanup(function () {
DownloadIntegration.allowObservers = false;
});
});
/**
* Tests notifications prompts when observers are notified if there are public
* and private active downloads.
*/
add_task(function* test_notifications()
{
for (let isPrivate of [false, true]) {
mustInterruptResponses();
let list = yield promiseNewList(isPrivate);
let download1 = yield promiseNewDownload(httpUrl("interruptible.txt"));
let download2 = yield promiseNewDownload(httpUrl("interruptible.txt"));
let download3 = yield promiseNewDownload(httpUrl("interruptible.txt"));
let promiseAttempt1 = download1.start();
let promiseAttempt2 = download2.start();
download3.start().catch(() => {});
// Add downloads to list.
yield list.add(download1);
yield list.add(download2);
yield list.add(download3);
// Cancel third download
yield download3.cancel();
notifyPromptObservers(isPrivate, 2, 2);
// Allow the downloads to complete.
continueResponses();
yield promiseAttempt1;
yield promiseAttempt2;
// Clean up.
yield list.remove(download1);
yield list.remove(download2);
yield list.remove(download3);
}
});
/**
* Tests that notifications prompts observers are not notified if there are no
* public or private active downloads.
*/
add_task(function* test_no_notifications()
{
for (let isPrivate of [false, true]) {
let list = yield promiseNewList(isPrivate);
let download1 = yield promiseNewDownload(httpUrl("interruptible.txt"));
let download2 = yield promiseNewDownload(httpUrl("interruptible.txt"));
download1.start().catch(() => {});
download2.start().catch(() => {});
// Add downloads to list.
yield list.add(download1);
yield list.add(download2);
yield download1.cancel();
yield download2.cancel();
notifyPromptObservers(isPrivate, 0, 0);
// Clean up.
yield list.remove(download1);
yield list.remove(download2);
}
});
/**
* Tests notifications prompts when observers are notified if there are public
* and private active downloads at the same time.
*/
add_task(function* test_mix_notifications()
{
mustInterruptResponses();
let publicList = yield promiseNewList();
let privateList = yield Downloads.getList(Downloads.PRIVATE);
let download1 = yield promiseNewDownload(httpUrl("interruptible.txt"));
let download2 = yield promiseNewDownload(httpUrl("interruptible.txt"));
let promiseAttempt1 = download1.start();
let promiseAttempt2 = download2.start();
// Add downloads to lists.
yield publicList.add(download1);
yield privateList.add(download2);
notifyPromptObservers(true, 2, 1);
// Allow the downloads to complete.
continueResponses();
yield promiseAttempt1;
yield promiseAttempt2;
// Clean up.
yield publicList.remove(download1);
yield privateList.remove(download2);
});
/**
* Tests suspending and resuming as well as going offline and then online again.
* The downloads should stop when suspending and start again when resuming.
*/
add_task(function* test_suspend_resume()
{
// The default wake delay is 10 seconds, so set the wake delay to be much
// faster for these tests.
Services.prefs.setIntPref("browser.download.manager.resumeOnWakeDelay", 5);
let addDownload = function(list)
{
return Task.spawn(function* () {
let download = yield promiseNewDownload(httpUrl("interruptible.txt"));
download.start().catch(() => {});
list.add(download);
return download;
});
}
let publicList = yield promiseNewList();
let privateList = yield promiseNewList(true);
let download1 = yield addDownload(publicList);
let download2 = yield addDownload(publicList);
let download3 = yield addDownload(privateList);
let download4 = yield addDownload(privateList);
let download5 = yield addDownload(publicList);
// First, check that the downloads are all canceled when going to sleep.
Services.obs.notifyObservers(null, "sleep_notification", null);
do_check_true(download1.canceled);
do_check_true(download2.canceled);
do_check_true(download3.canceled);
do_check_true(download4.canceled);
do_check_true(download5.canceled);
// Remove a download. It should not be started again.
publicList.remove(download5);
do_check_true(download5.canceled);
// When waking up again, the downloads start again after the wake delay. To be
// more robust, don't check after a delay but instead just wait for the
// downloads to finish.
Services.obs.notifyObservers(null, "wake_notification", null);
yield download1.whenSucceeded();
yield download2.whenSucceeded();
yield download3.whenSucceeded();
yield download4.whenSucceeded();
// Downloads should no longer be canceled. However, as download5 was removed
// from the public list, it will not be restarted.
do_check_false(download1.canceled);
do_check_true(download5.canceled);
// Create four new downloads and check for going offline and then online again.
download1 = yield addDownload(publicList);
download2 = yield addDownload(publicList);
download3 = yield addDownload(privateList);
download4 = yield addDownload(privateList);
// Going offline should cancel the downloads.
Services.obs.notifyObservers(null, "network:offline-about-to-go-offline", null);
do_check_true(download1.canceled);
do_check_true(download2.canceled);
do_check_true(download3.canceled);
do_check_true(download4.canceled);
// Going back online should start the downloads again.
Services.obs.notifyObservers(null, "network:offline-status-changed", "online");
yield download1.whenSucceeded();
yield download2.whenSucceeded();
yield download3.whenSucceeded();
yield download4.whenSucceeded();
Services.prefs.clearUserPref("browser.download.manager.resumeOnWakeDelay");
});
/**
* Tests both the downloads list and the in-progress downloads are clear when
* private browsing observer is notified.
*/
add_task(function* test_exit_private_browsing()
{
mustInterruptResponses();
let privateList = yield promiseNewList(true);
let download1 = yield promiseNewDownload(httpUrl("source.txt"));
let download2 = yield promiseNewDownload(httpUrl("interruptible.txt"));
let promiseAttempt1 = download1.start();
download2.start();
// Add downloads to list.
yield privateList.add(download1);
yield privateList.add(download2);
// Complete the download.
yield promiseAttempt1;
do_check_eq((yield privateList.getAll()).length, 2);
// Simulate exiting the private browsing.
yield new Promise(resolve => {
DownloadIntegration._testResolveClearPrivateList = resolve;
Services.obs.notifyObservers(null, "last-pb-context-exited", null);
});
delete DownloadIntegration._testResolveClearPrivateList;
do_check_eq((yield privateList.getAll()).length, 0);
continueResponses();
});

View file

@ -0,0 +1,17 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests the integration with legacy interfaces for downloads.
*/
"use strict";
// Execution of common tests
var gUseLegacySaver = true;
var scriptFile = do_get_file("common_test_Download.js");
Services.scriptloader.loadSubScript(NetUtil.newURI(scriptFile).spec);

View file

@ -0,0 +1,564 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests the DownloadList object.
*/
"use strict";
// Globals
/**
* Returns a PRTime in the past usable to add expirable visits.
*
* @note Expiration ignores any visit added in the last 7 days, but it's
* better be safe against DST issues, by going back one day more.
*/
function getExpirablePRTime()
{
let dateObj = new Date();
// Normalize to midnight
dateObj.setHours(0);
dateObj.setMinutes(0);
dateObj.setSeconds(0);
dateObj.setMilliseconds(0);
dateObj = new Date(dateObj.getTime() - 8 * 86400000);
return dateObj.getTime() * 1000;
}
/**
* Adds an expirable history visit for a download.
*
* @param aSourceUrl
* String containing the URI for the download source, or null to use
* httpUrl("source.txt").
*
* @return {Promise}
* @rejects JavaScript exception.
*/
function promiseExpirableDownloadVisit(aSourceUrl)
{
let deferred = Promise.defer();
PlacesUtils.asyncHistory.updatePlaces(
{
uri: NetUtil.newURI(aSourceUrl || httpUrl("source.txt")),
visits: [{
transitionType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
visitDate: getExpirablePRTime(),
}]
},
{
handleError: function handleError(aResultCode, aPlaceInfo) {
let ex = new Components.Exception("Unexpected error in adding visits.",
aResultCode);
deferred.reject(ex);
},
handleResult: function () {},
handleCompletion: function handleCompletion() {
deferred.resolve();
}
});
return deferred.promise;
}
// Tests
/**
* Checks the testing mechanism used to build different download lists.
*/
add_task(function* test_construction()
{
let downloadListOne = yield promiseNewList();
let downloadListTwo = yield promiseNewList();
let privateDownloadListOne = yield promiseNewList(true);
let privateDownloadListTwo = yield promiseNewList(true);
do_check_neq(downloadListOne, downloadListTwo);
do_check_neq(privateDownloadListOne, privateDownloadListTwo);
do_check_neq(downloadListOne, privateDownloadListOne);
});
/**
* Checks the methods to add and retrieve items from the list.
*/
add_task(function* test_add_getAll()
{
let list = yield promiseNewList();
let downloadOne = yield promiseNewDownload();
yield list.add(downloadOne);
let itemsOne = yield list.getAll();
do_check_eq(itemsOne.length, 1);
do_check_eq(itemsOne[0], downloadOne);
let downloadTwo = yield promiseNewDownload();
yield list.add(downloadTwo);
let itemsTwo = yield list.getAll();
do_check_eq(itemsTwo.length, 2);
do_check_eq(itemsTwo[0], downloadOne);
do_check_eq(itemsTwo[1], downloadTwo);
// The first snapshot should not have been modified.
do_check_eq(itemsOne.length, 1);
});
/**
* Checks the method to remove items from the list.
*/
add_task(function* test_remove()
{
let list = yield promiseNewList();
yield list.add(yield promiseNewDownload());
yield list.add(yield promiseNewDownload());
let items = yield list.getAll();
yield list.remove(items[0]);
// Removing an item that was never added should not raise an error.
yield list.remove(yield promiseNewDownload());
items = yield list.getAll();
do_check_eq(items.length, 1);
});
/**
* Tests that the "add", "remove", and "getAll" methods on the global
* DownloadCombinedList object combine the contents of the global DownloadList
* objects for public and private downloads.
*/
add_task(function* test_DownloadCombinedList_add_remove_getAll()
{
let publicList = yield promiseNewList();
let privateList = yield Downloads.getList(Downloads.PRIVATE);
let combinedList = yield Downloads.getList(Downloads.ALL);
let publicDownload = yield promiseNewDownload();
let privateDownload = yield Downloads.createDownload({
source: { url: httpUrl("source.txt"), isPrivate: true },
target: getTempFile(TEST_TARGET_FILE_NAME).path,
});
yield publicList.add(publicDownload);
yield privateList.add(privateDownload);
do_check_eq((yield combinedList.getAll()).length, 2);
yield combinedList.remove(publicDownload);
yield combinedList.remove(privateDownload);
do_check_eq((yield combinedList.getAll()).length, 0);
yield combinedList.add(publicDownload);
yield combinedList.add(privateDownload);
do_check_eq((yield publicList.getAll()).length, 1);
do_check_eq((yield privateList.getAll()).length, 1);
do_check_eq((yield combinedList.getAll()).length, 2);
yield publicList.remove(publicDownload);
yield privateList.remove(privateDownload);
do_check_eq((yield combinedList.getAll()).length, 0);
});
/**
* Checks that views receive the download add and remove notifications, and that
* adding and removing views works as expected, both for a normal and a combined
* list.
*/
add_task(function* test_notifications_add_remove()
{
for (let isCombined of [false, true]) {
// Force creating a new list for both the public and combined cases.
let list = yield promiseNewList();
if (isCombined) {
list = yield Downloads.getList(Downloads.ALL);
}
let downloadOne = yield promiseNewDownload();
let downloadTwo = yield Downloads.createDownload({
source: { url: httpUrl("source.txt"), isPrivate: true },
target: getTempFile(TEST_TARGET_FILE_NAME).path,
});
yield list.add(downloadOne);
yield list.add(downloadTwo);
// Check that we receive add notifications for existing elements.
let addNotifications = 0;
let viewOne = {
onDownloadAdded: function (aDownload) {
// The first download to be notified should be the first that was added.
if (addNotifications == 0) {
do_check_eq(aDownload, downloadOne);
} else if (addNotifications == 1) {
do_check_eq(aDownload, downloadTwo);
}
addNotifications++;
},
};
yield list.addView(viewOne);
do_check_eq(addNotifications, 2);
// Check that we receive add notifications for new elements.
yield list.add(yield promiseNewDownload());
do_check_eq(addNotifications, 3);
// Check that we receive remove notifications.
let removeNotifications = 0;
let viewTwo = {
onDownloadRemoved: function (aDownload) {
do_check_eq(aDownload, downloadOne);
removeNotifications++;
},
};
yield list.addView(viewTwo);
yield list.remove(downloadOne);
do_check_eq(removeNotifications, 1);
// We should not receive remove notifications after the view is removed.
yield list.removeView(viewTwo);
yield list.remove(downloadTwo);
do_check_eq(removeNotifications, 1);
// We should not receive add notifications after the view is removed.
yield list.removeView(viewOne);
yield list.add(yield promiseNewDownload());
do_check_eq(addNotifications, 3);
}
});
/**
* Checks that views receive the download change notifications, both for a
* normal and a combined list.
*/
add_task(function* test_notifications_change()
{
for (let isCombined of [false, true]) {
// Force creating a new list for both the public and combined cases.
let list = yield promiseNewList();
if (isCombined) {
list = yield Downloads.getList(Downloads.ALL);
}
let downloadOne = yield promiseNewDownload();
let downloadTwo = yield Downloads.createDownload({
source: { url: httpUrl("source.txt"), isPrivate: true },
target: getTempFile(TEST_TARGET_FILE_NAME).path,
});
yield list.add(downloadOne);
yield list.add(downloadTwo);
// Check that we receive change notifications.
let receivedOnDownloadChanged = false;
yield list.addView({
onDownloadChanged: function (aDownload) {
do_check_eq(aDownload, downloadOne);
receivedOnDownloadChanged = true;
},
});
yield downloadOne.start();
do_check_true(receivedOnDownloadChanged);
// We should not receive change notifications after a download is removed.
receivedOnDownloadChanged = false;
yield list.remove(downloadTwo);
yield downloadTwo.start();
do_check_false(receivedOnDownloadChanged);
}
});
/**
* Checks that the reference to "this" is correct in the view callbacks.
*/
add_task(function* test_notifications_this()
{
let list = yield promiseNewList();
// Check that we receive change notifications.
let receivedOnDownloadAdded = false;
let receivedOnDownloadChanged = false;
let receivedOnDownloadRemoved = false;
let view = {
onDownloadAdded: function () {
do_check_eq(this, view);
receivedOnDownloadAdded = true;
},
onDownloadChanged: function () {
// Only do this check once.
if (!receivedOnDownloadChanged) {
do_check_eq(this, view);
receivedOnDownloadChanged = true;
}
},
onDownloadRemoved: function () {
do_check_eq(this, view);
receivedOnDownloadRemoved = true;
},
};
yield list.addView(view);
let download = yield promiseNewDownload();
yield list.add(download);
yield download.start();
yield list.remove(download);
// Verify that we executed the checks.
do_check_true(receivedOnDownloadAdded);
do_check_true(receivedOnDownloadChanged);
do_check_true(receivedOnDownloadRemoved);
});
/**
* Checks that download is removed on history expiration.
*/
add_task(function* test_history_expiration()
{
mustInterruptResponses();
function cleanup() {
Services.prefs.clearUserPref("places.history.expiration.max_pages");
}
do_register_cleanup(cleanup);
// Set max pages to 0 to make the download expire.
Services.prefs.setIntPref("places.history.expiration.max_pages", 0);
let list = yield promiseNewList();
let downloadOne = yield promiseNewDownload();
let downloadTwo = yield promiseNewDownload(httpUrl("interruptible.txt"));
let deferred = Promise.defer();
let removeNotifications = 0;
let downloadView = {
onDownloadRemoved: function (aDownload) {
if (++removeNotifications == 2) {
deferred.resolve();
}
},
};
yield list.addView(downloadView);
// Work with one finished download and one canceled download.
yield downloadOne.start();
downloadTwo.start().catch(() => {});
yield downloadTwo.cancel();
// We must replace the visits added while executing the downloads with visits
// that are older than 7 days, otherwise they will not be expired.
yield PlacesTestUtils.clearHistory();
yield promiseExpirableDownloadVisit();
yield promiseExpirableDownloadVisit(httpUrl("interruptible.txt"));
// After clearing history, we can add the downloads to be removed to the list.
yield list.add(downloadOne);
yield list.add(downloadTwo);
// Force a history expiration.
Cc["@mozilla.org/places/expiration;1"]
.getService(Ci.nsIObserver).observe(null, "places-debug-start-expiration", -1);
// Wait for both downloads to be removed.
yield deferred.promise;
cleanup();
});
/**
* Checks all downloads are removed after clearing history.
*/
add_task(function* test_history_clear()
{
let list = yield promiseNewList();
let downloadOne = yield promiseNewDownload();
let downloadTwo = yield promiseNewDownload();
yield list.add(downloadOne);
yield list.add(downloadTwo);
let deferred = Promise.defer();
let removeNotifications = 0;
let downloadView = {
onDownloadRemoved: function (aDownload) {
if (++removeNotifications == 2) {
deferred.resolve();
}
},
};
yield list.addView(downloadView);
yield downloadOne.start();
yield downloadTwo.start();
yield PlacesTestUtils.clearHistory();
// Wait for the removal notifications that may still be pending.
yield deferred.promise;
});
/**
* Tests the removeFinished method to ensure that it only removes
* finished downloads.
*/
add_task(function* test_removeFinished()
{
let list = yield promiseNewList();
let downloadOne = yield promiseNewDownload();
let downloadTwo = yield promiseNewDownload();
let downloadThree = yield promiseNewDownload();
let downloadFour = yield promiseNewDownload();
yield list.add(downloadOne);
yield list.add(downloadTwo);
yield list.add(downloadThree);
yield list.add(downloadFour);
let deferred = Promise.defer();
let removeNotifications = 0;
let downloadView = {
onDownloadRemoved: function (aDownload) {
do_check_true(aDownload == downloadOne ||
aDownload == downloadTwo ||
aDownload == downloadThree);
do_check_true(removeNotifications < 3);
if (++removeNotifications == 3) {
deferred.resolve();
}
},
};
yield list.addView(downloadView);
// Start three of the downloads, but don't start downloadTwo, then set
// downloadFour to have partial data. All downloads except downloadFour
// should be removed.
yield downloadOne.start();
yield downloadThree.start();
yield downloadFour.start();
downloadFour.hasPartialData = true;
list.removeFinished();
yield deferred.promise;
let downloads = yield list.getAll()
do_check_eq(downloads.length, 1);
});
/**
* Tests the global DownloadSummary objects for the public, private, and
* combined download lists.
*/
add_task(function* test_DownloadSummary()
{
mustInterruptResponses();
let publicList = yield promiseNewList();
let privateList = yield Downloads.getList(Downloads.PRIVATE);
let publicSummary = yield Downloads.getSummary(Downloads.PUBLIC);
let privateSummary = yield Downloads.getSummary(Downloads.PRIVATE);
let combinedSummary = yield Downloads.getSummary(Downloads.ALL);
// Add a public download that has succeeded.
let succeededPublicDownload = yield promiseNewDownload();
yield succeededPublicDownload.start();
yield publicList.add(succeededPublicDownload);
// Add a public download that has been canceled midway.
let canceledPublicDownload =
yield promiseNewDownload(httpUrl("interruptible.txt"));
canceledPublicDownload.start().catch(() => {});
yield promiseDownloadMidway(canceledPublicDownload);
yield canceledPublicDownload.cancel();
yield publicList.add(canceledPublicDownload);
// Add a public download that is in progress.
let inProgressPublicDownload =
yield promiseNewDownload(httpUrl("interruptible.txt"));
inProgressPublicDownload.start().catch(() => {});
yield promiseDownloadMidway(inProgressPublicDownload);
yield publicList.add(inProgressPublicDownload);
// Add a private download that is in progress.
let inProgressPrivateDownload = yield Downloads.createDownload({
source: { url: httpUrl("interruptible.txt"), isPrivate: true },
target: getTempFile(TEST_TARGET_FILE_NAME).path,
});
inProgressPrivateDownload.start().catch(() => {});
yield promiseDownloadMidway(inProgressPrivateDownload);
yield privateList.add(inProgressPrivateDownload);
// Verify that the summary includes the total number of bytes and the
// currently transferred bytes only for the downloads that are not stopped.
// For simplicity, we assume that after a download is added to the list, its
// current state is immediately propagated to the summary object, which is
// true in the current implementation, though it is not guaranteed as all the
// download operations may happen asynchronously.
do_check_false(publicSummary.allHaveStopped);
do_check_eq(publicSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2);
do_check_eq(publicSummary.progressCurrentBytes, TEST_DATA_SHORT.length);
do_check_false(privateSummary.allHaveStopped);
do_check_eq(privateSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2);
do_check_eq(privateSummary.progressCurrentBytes, TEST_DATA_SHORT.length);
do_check_false(combinedSummary.allHaveStopped);
do_check_eq(combinedSummary.progressTotalBytes, TEST_DATA_SHORT.length * 4);
do_check_eq(combinedSummary.progressCurrentBytes, TEST_DATA_SHORT.length * 2);
yield inProgressPublicDownload.cancel();
// Stopping the download should have excluded it from the summary.
do_check_true(publicSummary.allHaveStopped);
do_check_eq(publicSummary.progressTotalBytes, 0);
do_check_eq(publicSummary.progressCurrentBytes, 0);
do_check_false(privateSummary.allHaveStopped);
do_check_eq(privateSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2);
do_check_eq(privateSummary.progressCurrentBytes, TEST_DATA_SHORT.length);
do_check_false(combinedSummary.allHaveStopped);
do_check_eq(combinedSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2);
do_check_eq(combinedSummary.progressCurrentBytes, TEST_DATA_SHORT.length);
yield inProgressPrivateDownload.cancel();
// All the downloads should be stopped now.
do_check_true(publicSummary.allHaveStopped);
do_check_eq(publicSummary.progressTotalBytes, 0);
do_check_eq(publicSummary.progressCurrentBytes, 0);
do_check_true(privateSummary.allHaveStopped);
do_check_eq(privateSummary.progressTotalBytes, 0);
do_check_eq(privateSummary.progressCurrentBytes, 0);
do_check_true(combinedSummary.allHaveStopped);
do_check_eq(combinedSummary.progressTotalBytes, 0);
do_check_eq(combinedSummary.progressCurrentBytes, 0);
});
/**
* Checks that views receive the summary change notification. This is tested on
* the combined summary when adding a public download, as we assume that if we
* pass the test in this case we will also pass it in the others.
*/
add_task(function* test_DownloadSummary_notifications()
{
let list = yield promiseNewList();
let summary = yield Downloads.getSummary(Downloads.ALL);
let download = yield promiseNewDownload();
yield list.add(download);
// Check that we receive change notifications.
let receivedOnSummaryChanged = false;
yield summary.addView({
onSummaryChanged: function () {
receivedOnSummaryChanged = true;
},
});
yield download.start();
do_check_true(receivedOnSummaryChanged);
});

View file

@ -0,0 +1,315 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests the DownloadStore object.
*/
"use strict";
// Globals
XPCOMUtils.defineLazyModuleGetter(this, "DownloadStore",
"resource://gre/modules/DownloadStore.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm")
/**
* Returns a new DownloadList object with an associated DownloadStore.
*
* @param aStorePath
* String pointing to the file to be associated with the DownloadStore,
* or undefined to use a non-existing temporary file. In this case, the
* temporary file is deleted when the test file execution finishes.
*
* @return {Promise}
* @resolves Array [ Newly created DownloadList , associated DownloadStore ].
* @rejects JavaScript exception.
*/
function promiseNewListAndStore(aStorePath)
{
return promiseNewList().then(function (aList) {
let path = aStorePath || getTempFile(TEST_STORE_FILE_NAME).path;
let store = new DownloadStore(aList, path);
return [aList, store];
});
}
// Tests
/**
* Saves downloads to a file, then reloads them.
*/
add_task(function* test_save_reload()
{
let [listForSave, storeForSave] = yield promiseNewListAndStore();
let [listForLoad, storeForLoad] = yield promiseNewListAndStore(
storeForSave.path);
listForSave.add(yield promiseNewDownload(httpUrl("source.txt")));
listForSave.add(yield Downloads.createDownload({
source: { url: httpUrl("empty.txt"),
referrer: TEST_REFERRER_URL },
target: getTempFile(TEST_TARGET_FILE_NAME),
}));
// This PDF download should not be serialized because it never succeeds.
let pdfDownload = yield Downloads.createDownload({
source: { url: httpUrl("empty.txt"),
referrer: TEST_REFERRER_URL },
target: getTempFile(TEST_TARGET_FILE_NAME),
saver: "pdf",
});
listForSave.add(pdfDownload);
// If we used a callback to adjust the channel, the download should
// not be serialized because we can't recreate it across sessions.
let adjustedDownload = yield Downloads.createDownload({
source: { url: httpUrl("empty.txt"),
adjustChannel: () => Promise.resolve() },
target: getTempFile(TEST_TARGET_FILE_NAME),
});
listForSave.add(adjustedDownload);
let legacyDownload = yield promiseStartLegacyDownload();
yield legacyDownload.cancel();
listForSave.add(legacyDownload);
yield storeForSave.save();
yield storeForLoad.load();
// Remove the PDF and adjusted downloads because they should not appear here.
listForSave.remove(adjustedDownload);
listForSave.remove(pdfDownload);
let itemsForSave = yield listForSave.getAll();
let itemsForLoad = yield listForLoad.getAll();
do_check_eq(itemsForSave.length, itemsForLoad.length);
// Downloads should be reloaded in the same order.
for (let i = 0; i < itemsForSave.length; i++) {
// The reloaded downloads are different objects.
do_check_neq(itemsForSave[i], itemsForLoad[i]);
// The reloaded downloads have the same properties.
do_check_eq(itemsForSave[i].source.url,
itemsForLoad[i].source.url);
do_check_eq(itemsForSave[i].source.referrer,
itemsForLoad[i].source.referrer);
do_check_eq(itemsForSave[i].target.path,
itemsForLoad[i].target.path);
do_check_eq(itemsForSave[i].saver.toSerializable(),
itemsForLoad[i].saver.toSerializable());
}
});
/**
* Checks that saving an empty list deletes any existing file.
*/
add_task(function* test_save_empty()
{
let [, store] = yield promiseNewListAndStore();
let createdFile = yield OS.File.open(store.path, { create: true });
yield createdFile.close();
yield store.save();
do_check_false(yield OS.File.exists(store.path));
// If the file does not exist, saving should not generate exceptions.
yield store.save();
});
/**
* Checks that loading from a missing file results in an empty list.
*/
add_task(function* test_load_empty()
{
let [list, store] = yield promiseNewListAndStore();
do_check_false(yield OS.File.exists(store.path));
yield store.load();
let items = yield list.getAll();
do_check_eq(items.length, 0);
});
/**
* Loads downloads from a string in a predefined format. The purpose of this
* test is to verify that the JSON format used in previous versions can be
* loaded, assuming the file is reloaded on the same platform.
*/
add_task(function* test_load_string_predefined()
{
let [list, store] = yield promiseNewListAndStore();
// The platform-dependent file name should be generated dynamically.
let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
let filePathLiteral = JSON.stringify(targetPath);
let sourceUriLiteral = JSON.stringify(httpUrl("source.txt"));
let emptyUriLiteral = JSON.stringify(httpUrl("empty.txt"));
let referrerUriLiteral = JSON.stringify(TEST_REFERRER_URL);
let string = "{\"list\":[{\"source\":" + sourceUriLiteral + "," +
"\"target\":" + filePathLiteral + "}," +
"{\"source\":{\"url\":" + emptyUriLiteral + "," +
"\"referrer\":" + referrerUriLiteral + "}," +
"\"target\":" + filePathLiteral + "}]}";
yield OS.File.writeAtomic(store.path,
new TextEncoder().encode(string),
{ tmpPath: store.path + ".tmp" });
yield store.load();
let items = yield list.getAll();
do_check_eq(items.length, 2);
do_check_eq(items[0].source.url, httpUrl("source.txt"));
do_check_eq(items[0].target.path, targetPath);
do_check_eq(items[1].source.url, httpUrl("empty.txt"));
do_check_eq(items[1].source.referrer, TEST_REFERRER_URL);
do_check_eq(items[1].target.path, targetPath);
});
/**
* Loads downloads from a well-formed JSON string containing unrecognized data.
*/
add_task(function* test_load_string_unrecognized()
{
let [list, store] = yield promiseNewListAndStore();
// The platform-dependent file name should be generated dynamically.
let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
let filePathLiteral = JSON.stringify(targetPath);
let sourceUriLiteral = JSON.stringify(httpUrl("source.txt"));
let string = "{\"list\":[{\"source\":null," +
"\"target\":null}," +
"{\"source\":{\"url\":" + sourceUriLiteral + "}," +
"\"target\":{\"path\":" + filePathLiteral + "}," +
"\"saver\":{\"type\":\"copy\"}}]}";
yield OS.File.writeAtomic(store.path,
new TextEncoder().encode(string),
{ tmpPath: store.path + ".tmp" });
yield store.load();
let items = yield list.getAll();
do_check_eq(items.length, 1);
do_check_eq(items[0].source.url, httpUrl("source.txt"));
do_check_eq(items[0].target.path, targetPath);
});
/**
* Loads downloads from a malformed JSON string.
*/
add_task(function* test_load_string_malformed()
{
let [list, store] = yield promiseNewListAndStore();
let string = "{\"list\":[{\"source\":null,\"target\":null}," +
"{\"source\":{\"url\":\"about:blank\"}}}";
yield OS.File.writeAtomic(store.path, new TextEncoder().encode(string),
{ tmpPath: store.path + ".tmp" });
try {
yield store.load();
do_throw("Exception expected when JSON data is malformed.");
} catch (ex) {
if (ex.name != "SyntaxError") {
throw ex;
}
do_print("The expected SyntaxError exception was thrown.");
}
let items = yield list.getAll();
do_check_eq(items.length, 0);
});
/**
* Saves downloads with unknown properties to a file and then reloads
* them to ensure that these properties are preserved.
*/
add_task(function* test_save_reload_unknownProperties()
{
let [listForSave, storeForSave] = yield promiseNewListAndStore();
let [listForLoad, storeForLoad] = yield promiseNewListAndStore(
storeForSave.path);
let download1 = yield promiseNewDownload(httpUrl("source.txt"));
// startTime should be ignored as it is a known property, and error
// is ignored by serialization
download1._unknownProperties = { peanut: "butter",
orange: "marmalade",
startTime: 77,
error: { message: "Passed" } };
listForSave.add(download1);
let download2 = yield promiseStartLegacyDownload();
yield download2.cancel();
download2._unknownProperties = { number: 5, object: { test: "string" } };
listForSave.add(download2);
let download3 = yield Downloads.createDownload({
source: { url: httpUrl("empty.txt"),
referrer: TEST_REFERRER_URL,
source1: "download3source1",
source2: "download3source2" },
target: { path: getTempFile(TEST_TARGET_FILE_NAME).path,
target1: "download3target1",
target2: "download3target2" },
saver : { type: "copy",
saver1: "download3saver1",
saver2: "download3saver2" },
});
listForSave.add(download3);
yield storeForSave.save();
yield storeForLoad.load();
let itemsForSave = yield listForSave.getAll();
let itemsForLoad = yield listForLoad.getAll();
do_check_eq(itemsForSave.length, itemsForLoad.length);
do_check_eq(Object.keys(itemsForLoad[0]._unknownProperties).length, 2);
do_check_eq(itemsForLoad[0]._unknownProperties.peanut, "butter");
do_check_eq(itemsForLoad[0]._unknownProperties.orange, "marmalade");
do_check_false("startTime" in itemsForLoad[0]._unknownProperties);
do_check_false("error" in itemsForLoad[0]._unknownProperties);
do_check_eq(Object.keys(itemsForLoad[1]._unknownProperties).length, 2);
do_check_eq(itemsForLoad[1]._unknownProperties.number, 5);
do_check_eq(itemsForLoad[1]._unknownProperties.object.test, "string");
do_check_eq(Object.keys(itemsForLoad[2].source._unknownProperties).length, 2);
do_check_eq(itemsForLoad[2].source._unknownProperties.source1,
"download3source1");
do_check_eq(itemsForLoad[2].source._unknownProperties.source2,
"download3source2");
do_check_eq(Object.keys(itemsForLoad[2].target._unknownProperties).length, 2);
do_check_eq(itemsForLoad[2].target._unknownProperties.target1,
"download3target1");
do_check_eq(itemsForLoad[2].target._unknownProperties.target2,
"download3target2");
do_check_eq(Object.keys(itemsForLoad[2].saver._unknownProperties).length, 2);
do_check_eq(itemsForLoad[2].saver._unknownProperties.saver1,
"download3saver1");
do_check_eq(itemsForLoad[2].saver._unknownProperties.saver2,
"download3saver2");
});

View file

@ -0,0 +1,194 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests the functions located directly in the "Downloads" object.
*/
"use strict";
// Tests
/**
* Tests that the createDownload function exists and can be called. More
* detailed tests are implemented separately for the DownloadCore module.
*/
add_task(function* test_createDownload()
{
// Creates a simple Download object without starting the download.
yield Downloads.createDownload({
source: { url: "about:blank" },
target: { path: getTempFile(TEST_TARGET_FILE_NAME).path },
saver: { type: "copy" },
});
});
/**
* Tests createDownload for private download.
*/
add_task(function* test_createDownload_private()
{
let download = yield Downloads.createDownload({
source: { url: "about:blank", isPrivate: true },
target: { path: getTempFile(TEST_TARGET_FILE_NAME).path },
saver: { type: "copy" }
});
do_check_true(download.source.isPrivate);
});
/**
* Tests createDownload for normal (public) download.
*/
add_task(function* test_createDownload_public()
{
let tempPath = getTempFile(TEST_TARGET_FILE_NAME).path;
let download = yield Downloads.createDownload({
source: { url: "about:blank", isPrivate: false },
target: { path: tempPath },
saver: { type: "copy" }
});
do_check_false(download.source.isPrivate);
download = yield Downloads.createDownload({
source: { url: "about:blank" },
target: { path: tempPath },
saver: { type: "copy" }
});
do_check_false(download.source.isPrivate);
});
/**
* Tests createDownload for a pdf saver throws if only given a url.
*/
add_task(function* test_createDownload_pdf()
{
let download = yield Downloads.createDownload({
source: { url: "about:blank" },
target: { path: getTempFile(TEST_TARGET_FILE_NAME).path },
saver: { type: "pdf" },
});
try {
yield download.start();
do_throw("The download should have failed.");
} catch (ex) {
if (!(ex instanceof Downloads.Error) || !ex.becauseSourceFailed) {
throw ex;
}
}
do_check_false(download.succeeded);
do_check_true(download.stopped);
do_check_false(download.canceled);
do_check_true(download.error !== null);
do_check_true(download.error.becauseSourceFailed);
do_check_false(download.error.becauseTargetFailed);
do_check_false(yield OS.File.exists(download.target.path));
});
/**
* Tests "fetch" with nsIURI and nsIFile as arguments.
*/
add_task(function* test_fetch_uri_file_arguments()
{
let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
yield Downloads.fetch(NetUtil.newURI(httpUrl("source.txt")), targetFile);
yield promiseVerifyContents(targetFile.path, TEST_DATA_SHORT);
});
/**
* Tests "fetch" with DownloadSource and DownloadTarget as arguments.
*/
add_task(function* test_fetch_object_arguments()
{
let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
yield Downloads.fetch({ url: httpUrl("source.txt") }, { path: targetPath });
yield promiseVerifyContents(targetPath, TEST_DATA_SHORT);
});
/**
* Tests "fetch" with string arguments.
*/
add_task(function* test_fetch_string_arguments()
{
let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
yield Downloads.fetch(httpUrl("source.txt"), targetPath);
yield promiseVerifyContents(targetPath, TEST_DATA_SHORT);
targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
yield Downloads.fetch(new String(httpUrl("source.txt")),
new String(targetPath));
yield promiseVerifyContents(targetPath, TEST_DATA_SHORT);
});
/**
* Tests that the getList function returns the same list when called multiple
* times with the same argument, but returns different lists when called with
* different arguments. More detailed tests are implemented separately for the
* DownloadList module.
*/
add_task(function* test_getList()
{
let publicListOne = yield Downloads.getList(Downloads.PUBLIC);
let privateListOne = yield Downloads.getList(Downloads.PRIVATE);
let publicListTwo = yield Downloads.getList(Downloads.PUBLIC);
let privateListTwo = yield Downloads.getList(Downloads.PRIVATE);
do_check_eq(publicListOne, publicListTwo);
do_check_eq(privateListOne, privateListTwo);
do_check_neq(publicListOne, privateListOne);
});
/**
* Tests that the getSummary function returns the same summary when called
* multiple times with the same argument, but returns different summaries when
* called with different arguments. More detailed tests are implemented
* separately for the DownloadSummary object in the DownloadList module.
*/
add_task(function* test_getSummary()
{
let publicSummaryOne = yield Downloads.getSummary(Downloads.PUBLIC);
let privateSummaryOne = yield Downloads.getSummary(Downloads.PRIVATE);
let publicSummaryTwo = yield Downloads.getSummary(Downloads.PUBLIC);
let privateSummaryTwo = yield Downloads.getSummary(Downloads.PRIVATE);
do_check_eq(publicSummaryOne, publicSummaryTwo);
do_check_eq(privateSummaryOne, privateSummaryTwo);
do_check_neq(publicSummaryOne, privateSummaryOne);
});
/**
* Tests that the getSystemDownloadsDirectory returns a non-empty download
* directory string.
*/
add_task(function* test_getSystemDownloadsDirectory()
{
let downloadDir = yield Downloads.getSystemDownloadsDirectory();
do_check_neq(downloadDir, "");
});
/**
* Tests that the getPreferredDownloadsDirectory returns a non-empty download
* directory string.
*/
add_task(function* test_getPreferredDownloadsDirectory()
{
let downloadDir = yield Downloads.getPreferredDownloadsDirectory();
do_check_neq(downloadDir, "");
});
/**
* Tests that the getTemporaryDownloadsDirectory returns a non-empty download
* directory string.
*/
add_task(function* test_getTemporaryDownloadsDirectory()
{
let downloadDir = yield Downloads.getTemporaryDownloadsDirectory();
do_check_neq(downloadDir, "");
});

View file

@ -0,0 +1,24 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/*
* The temporary directory downloads saves to, should be only readable
* for the current user.
*/
add_task(function* test_private_temp() {
let download = yield promiseStartExternalHelperAppServiceDownload(
httpUrl("empty.txt"));
yield promiseDownloadStopped(download);
var targetFile = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsIFile);
targetFile.initWithPath(download.target.path);
// 488 is the decimal value of 0o700.
equal(targetFile.parent.permissions, 448);
});

View file

@ -0,0 +1,19 @@
[DEFAULT]
head = head.js
tail =
skip-if = toolkit == 'android'
# Note: The "tail.js" file is not defined in the "tail" key because it calls
# the "add_test_task" function, that does not work properly in tail files.
support-files =
common_test_Download.js
[test_DownloadCore.js]
[test_DownloadImport.js]
[test_DownloadIntegration.js]
[test_DownloadLegacy.js]
[test_DownloadList.js]
[test_Downloads.js]
[test_DownloadStore.js]
[test_PrivateTemp.js]
skip-if = os != 'linux'