mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-10-31 11:37:30 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
18
toolkit/components/jsdownloads/moz.build
Normal file
18
toolkit/components/jsdownloads/moz.build
Normal 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',
|
||||
]
|
||||
9
toolkit/components/jsdownloads/public/moz.build
Normal file
9
toolkit/components/jsdownloads/public/moz.build
Normal 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',
|
||||
]
|
||||
|
|
@ -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);
|
||||
};
|
||||
2871
toolkit/components/jsdownloads/src/DownloadCore.jsm
Normal file
2871
toolkit/components/jsdownloads/src/DownloadCore.jsm
Normal file
File diff suppressed because it is too large
Load diff
193
toolkit/components/jsdownloads/src/DownloadImport.jsm
Normal file
193
toolkit/components/jsdownloads/src/DownloadImport.jsm
Normal 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));
|
||||
}
|
||||
}
|
||||
|
||||
1273
toolkit/components/jsdownloads/src/DownloadIntegration.jsm
Normal file
1273
toolkit/components/jsdownloads/src/DownloadIntegration.jsm
Normal file
File diff suppressed because it is too large
Load diff
309
toolkit/components/jsdownloads/src/DownloadLegacy.js
Normal file
309
toolkit/components/jsdownloads/src/DownloadLegacy.js
Normal 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]);
|
||||
559
toolkit/components/jsdownloads/src/DownloadList.jsm
Normal file
559
toolkit/components/jsdownloads/src/DownloadList.jsm
Normal 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();
|
||||
},
|
||||
};
|
||||
275
toolkit/components/jsdownloads/src/DownloadPlatform.cpp
Normal file
275
toolkit/components/jsdownloads/src/DownloadPlatform.cpp
Normal 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;
|
||||
}
|
||||
34
toolkit/components/jsdownloads/src/DownloadPlatform.h
Normal file
34
toolkit/components/jsdownloads/src/DownloadPlatform.h
Normal 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
|
||||
203
toolkit/components/jsdownloads/src/DownloadStore.jsm
Normal file
203
toolkit/components/jsdownloads/src/DownloadStore.jsm
Normal 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));
|
||||
},
|
||||
};
|
||||
243
toolkit/components/jsdownloads/src/DownloadUIHelper.jsm
Normal file
243
toolkit/components/jsdownloads/src/DownloadUIHelper.jsm
Normal 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);
|
||||
}
|
||||
};
|
||||
305
toolkit/components/jsdownloads/src/Downloads.jsm
Normal file
305
toolkit/components/jsdownloads/src/Downloads.jsm
Normal 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,
|
||||
};
|
||||
2
toolkit/components/jsdownloads/src/Downloads.manifest
Normal file
2
toolkit/components/jsdownloads/src/Downloads.manifest
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
component {1b4c85df-cbdd-4bb6-b04e-613caece083c} DownloadLegacy.js
|
||||
contract @mozilla.org/transfer;1 {1b4c85df-cbdd-4bb6-b04e-613caece083c}
|
||||
31
toolkit/components/jsdownloads/src/moz.build
Normal file
31
toolkit/components/jsdownloads/src/moz.build
Normal 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']
|
||||
7
toolkit/components/jsdownloads/test/browser/.eslintrc.js
Normal file
7
toolkit/components/jsdownloads/test/browser/.eslintrc.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
"extends": [
|
||||
"../../../../../testing/mochitest/browser.eslintrc.js"
|
||||
]
|
||||
};
|
||||
7
toolkit/components/jsdownloads/test/browser/browser.ini
Normal file
7
toolkit/components/jsdownloads/test/browser/browser.ini
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[DEFAULT]
|
||||
support-files =
|
||||
head.js
|
||||
testFile.html
|
||||
|
||||
[browser_DownloadPDFSaver.js]
|
||||
skip-if = os != "win"
|
||||
|
|
@ -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);
|
||||
});
|
||||
87
toolkit/components/jsdownloads/test/browser/head.js
Normal file
87
toolkit/components/jsdownloads/test/browser/head.js
Normal 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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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>
|
||||
7
toolkit/components/jsdownloads/test/data/.eslintrc.js
Normal file
7
toolkit/components/jsdownloads/test/data/.eslintrc.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
"extends": [
|
||||
"../../../../../testing/xpcshell/xpcshell.eslintrc.js"
|
||||
]
|
||||
};
|
||||
0
toolkit/components/jsdownloads/test/data/empty.txt
Normal file
0
toolkit/components/jsdownloads/test/data/empty.txt
Normal file
1
toolkit/components/jsdownloads/test/data/source.txt
Normal file
1
toolkit/components/jsdownloads/test/data/source.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
This test string is downloaded.
|
||||
7
toolkit/components/jsdownloads/test/unit/.eslintrc.js
Normal file
7
toolkit/components/jsdownloads/test/unit/.eslintrc.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
"extends": [
|
||||
"../../../../../testing/xpcshell/xpcshell.eslintrc.js"
|
||||
]
|
||||
};
|
||||
2432
toolkit/components/jsdownloads/test/unit/common_test_Download.js
Normal file
2432
toolkit/components/jsdownloads/test/unit/common_test_Download.js
Normal file
File diff suppressed because it is too large
Load diff
843
toolkit/components/jsdownloads/test/unit/head.js
Normal file
843
toolkit/components/jsdownloads/test/unit/head.js
Normal 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
701
toolkit/components/jsdownloads/test/unit/test_DownloadImport.js
Normal file
701
toolkit/components/jsdownloads/test/unit/test_DownloadImport.js
Normal 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]);
|
||||
}
|
||||
})
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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);
|
||||
564
toolkit/components/jsdownloads/test/unit/test_DownloadList.js
Normal file
564
toolkit/components/jsdownloads/test/unit/test_DownloadList.js
Normal 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);
|
||||
});
|
||||
315
toolkit/components/jsdownloads/test/unit/test_DownloadStore.js
Normal file
315
toolkit/components/jsdownloads/test/unit/test_DownloadStore.js
Normal 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");
|
||||
});
|
||||
194
toolkit/components/jsdownloads/test/unit/test_Downloads.js
Normal file
194
toolkit/components/jsdownloads/test/unit/test_Downloads.js
Normal 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, "");
|
||||
});
|
||||
24
toolkit/components/jsdownloads/test/unit/test_PrivateTemp.js
Normal file
24
toolkit/components/jsdownloads/test/unit/test_PrivateTemp.js
Normal 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);
|
||||
});
|
||||
19
toolkit/components/jsdownloads/test/unit/xpcshell.ini
Normal file
19
toolkit/components/jsdownloads/test/unit/xpcshell.ini
Normal 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'
|
||||
Loading…
Add table
Add a link
Reference in a new issue