We can pass over the localized error message from nsExternalHelperAppService to the downloads UI and show it to the user. That may help the user figuring out why the download failed. For now long messages are delegated to the tooltip. Differential Revision: https://phabricator.services.mozilla.com/D217317
3093 lines
105 KiB
JavaScript
3093 lines
105 KiB
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 implementation of the Downloads API objects. Consumers should get
|
|
* references to these objects through the "Downloads.sys.mjs" module.
|
|
*/
|
|
|
|
import { Integration } from "resource://gre/modules/Integration.sys.mjs";
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
DownloadHistory: "resource://gre/modules/DownloadHistory.sys.mjs",
|
|
DownloadPaths: "resource://gre/modules/DownloadPaths.sys.mjs",
|
|
E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
|
|
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
|
|
NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
lazy,
|
|
"gExternalAppLauncher",
|
|
"@mozilla.org/uriloader/external-helper-app-service;1",
|
|
Ci.nsPIExternalAppLauncher
|
|
);
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
lazy,
|
|
"gExternalHelperAppService",
|
|
"@mozilla.org/uriloader/external-helper-app-service;1",
|
|
Ci.nsIExternalHelperAppService
|
|
);
|
|
|
|
Integration.downloads.defineESModuleGetter(
|
|
lazy,
|
|
"DownloadIntegration",
|
|
"resource://gre/modules/DownloadIntegration.sys.mjs"
|
|
);
|
|
|
|
const BackgroundFileSaverStreamListener = Components.Constructor(
|
|
"@mozilla.org/network/background-file-saver;1?mode=streamlistener",
|
|
"nsIBackgroundFileSaver"
|
|
);
|
|
|
|
/**
|
|
* Returns true if the given value is a primitive string or a String object.
|
|
*/
|
|
function isString(aValue) {
|
|
// We cannot use the "instanceof" operator reliably across module boundaries.
|
|
return (
|
|
typeof aValue == "string" ||
|
|
(typeof aValue == "object" && "charAt" in aValue)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Serialize the unknown properties of aObject into aSerializable.
|
|
*/
|
|
function serializeUnknownProperties(aObject, aSerializable) {
|
|
if (aObject._unknownProperties) {
|
|
for (let property in aObject._unknownProperties) {
|
|
aSerializable[property] = aObject._unknownProperties[property];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check for any unknown properties in aSerializable and preserve those in the
|
|
* _unknownProperties field of aObject. aFilterFn is called for each property
|
|
* name of aObject and should return true only for unknown properties.
|
|
*/
|
|
function deserializeUnknownProperties(aObject, aSerializable, aFilterFn) {
|
|
for (let property in aSerializable) {
|
|
if (aFilterFn(property)) {
|
|
if (!aObject._unknownProperties) {
|
|
aObject._unknownProperties = {};
|
|
}
|
|
|
|
aObject._unknownProperties[property] = aSerializable[property];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if the file is a placeholder.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves {boolean}
|
|
* @rejects Never.
|
|
*/
|
|
async function isPlaceholder(path) {
|
|
try {
|
|
if ((await IOUtils.stat(path)).size == 0) {
|
|
return true;
|
|
}
|
|
} catch (ex) {
|
|
// Canceling the download may have removed the placeholder already.
|
|
if (ex.name != "NotFoundError") {
|
|
console.error(ex);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* This determines the minimum time interval between updates to the number of
|
|
* bytes transferred, and is a limiting factor to the sequence of readings used
|
|
* in calculating the speed of the download.
|
|
*/
|
|
const kProgressUpdateIntervalMs = 400;
|
|
|
|
/**
|
|
* Represents a single download, with associated state and actions. This object
|
|
* is transient, though it can be included in a DownloadList so that it can be
|
|
* managed by the user interface and persisted across sessions.
|
|
*/
|
|
export var Download = function () {
|
|
this._deferSucceeded = Promise.withResolvers();
|
|
};
|
|
|
|
Download.prototype = {
|
|
/**
|
|
* DownloadSource object associated with this download.
|
|
*/
|
|
source: null,
|
|
|
|
/**
|
|
* DownloadTarget object associated with this download.
|
|
*/
|
|
target: null,
|
|
|
|
/**
|
|
* DownloadSaver object associated with this download.
|
|
*/
|
|
saver: null,
|
|
|
|
/**
|
|
* Indicates that the download never started, has been completed successfully,
|
|
* failed, or has been canceled. This property becomes false when a download
|
|
* is started for the first time, or when a failed or canceled download is
|
|
* restarted.
|
|
*/
|
|
stopped: true,
|
|
|
|
/**
|
|
* Indicates that the download has been completed successfully.
|
|
*/
|
|
succeeded: false,
|
|
|
|
/**
|
|
* Indicates that the download has been canceled. This property can become
|
|
* true, then it can be reset to false when a canceled download is restarted.
|
|
*
|
|
* This property becomes true as soon as the "cancel" method is called, though
|
|
* the "stopped" property might remain false until the cancellation request
|
|
* has been processed. Temporary files or part files may still exist even if
|
|
* they are expected to be deleted, until the "stopped" property becomes true.
|
|
*/
|
|
canceled: false,
|
|
|
|
/**
|
|
* Downloaded files can be deleted from within Firefox, e.g. via the context
|
|
* menu. Currently Firefox does not track file moves (see bug 1746386), so if
|
|
* a download's target file stops existing we have to assume it's "moved or
|
|
* missing." To distinguish files intentionally deleted within Firefox from
|
|
* files that are moved/missing, we mark them as "deleted" with this property.
|
|
*/
|
|
deleted: false,
|
|
|
|
/**
|
|
* When the download fails, this is set to a DownloadError instance indicating
|
|
* the cause of the failure. If the download has been completed successfully
|
|
* or has been canceled, this property is null. This property is reset to
|
|
* null when a failed download is restarted.
|
|
*/
|
|
error: null,
|
|
|
|
/**
|
|
* Indicates the start time of the download. When the download starts,
|
|
* this property is set to a valid Date object. The default value is null
|
|
* before the download starts.
|
|
*/
|
|
startTime: null,
|
|
|
|
/**
|
|
* Indicates whether this download's "progress" property is able to report
|
|
* partial progress while the download proceeds, and whether the value in
|
|
* totalBytes is relevant. This depends on the saver and the download source.
|
|
*/
|
|
hasProgress: false,
|
|
|
|
/**
|
|
* Progress percent, from 0 to 100. Intermediate values are reported only if
|
|
* hasProgress is true.
|
|
*
|
|
* @note You shouldn't rely on this property being equal to 100 to determine
|
|
* whether the download is completed. You should use the individual
|
|
* state properties instead.
|
|
*/
|
|
progress: 0,
|
|
|
|
/**
|
|
* When hasProgress is true, indicates the total number of bytes to be
|
|
* transferred before the download finishes, that can be zero for empty files.
|
|
*
|
|
* When hasProgress is false, this property is always zero.
|
|
*
|
|
* @note This property may be different than the final file size on disk for
|
|
* downloads that are encoded during the network transfer. You can use
|
|
* the "size" property of the DownloadTarget object to get the actual
|
|
* size on disk once the download succeeds.
|
|
*/
|
|
totalBytes: 0,
|
|
|
|
/**
|
|
* Number of bytes currently transferred. This value starts at zero, and may
|
|
* be updated regardless of the value of hasProgress.
|
|
*
|
|
* @note You shouldn't rely on this property being equal to totalBytes to
|
|
* determine whether the download is completed. You should use the
|
|
* individual state properties instead. This property may not be
|
|
* updated during the last part of the download.
|
|
*/
|
|
currentBytes: 0,
|
|
|
|
/**
|
|
* Fractional number representing the speed of the download, in bytes per
|
|
* second. This value is zero when the download is stopped, and may be
|
|
* updated regardless of the value of hasProgress.
|
|
*/
|
|
speed: 0,
|
|
|
|
/**
|
|
* Indicates whether, at this time, there is any partially downloaded data
|
|
* that can be used when restarting a failed or canceled download.
|
|
*
|
|
* Even if the download has partial data on disk, hasPartialData will be false
|
|
* if that data cannot be used to restart the download. In order to determine
|
|
* if a part file is being used which contains partial data the
|
|
* Download.target.partFilePath should be checked.
|
|
*
|
|
* This property is relevant while the download is in progress, and also if it
|
|
* failed or has been canceled. If the download has been completed
|
|
* successfully, this property is always false.
|
|
*
|
|
* Whether partial data can actually be retained depends on the saver and the
|
|
* download source, and may not be known before the download is started.
|
|
*/
|
|
hasPartialData: false,
|
|
|
|
/**
|
|
* Indicates whether, at this time, there is any data that has been blocked.
|
|
* Since reputation blocking takes place after the download has fully
|
|
* completed a value of true also indicates 100% of the data is present.
|
|
*/
|
|
hasBlockedData: false,
|
|
|
|
/**
|
|
* This can be set to a function that is called after other properties change.
|
|
*/
|
|
onchange: null,
|
|
|
|
/**
|
|
* This tells if the user has chosen to open/run the downloaded file after
|
|
* download has completed.
|
|
*/
|
|
launchWhenSucceeded: false,
|
|
|
|
/**
|
|
* When a download starts, we typically want to automatically open the
|
|
* downloads panel if the pref browser.download.alwaysOpenPanel is enabled.
|
|
* However, there are conditions where we want to prevent this. For example, a
|
|
* false value can prevent the downloads panel from opening when an add-on
|
|
* creates a download without user input as part of some background operation.
|
|
*/
|
|
openDownloadsListOnStart: true,
|
|
|
|
/**
|
|
* This represents the MIME type of the download.
|
|
*/
|
|
contentType: null,
|
|
|
|
/**
|
|
* This indicates the path of the application to be used to launch the file,
|
|
* or null if the file should be launched with the default application.
|
|
*/
|
|
launcherPath: null,
|
|
|
|
/**
|
|
* Raises the onchange notification.
|
|
*/
|
|
_notifyChange: function D_notifyChange() {
|
|
try {
|
|
if (this.onchange) {
|
|
this.onchange();
|
|
}
|
|
} catch (ex) {
|
|
console.error(ex);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* The download may be stopped and restarted multiple times before it
|
|
* completes successfully. This may happen if any of the download attempts is
|
|
* canceled or fails.
|
|
*
|
|
* This property contains a promise that is linked to the current attempt, or
|
|
* null if the download is either stopped or in the process of being canceled.
|
|
* If the download restarts, this property is replaced with a new promise.
|
|
*
|
|
* The promise is resolved if the attempt it represents finishes successfully,
|
|
* and rejected if the attempt fails.
|
|
*/
|
|
_currentAttempt: null,
|
|
|
|
/**
|
|
* The download was launched to open from the Downloads Panel.
|
|
*/
|
|
_launchedFromPanel: false,
|
|
|
|
/**
|
|
* Starts the download for the first time, or restarts a download that failed
|
|
* or has been canceled.
|
|
*
|
|
* Calling this method when the download has been completed successfully has
|
|
* no effect, and the method returns a resolved promise. If the download is
|
|
* in progress, the method returns the same promise as the previous call.
|
|
*
|
|
* If the "cancel" method was called but the cancellation process has not
|
|
* finished yet, this method waits for the cancellation to finish, then
|
|
* restarts the download immediately.
|
|
*
|
|
* @note If you need to start a new download from the same source, rather than
|
|
* restarting a failed or canceled one, you should create a separate
|
|
* Download object with the same source as the current one.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the download has finished successfully.
|
|
* @rejects JavaScript exception if the download failed.
|
|
*/
|
|
start: function D_start() {
|
|
// If the download succeeded, it's the final state, we have nothing to do.
|
|
if (this.succeeded) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
// If the download already started and hasn't failed or hasn't been
|
|
// canceled, return the same promise as the previous call, allowing the
|
|
// caller to wait for the current attempt to finish.
|
|
if (this._currentAttempt) {
|
|
return this._currentAttempt;
|
|
}
|
|
|
|
// While shutting down or disposing of this object, we prevent the download
|
|
// from returning to be in progress.
|
|
if (this._finalized) {
|
|
return Promise.reject(
|
|
new DownloadError({
|
|
message: "Cannot start after finalization.",
|
|
})
|
|
);
|
|
}
|
|
|
|
if (this.error && this.error.becauseBlockedByReputationCheck) {
|
|
return Promise.reject(
|
|
new DownloadError({
|
|
message: "Cannot start after being blocked by a reputation check.",
|
|
})
|
|
);
|
|
}
|
|
|
|
// Initialize all the status properties for a new or restarted download.
|
|
this.stopped = false;
|
|
this.canceled = false;
|
|
this.error = null;
|
|
// Avoid serializing the previous error, or it would be restored on the next
|
|
// startup, even if the download was restarted.
|
|
delete this._unknownProperties?.errorObj;
|
|
this.hasProgress = false;
|
|
this.hasBlockedData = false;
|
|
this.progress = 0;
|
|
this.totalBytes = 0;
|
|
this.currentBytes = 0;
|
|
this.startTime = new Date();
|
|
|
|
// Create a new deferred object and an associated promise before starting
|
|
// the actual download. We store it on the download as the current attempt.
|
|
let deferAttempt = Promise.withResolvers();
|
|
let currentAttempt = deferAttempt.promise;
|
|
this._currentAttempt = currentAttempt;
|
|
|
|
// Restart the progress and speed calculations from scratch.
|
|
this._lastProgressTimeMs = 0;
|
|
|
|
// This function propagates progress from the DownloadSaver object, unless
|
|
// it comes in late from a download attempt that was replaced by a new one.
|
|
// If the cancellation process for the download has started, then the update
|
|
// is ignored.
|
|
function DS_setProgressBytes(aCurrentBytes, aTotalBytes, aHasPartialData) {
|
|
if (this._currentAttempt == currentAttempt) {
|
|
this._setBytes(aCurrentBytes, aTotalBytes, aHasPartialData);
|
|
}
|
|
}
|
|
|
|
// This function propagates download properties from the DownloadSaver
|
|
// object, unless it comes in late from a download attempt that was
|
|
// replaced by a new one. If the cancellation process for the download has
|
|
// started, then the update is ignored.
|
|
function DS_setProperties(aOptions) {
|
|
if (this._currentAttempt != currentAttempt) {
|
|
return;
|
|
}
|
|
|
|
let changeMade = false;
|
|
|
|
for (let property of [
|
|
"contentType",
|
|
"progress",
|
|
"hasPartialData",
|
|
"hasBlockedData",
|
|
]) {
|
|
if (property in aOptions && this[property] != aOptions[property]) {
|
|
this[property] = aOptions[property];
|
|
changeMade = true;
|
|
}
|
|
}
|
|
|
|
if (changeMade) {
|
|
this._notifyChange();
|
|
}
|
|
}
|
|
|
|
// Now that we stored the promise in the download object, we can start the
|
|
// task that will actually execute the download.
|
|
deferAttempt.resolve(
|
|
(async () => {
|
|
// Wait upon any pending operation before restarting.
|
|
if (this._promiseCanceled) {
|
|
await this._promiseCanceled;
|
|
}
|
|
if (this._promiseRemovePartialData) {
|
|
try {
|
|
await this._promiseRemovePartialData;
|
|
} catch (ex) {
|
|
// Ignore any errors, which are already reported by the original
|
|
// caller of the removePartialData method.
|
|
}
|
|
}
|
|
|
|
// In case the download was restarted while cancellation was in progress,
|
|
// but the previous attempt actually succeeded before cancellation could
|
|
// be processed, it is possible that the download has already finished.
|
|
if (this.succeeded) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (this.downloadingToSameFile()) {
|
|
throw new DownloadError({
|
|
message: "Can't overwrite the source file.",
|
|
becauseTargetFailed: true,
|
|
});
|
|
}
|
|
|
|
// Disallow download if parental controls service restricts it.
|
|
if (
|
|
await lazy.DownloadIntegration.shouldBlockForParentalControls(this)
|
|
) {
|
|
throw new DownloadError({ becauseBlockedByParentalControls: true });
|
|
}
|
|
|
|
// We should check if we have been canceled in the meantime, after all
|
|
// the previous asynchronous operations have been executed and just
|
|
// before we call the "execute" method of the saver.
|
|
if (this._promiseCanceled) {
|
|
// The exception will become a cancellation in the "catch" block.
|
|
throw new Error(undefined);
|
|
}
|
|
|
|
// Execute the actual download through the saver object.
|
|
this._saverExecuting = true;
|
|
try {
|
|
await this.saver.execute(
|
|
DS_setProgressBytes.bind(this),
|
|
DS_setProperties.bind(this)
|
|
);
|
|
} catch (ex) {
|
|
// Remove the target file placeholder and all partial data when
|
|
// needed, independently of which code path failed. In some cases, the
|
|
// component executing the download may have already removed the file.
|
|
if (!this.hasPartialData && !this.hasBlockedData) {
|
|
await this.saver.removeData(true);
|
|
}
|
|
throw ex;
|
|
}
|
|
|
|
// Now that the actual saving finished, read the actual file size on
|
|
// disk, that may be different from the amount of data transferred.
|
|
await this.target.refresh();
|
|
|
|
// Check for the last time if the download has been canceled. This must
|
|
// be done right before setting the "stopped" property of the download,
|
|
// without any asynchronous operations in the middle, so that another
|
|
// cancellation request cannot start in the meantime and stay unhandled.
|
|
if (this._promiseCanceled) {
|
|
// To keep the internal state of the Download object consistent, we
|
|
// just delete the target and effectively cancel the download. Since
|
|
// the DownloadSaver succeeded, we already renamed the ".part" file to
|
|
// the final name, and this results in all the data being deleted.
|
|
await this.saver.removeData(true);
|
|
|
|
// Cancellation exceptions will be changed in the catch block below.
|
|
throw new DownloadError();
|
|
}
|
|
|
|
// Update the status properties for a successful download.
|
|
this.progress = 100;
|
|
this.succeeded = true;
|
|
this.hasPartialData = false;
|
|
} catch (originalEx) {
|
|
// We may choose a different exception to propagate in the code below,
|
|
// or wrap the original one. We do this mutation in a different variable
|
|
// because of the "no-ex-assign" ESLint rule.
|
|
let ex = originalEx;
|
|
|
|
// Fail with a generic status code on cancellation, so that the caller
|
|
// is forced to actually check the status properties to see if the
|
|
// download was canceled or failed because of other reasons.
|
|
if (this._promiseCanceled) {
|
|
throw new DownloadError({ message: "Download canceled." });
|
|
}
|
|
|
|
// An HTTP 450 error code is used by Windows to indicate that a uri is
|
|
// blocked by parental controls. This will prevent the download from
|
|
// occuring, so an error needs to be raised. This is not performed
|
|
// during the parental controls check above as it requires the request
|
|
// to start.
|
|
if (this._blockedByParentalControls) {
|
|
ex = new DownloadError({ becauseBlockedByParentalControls: true });
|
|
}
|
|
|
|
// Update the download error, unless a new attempt already started. The
|
|
// change in the status property is notified in the finally block.
|
|
if (this._currentAttempt == currentAttempt || !this._currentAttempt) {
|
|
if (!(ex instanceof DownloadError)) {
|
|
let properties = { innerException: ex };
|
|
|
|
if (ex.message) {
|
|
properties.message = ex.message;
|
|
}
|
|
|
|
ex = new DownloadError(properties);
|
|
}
|
|
// Don't store an error if it's an abort caused by shutdown, so the
|
|
// download can be retried automatically at the next startup.
|
|
if (
|
|
originalEx.result != Cr.NS_ERROR_ABORT ||
|
|
!Services.startup.isInOrBeyondShutdownPhase(
|
|
Ci.nsIAppStartup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
|
|
)
|
|
) {
|
|
this.error = ex;
|
|
}
|
|
}
|
|
throw ex;
|
|
} finally {
|
|
// Any cancellation request has now been processed.
|
|
this._saverExecuting = false;
|
|
this._promiseCanceled = null;
|
|
|
|
// Update the status properties, unless a new attempt already started.
|
|
if (this._currentAttempt == currentAttempt || !this._currentAttempt) {
|
|
this._currentAttempt = null;
|
|
this.stopped = true;
|
|
this.speed = 0;
|
|
this._notifyChange();
|
|
if (this.succeeded) {
|
|
await this._succeed();
|
|
}
|
|
}
|
|
}
|
|
})()
|
|
);
|
|
|
|
// Notify the new download state before returning.
|
|
this._notifyChange();
|
|
return currentAttempt;
|
|
},
|
|
|
|
/**
|
|
* Perform the actions necessary when a Download succeeds.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the steps to take after success have completed.
|
|
* @rejects JavaScript exception if any of the operations failed.
|
|
*/
|
|
async _succeed() {
|
|
await lazy.DownloadIntegration.downloadDone(this);
|
|
|
|
this._deferSucceeded.resolve();
|
|
|
|
if (this.launchWhenSucceeded) {
|
|
this.launch().catch(console.error);
|
|
|
|
// Always schedule files to be deleted at the end of the private browsing
|
|
// mode, regardless of the value of the pref.
|
|
if (this.source.isPrivate) {
|
|
lazy.gExternalAppLauncher.deleteTemporaryPrivateFileWhenPossible(
|
|
new lazy.FileUtils.File(this.target.path)
|
|
);
|
|
} else if (
|
|
Services.prefs.getBoolPref("browser.helperApps.deleteTempFileOnExit") &&
|
|
Services.prefs.getBoolPref(
|
|
"browser.download.start_downloads_in_tmp_dir",
|
|
false
|
|
)
|
|
) {
|
|
lazy.gExternalAppLauncher.deleteTemporaryFileOnExit(
|
|
new lazy.FileUtils.File(this.target.path)
|
|
);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* When a request to unblock the download is received, contains a promise
|
|
* that will be resolved when the unblock request is completed. This property
|
|
* will then continue to hold the promise indefinitely.
|
|
*/
|
|
_promiseUnblock: null,
|
|
|
|
/**
|
|
* When a request to confirm the block of the download is received, contains
|
|
* a promise that will be resolved when cleaning up the download has
|
|
* completed. This property will then continue to hold the promise
|
|
* indefinitely.
|
|
*/
|
|
_promiseConfirmBlock: null,
|
|
|
|
/**
|
|
* Unblocks a download which had been blocked by reputation.
|
|
*
|
|
* The file will be moved out of quarantine and the download will be
|
|
* marked as succeeded.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the Download has been unblocked and succeeded.
|
|
* @rejects JavaScript exception if any of the operations failed.
|
|
*/
|
|
unblock() {
|
|
if (this._promiseUnblock) {
|
|
return this._promiseUnblock;
|
|
}
|
|
|
|
if (this._promiseConfirmBlock) {
|
|
return Promise.reject(
|
|
new Error("Download block has been confirmed, cannot unblock.")
|
|
);
|
|
}
|
|
|
|
if (this.error?.becauseBlockedByReputationCheck) {
|
|
Services.telemetry
|
|
.getKeyedHistogramById("DOWNLOADS_USER_ACTION_ON_BLOCKED_DOWNLOAD")
|
|
.add(this.error.reputationCheckVerdict, 2); // unblock
|
|
}
|
|
|
|
if (
|
|
this.error?.reputationCheckVerdict == DownloadError.BLOCK_VERDICT_INSECURE
|
|
) {
|
|
// In this Error case, the download was actually canceled before it was
|
|
// passed to the Download UI. So we need to start the download here.
|
|
this.error = null;
|
|
this.succeeded = false;
|
|
this.hasBlockedData = false;
|
|
// This ensures the verdict will not get set again after the browser
|
|
// restarts and the download gets serialized and de-serialized again.
|
|
delete this._unknownProperties?.errorObj;
|
|
this.start()
|
|
.catch(err => {
|
|
if (err.becauseTargetFailed) {
|
|
// In case we cannot write to the target file
|
|
// retry with a new unique name
|
|
let uniquePath = lazy.DownloadPaths.createNiceUniqueFile(
|
|
new lazy.FileUtils.File(this.target.path)
|
|
).path;
|
|
this.target.path = uniquePath;
|
|
return this.start();
|
|
}
|
|
return Promise.reject(err);
|
|
})
|
|
.catch(err => {
|
|
if (!this.canceled) {
|
|
console.error(err);
|
|
}
|
|
this._notifyChange();
|
|
});
|
|
this._notifyChange();
|
|
this._promiseUnblock = lazy.DownloadIntegration.downloadDone(this);
|
|
return this._promiseUnblock;
|
|
}
|
|
|
|
if (!this.hasBlockedData) {
|
|
return Promise.reject(
|
|
new Error("unblock may only be called on Downloads with blocked data.")
|
|
);
|
|
}
|
|
|
|
this._promiseUnblock = (async () => {
|
|
try {
|
|
await IOUtils.move(this.target.partFilePath, this.target.path);
|
|
await this.target.refresh();
|
|
} catch (ex) {
|
|
await this.refresh();
|
|
this._promiseUnblock = null;
|
|
throw ex;
|
|
}
|
|
|
|
this.succeeded = true;
|
|
this.hasBlockedData = false;
|
|
this._notifyChange();
|
|
await this._succeed();
|
|
})();
|
|
|
|
return this._promiseUnblock;
|
|
},
|
|
|
|
/**
|
|
* Confirms that a blocked download should be cleaned up.
|
|
*
|
|
* If a download was blocked but retained on disk this method can be used
|
|
* to remove the file.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the Download's data has been removed.
|
|
* @rejects JavaScript exception if any of the operations failed.
|
|
*/
|
|
confirmBlock() {
|
|
if (this._promiseConfirmBlock) {
|
|
return this._promiseConfirmBlock;
|
|
}
|
|
|
|
if (this._promiseUnblock) {
|
|
return Promise.reject(
|
|
new Error("Download is being unblocked, cannot confirmBlock.")
|
|
);
|
|
}
|
|
|
|
if (this.error?.becauseBlockedByReputationCheck) {
|
|
// We have to record the telemetry in both DownloadsCommon.deleteDownload
|
|
// and confirmBlock here. The former is for cases where users click
|
|
// "Remove file" in the download panel and the latter is when
|
|
// users click "X" button in about:downloads.
|
|
Services.telemetry
|
|
.getKeyedHistogramById("DOWNLOADS_USER_ACTION_ON_BLOCKED_DOWNLOAD")
|
|
.add(this.error.reputationCheckVerdict, 1); // confirm block
|
|
}
|
|
|
|
if (!this.hasBlockedData) {
|
|
return Promise.reject(
|
|
new Error(
|
|
"confirmBlock may only be called on Downloads with blocked data."
|
|
)
|
|
);
|
|
}
|
|
|
|
this._promiseConfirmBlock = (async () => {
|
|
// This call never throws exceptions. If the removal fails, the blocked
|
|
// data remains stored on disk in the ".part" file.
|
|
await this.saver.removeData();
|
|
|
|
this.hasBlockedData = false;
|
|
this._notifyChange();
|
|
})();
|
|
|
|
return this._promiseConfirmBlock;
|
|
},
|
|
|
|
/*
|
|
* Launches the file after download has completed. This can open
|
|
* the file with the default application for the target MIME type
|
|
* or file extension, or with a custom application if launcherPath
|
|
* is set.
|
|
*
|
|
* @param options.openWhere Optional string indicating how to open when handling
|
|
* download by opening the target file URI.
|
|
* One of "window", "tab", "tabshifted"
|
|
* @param options.useSystemDefault
|
|
* Optional value indicating how to handle launching this download,
|
|
* this time only. Will override the associated mimeInfo.preferredAction
|
|
* @return {Promise}
|
|
* @resolves When the instruction to launch the file has been
|
|
* successfully given to the operating system. Note that
|
|
* the OS might still take a while until the file is actually
|
|
* launched.
|
|
* @rejects JavaScript exception if there was an error trying to launch
|
|
* the file.
|
|
*/
|
|
launch(options = {}) {
|
|
if (!this.succeeded) {
|
|
return Promise.reject(
|
|
new Error("launch can only be called if the download succeeded")
|
|
);
|
|
}
|
|
|
|
if (this._launchedFromPanel) {
|
|
Services.telemetry.scalarAdd("downloads.file_opened", 1);
|
|
}
|
|
|
|
return lazy.DownloadIntegration.launchDownload(this, options);
|
|
},
|
|
|
|
/*
|
|
* Shows the folder containing the target file, or where the target file
|
|
* will be saved. This may be called at any time, even if the download
|
|
* failed or is currently in progress.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the instruction to open the containing folder has been
|
|
* successfully given to the operating system. Note that
|
|
* the OS might still take a while until the folder is actually
|
|
* opened.
|
|
* @rejects JavaScript exception if there was an error trying to open
|
|
* the containing folder.
|
|
*/
|
|
showContainingDirectory: function D_showContainingDirectory() {
|
|
return lazy.DownloadIntegration.showContainingDirectory(this.target.path);
|
|
},
|
|
|
|
/**
|
|
* When a request to cancel the download is received, contains a promise that
|
|
* will be resolved when the cancellation request is processed. When the
|
|
* request is processed, this property becomes null again.
|
|
*/
|
|
_promiseCanceled: null,
|
|
|
|
/**
|
|
* True between the call to the "execute" method of the saver and the
|
|
* completion of the current download attempt.
|
|
*/
|
|
_saverExecuting: false,
|
|
|
|
/**
|
|
* Cancels the download.
|
|
*
|
|
* The cancellation request is asynchronous. Until the cancellation process
|
|
* finishes, temporary files or part files may still exist even if they are
|
|
* expected to be deleted.
|
|
*
|
|
* In case the download completes successfully before the cancellation request
|
|
* could be processed, this method has no effect, and it returns a resolved
|
|
* promise. You should check the properties of the download at the time the
|
|
* returned promise is resolved to determine if the download was cancelled.
|
|
*
|
|
* Calling this method when the download has been completed successfully,
|
|
* failed, or has been canceled has no effect, and the method returns a
|
|
* resolved promise. This behavior is designed for the case where the call
|
|
* to "cancel" happens asynchronously, and is consistent with the case where
|
|
* the cancellation request could not be processed in time.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the cancellation process has finished.
|
|
* @rejects Never.
|
|
*/
|
|
cancel: function D_cancel() {
|
|
// If the download is currently stopped, we have nothing to do.
|
|
if (this.stopped) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
if (!this._promiseCanceled) {
|
|
// Start a new cancellation request.
|
|
this._promiseCanceled = new Promise(resolve => {
|
|
this._currentAttempt.then(resolve, resolve);
|
|
});
|
|
|
|
// The download can already be restarted.
|
|
this._currentAttempt = null;
|
|
|
|
// Notify that the cancellation request was received.
|
|
this.canceled = true;
|
|
this._notifyChange();
|
|
|
|
// Execute the actual cancellation through the saver object, in case it
|
|
// has already started. Otherwise, the cancellation will be handled just
|
|
// before the saver is started.
|
|
if (this._saverExecuting) {
|
|
this.saver.cancel();
|
|
}
|
|
}
|
|
|
|
return this._promiseCanceled;
|
|
},
|
|
|
|
/**
|
|
* Indicates whether any partially downloaded data should be retained, to use
|
|
* when restarting a failed or canceled download. The default is false.
|
|
*
|
|
* Whether partial data can actually be retained depends on the saver and the
|
|
* download source, and may not be known before the download is started.
|
|
*
|
|
* To have any effect, this property must be set before starting the download.
|
|
* Resetting this property to false after the download has already started
|
|
* will not remove any partial data.
|
|
*
|
|
* If this property is set to true, care should be taken that partial data is
|
|
* removed before the reference to the download is discarded. This can be
|
|
* done using the removePartialData or the "finalize" methods.
|
|
*/
|
|
tryToKeepPartialData: false,
|
|
|
|
/**
|
|
* When a request to remove partially downloaded data is received, contains a
|
|
* promise that will be resolved when the removal request is processed. When
|
|
* the request is processed, this property becomes null again.
|
|
*/
|
|
_promiseRemovePartialData: null,
|
|
|
|
/**
|
|
* Removes any partial data kept as part of a canceled or failed download.
|
|
*
|
|
* If the download is not canceled or failed, this method has no effect, and
|
|
* it returns a resolved promise. If the "cancel" method was called but the
|
|
* cancellation process has not finished yet, this method waits for the
|
|
* cancellation to finish, then removes the partial data.
|
|
*
|
|
* After this method has been called, if the tryToKeepPartialData property is
|
|
* still true when the download is restarted, partial data will be retained
|
|
* during the new download attempt.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the partial data has been successfully removed.
|
|
* @rejects JavaScript exception if the operation could not be completed.
|
|
*/
|
|
removePartialData() {
|
|
if (!this.canceled && !this.error) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
if (!this._promiseRemovePartialData) {
|
|
this._promiseRemovePartialData = (async () => {
|
|
try {
|
|
// Wait upon any pending cancellation request.
|
|
if (this._promiseCanceled) {
|
|
await this._promiseCanceled;
|
|
}
|
|
// Ask the saver object to remove any partial data.
|
|
await this.saver.removeData();
|
|
// For completeness, clear the number of bytes transferred.
|
|
if (this.currentBytes != 0 || this.hasPartialData) {
|
|
this.currentBytes = 0;
|
|
this.hasPartialData = false;
|
|
this.target.refreshPartFileState();
|
|
this._notifyChange();
|
|
}
|
|
} finally {
|
|
this._promiseRemovePartialData = null;
|
|
}
|
|
})();
|
|
}
|
|
|
|
return this._promiseRemovePartialData;
|
|
},
|
|
|
|
/**
|
|
* Returns true if the download source is the same as the target file.
|
|
*/
|
|
downloadingToSameFile() {
|
|
if (!this.source.url || !this.source.url.startsWith("file:")) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
let sourceUri = lazy.NetUtil.newURI(this.source.url);
|
|
let targetUri = lazy.NetUtil.newURI(
|
|
new lazy.FileUtils.File(this.target.path)
|
|
);
|
|
return sourceUri.equals(targetUri);
|
|
} catch (ex) {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* This deferred object contains a promise that is resolved as soon as this
|
|
* download finishes successfully, and is never rejected. This property is
|
|
* initialized when the download is created, and never changes.
|
|
*/
|
|
_deferSucceeded: null,
|
|
|
|
/**
|
|
* Returns a promise that is resolved as soon as this download finishes
|
|
* successfully, even if the download was stopped and restarted meanwhile.
|
|
*
|
|
* You can use this property for scheduling download completion actions in the
|
|
* current session, for downloads that are controlled interactively. If the
|
|
* download is not controlled interactively, you should use the promise
|
|
* returned by the "start" method instead, to check for success or failure.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the download has finished successfully.
|
|
* @rejects Never.
|
|
*/
|
|
whenSucceeded: function D_whenSucceeded() {
|
|
return this._deferSucceeded.promise;
|
|
},
|
|
|
|
/**
|
|
* Updates the state of a finished, failed, or canceled download based on the
|
|
* current state in the file system. If the download is in progress or it has
|
|
* been finalized, this method has no effect, and it returns a resolved
|
|
* promise.
|
|
*
|
|
* This allows the properties of the download to be updated in case the user
|
|
* moved or deleted the target file or its associated ".part" file.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the operation has completed.
|
|
* @rejects Never.
|
|
*/
|
|
refresh() {
|
|
return (async () => {
|
|
if (!this.stopped || this._finalized) {
|
|
return;
|
|
}
|
|
|
|
if (this.succeeded) {
|
|
let oldExists = this.target.exists;
|
|
let oldSize = this.target.size;
|
|
await this.target.refresh();
|
|
if (oldExists != this.target.exists || oldSize != this.target.size) {
|
|
this._notifyChange();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Update the current progress from disk if we retained partial data.
|
|
if (
|
|
(this.hasPartialData || this.hasBlockedData) &&
|
|
this.target.partFilePath
|
|
) {
|
|
try {
|
|
let stat = await IOUtils.stat(this.target.partFilePath);
|
|
|
|
// Ignore the result if the state has changed meanwhile.
|
|
if (!this.stopped || this._finalized) {
|
|
return;
|
|
}
|
|
|
|
// Update the bytes transferred and the related progress properties.
|
|
this.currentBytes = stat.size;
|
|
if (this.totalBytes > 0) {
|
|
this.hasProgress = true;
|
|
this.progress = Math.floor(
|
|
(this.currentBytes / this.totalBytes) * 100
|
|
);
|
|
}
|
|
} catch (ex) {
|
|
if (ex.name != "NotFoundError") {
|
|
throw ex;
|
|
}
|
|
// Ignore the result if the state has changed meanwhile.
|
|
if (!this.stopped || this._finalized) {
|
|
return;
|
|
}
|
|
// In case we've blocked the Download becasue its
|
|
// insecure, we should not set hasBlockedData to
|
|
// false as its required to show the Unblock option.
|
|
if (
|
|
this.error.reputationCheckVerdict ==
|
|
DownloadError.BLOCK_VERDICT_INSECURE
|
|
) {
|
|
return;
|
|
}
|
|
|
|
this.hasBlockedData = false;
|
|
this.hasPartialData = false;
|
|
}
|
|
|
|
this._notifyChange();
|
|
}
|
|
})().catch(console.error);
|
|
},
|
|
|
|
/**
|
|
* True if the "finalize" method has been called. This prevents the download
|
|
* from starting again after having been stopped.
|
|
*/
|
|
_finalized: false,
|
|
|
|
/**
|
|
* True if the "finalize" has been called and fully finished it's execution.
|
|
*/
|
|
_finalizeExecuted: false,
|
|
|
|
/**
|
|
* Ensures that the download is stopped, and optionally removes any partial
|
|
* data kept as part of a canceled or failed download. After this method has
|
|
* been called, the download cannot be started again.
|
|
*
|
|
* This method should be used in place of "cancel" and removePartialData while
|
|
* shutting down or disposing of the download object, to prevent other callers
|
|
* from interfering with the operation. This is required because cancellation
|
|
* and other operations are asynchronous.
|
|
*
|
|
* @param aRemovePartialData
|
|
* Whether any partially downloaded data should be removed after the
|
|
* download has been stopped.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the operation has finished successfully.
|
|
* @rejects JavaScript exception if an error occurred while removing the
|
|
* partially downloaded data.
|
|
*/
|
|
finalize(aRemovePartialData) {
|
|
// Prevents the download from starting again after having been stopped.
|
|
this._finalized = true;
|
|
let promise;
|
|
|
|
if (aRemovePartialData) {
|
|
// Cancel the download, in case it is currently in progress, then remove
|
|
// any partially downloaded data. The removal operation waits for
|
|
// cancellation to be completed before resolving the promise it returns.
|
|
this.cancel();
|
|
promise = this.removePartialData();
|
|
} else {
|
|
// Just cancel the download, in case it is currently in progress.
|
|
promise = this.cancel();
|
|
}
|
|
promise.then(() => {
|
|
// At this point, either removing data / just cancelling the download should be done.
|
|
this._finalizeExecuted = true;
|
|
});
|
|
|
|
return promise;
|
|
},
|
|
|
|
/**
|
|
* Deletes all file data associated with a download, preserving the download
|
|
* object itself and updating it for download views.
|
|
*/
|
|
async manuallyRemoveData() {
|
|
let { path } = this.target;
|
|
if (this.succeeded) {
|
|
// Temp files are made "read-only" by DownloadIntegration.downloadDone, so
|
|
// reset the permission bits to read/write. This won't be necessary after
|
|
// bug 1733587 since Downloads won't ever be temporary.
|
|
await IOUtils.setPermissions(path, 0o660);
|
|
await IOUtils.remove(path, { ignoreAbsent: true });
|
|
}
|
|
this.deleted = true;
|
|
await this.cancel();
|
|
await this.removePartialData();
|
|
// We need to guarantee that the UI is refreshed irrespective of what state
|
|
// the download is in when this is called, to ensure the download doesn't
|
|
// wind up stuck displaying as if it exists when it actually doesn't. And
|
|
// that means updating this.target.partFileExists no matter what.
|
|
await this.target.refreshPartFileState();
|
|
await this.refresh();
|
|
// The above methods will sometimes call _notifyChange, but not always. It
|
|
// depends on whether the download is `succeeded`, `stopped`, `canceled`,
|
|
// etc. Since this method needs to update the UI and can be invoked on any
|
|
// download as long as its target has some file on the system, we need to
|
|
// call _notifyChange no matter what state the download is in.
|
|
this._notifyChange();
|
|
},
|
|
|
|
/**
|
|
* Indicates the time of the last progress notification, expressed as the
|
|
* number of milliseconds since January 1, 1970, 00:00:00 UTC. This is zero
|
|
* until some bytes have actually been transferred.
|
|
*/
|
|
_lastProgressTimeMs: 0,
|
|
|
|
/**
|
|
* Updates progress notifications based on the number of bytes transferred.
|
|
*
|
|
* The number of bytes transferred is not updated unless enough time passed
|
|
* since this function was last called. This limits the computation load, in
|
|
* particular when the listeners update the user interface in response.
|
|
*
|
|
* @param aCurrentBytes
|
|
* Number of bytes transferred until now.
|
|
* @param aTotalBytes
|
|
* Total number of bytes to be transferred, or -1 if unknown.
|
|
* @param aHasPartialData
|
|
* Indicates whether the partially downloaded data can be used when
|
|
* restarting the download if it fails or is canceled.
|
|
*/
|
|
_setBytes: function D_setBytes(aCurrentBytes, aTotalBytes, aHasPartialData) {
|
|
let changeMade = this.hasPartialData != aHasPartialData;
|
|
this.hasPartialData = aHasPartialData;
|
|
|
|
// Unless aTotalBytes is -1, we can report partial download progress. In
|
|
// this case, notify when the related properties changed since last time.
|
|
if (
|
|
aTotalBytes != -1 &&
|
|
(!this.hasProgress || this.totalBytes != aTotalBytes)
|
|
) {
|
|
this.hasProgress = true;
|
|
this.totalBytes = aTotalBytes;
|
|
changeMade = true;
|
|
}
|
|
|
|
// Updating the progress and computing the speed require that enough time
|
|
// passed since the last update, or that we haven't started throttling yet.
|
|
let currentTimeMs = Date.now();
|
|
let intervalMs = currentTimeMs - this._lastProgressTimeMs;
|
|
if (intervalMs >= kProgressUpdateIntervalMs) {
|
|
// Don't compute the speed unless we started throttling notifications.
|
|
if (this._lastProgressTimeMs != 0) {
|
|
// Calculate the speed in bytes per second.
|
|
let rawSpeed =
|
|
((aCurrentBytes - this.currentBytes) / intervalMs) * 1000;
|
|
if (this.speed == 0) {
|
|
// When the previous speed is exactly zero instead of a fractional
|
|
// number, this can be considered the first element of the series.
|
|
this.speed = rawSpeed;
|
|
} else {
|
|
// Apply exponential smoothing, with a smoothing factor of 0.1.
|
|
this.speed = rawSpeed * 0.1 + this.speed * 0.9;
|
|
}
|
|
}
|
|
|
|
// Start throttling notifications only when we have actually received some
|
|
// bytes for the first time. The timing of the first part of the download
|
|
// is not reliable, due to possible latency in the initial notifications.
|
|
// This also allows automated tests to receive and verify the number of
|
|
// bytes initially transferred.
|
|
if (aCurrentBytes > 0) {
|
|
this._lastProgressTimeMs = currentTimeMs;
|
|
|
|
// Update the progress now that we don't need its previous value.
|
|
this.currentBytes = aCurrentBytes;
|
|
if (this.totalBytes > 0) {
|
|
this.progress = Math.floor(
|
|
(this.currentBytes / this.totalBytes) * 100
|
|
);
|
|
}
|
|
changeMade = true;
|
|
}
|
|
|
|
if (this.hasProgress && this.target && !this.target.partFileExists) {
|
|
this.target.refreshPartFileState();
|
|
}
|
|
}
|
|
|
|
if (changeMade) {
|
|
this._notifyChange();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns a static representation of the current object state.
|
|
*
|
|
* @return A JavaScript object that can be serialized to JSON.
|
|
*/
|
|
toSerializable() {
|
|
let serializable = {
|
|
source: this.source.toSerializable(),
|
|
target: this.target.toSerializable(),
|
|
};
|
|
|
|
let saver = this.saver.toSerializable();
|
|
if (!serializable.source || !saver) {
|
|
// If we are unable to serialize either the source or the saver,
|
|
// we won't persist the download.
|
|
return null;
|
|
}
|
|
|
|
// Simplify the representation for the most common saver type. If the saver
|
|
// is an object instead of a simple string, we can't simplify it because we
|
|
// need to persist all its properties, not only "type". This may happen for
|
|
// savers of type "copy" as well as other types.
|
|
if (saver !== "copy") {
|
|
serializable.saver = saver;
|
|
}
|
|
|
|
if (this.error) {
|
|
serializable.errorObj = this.error.toSerializable();
|
|
}
|
|
|
|
if (this.startTime) {
|
|
serializable.startTime = this.startTime.toJSON();
|
|
}
|
|
|
|
// These are serialized unless they are false, null, or empty strings.
|
|
for (let property of kPlainSerializableDownloadProperties) {
|
|
if (this[property]) {
|
|
serializable[property] = this[property];
|
|
}
|
|
}
|
|
|
|
serializeUnknownProperties(this, serializable);
|
|
|
|
return serializable;
|
|
},
|
|
|
|
/**
|
|
* Returns a value that changes only when one of the properties of a Download
|
|
* object that should be saved into a file also change. This excludes
|
|
* properties whose value doesn't usually change during the download lifetime.
|
|
*
|
|
* This function is used to determine whether the download should be
|
|
* serialized after a property change notification has been received.
|
|
*
|
|
* @return String representing the relevant download state.
|
|
*/
|
|
getSerializationHash() {
|
|
// The "succeeded", "canceled", "error", and startTime properties are not
|
|
// taken into account because they all change before the "stopped" property
|
|
// changes, and are not altered in other cases.
|
|
return (
|
|
this.stopped +
|
|
"," +
|
|
this.totalBytes +
|
|
"," +
|
|
this.hasPartialData +
|
|
"," +
|
|
this.contentType
|
|
);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Defines which properties of the Download object are serializable.
|
|
*/
|
|
const kPlainSerializableDownloadProperties = [
|
|
"succeeded",
|
|
"canceled",
|
|
"totalBytes",
|
|
"hasPartialData",
|
|
"hasBlockedData",
|
|
"tryToKeepPartialData",
|
|
"launcherPath",
|
|
"launchWhenSucceeded",
|
|
"contentType",
|
|
"handleInternally",
|
|
"openDownloadsListOnStart",
|
|
];
|
|
|
|
/**
|
|
* Creates a new Download object from a serializable representation. This
|
|
* function is used by the createDownload method of Downloads.sys.mjs when a new
|
|
* Download object is requested, thus some properties may refer to live objects
|
|
* in place of their serializable representations.
|
|
*
|
|
* @param aSerializable
|
|
* An object with the following fields:
|
|
* {
|
|
* source: DownloadSource object, or its serializable representation.
|
|
* See DownloadSource.fromSerializable for details.
|
|
* target: DownloadTarget object, or its serializable representation.
|
|
* See DownloadTarget.fromSerializable for details.
|
|
* saver: Serializable representation of a DownloadSaver object. See
|
|
* DownloadSaver.fromSerializable for details. If omitted,
|
|
* defaults to "copy".
|
|
* }
|
|
*
|
|
* @return The newly created Download object.
|
|
*/
|
|
Download.fromSerializable = function (aSerializable) {
|
|
let download = new Download();
|
|
if (aSerializable.source instanceof DownloadSource) {
|
|
download.source = aSerializable.source;
|
|
} else {
|
|
download.source = DownloadSource.fromSerializable(aSerializable.source);
|
|
}
|
|
if (aSerializable.target instanceof DownloadTarget) {
|
|
download.target = aSerializable.target;
|
|
} else {
|
|
download.target = DownloadTarget.fromSerializable(aSerializable.target);
|
|
}
|
|
if ("saver" in aSerializable) {
|
|
download.saver = DownloadSaver.fromSerializable(aSerializable.saver);
|
|
} else {
|
|
download.saver = DownloadSaver.fromSerializable("copy");
|
|
}
|
|
download.saver.download = download;
|
|
|
|
if ("startTime" in aSerializable) {
|
|
let time = aSerializable.startTime.getTime
|
|
? aSerializable.startTime.getTime()
|
|
: aSerializable.startTime;
|
|
download.startTime = new Date(time);
|
|
}
|
|
|
|
// If 'errorObj' is present it will take precedence over the 'error' property.
|
|
// 'error' is a legacy property only containing message, which is insufficient
|
|
// to represent all of the error information.
|
|
//
|
|
// Instead of just replacing 'error' we use a new 'errorObj' so that previous
|
|
// versions will keep it as an unknown property.
|
|
if ("errorObj" in aSerializable) {
|
|
download.error = DownloadError.fromSerializable(aSerializable.errorObj);
|
|
} else if ("error" in aSerializable) {
|
|
download.error = aSerializable.error;
|
|
}
|
|
|
|
for (let property of kPlainSerializableDownloadProperties) {
|
|
if (property in aSerializable) {
|
|
download[property] = aSerializable[property];
|
|
}
|
|
}
|
|
|
|
deserializeUnknownProperties(
|
|
download,
|
|
aSerializable,
|
|
property =>
|
|
!kPlainSerializableDownloadProperties.includes(property) &&
|
|
property != "startTime" &&
|
|
property != "source" &&
|
|
property != "target" &&
|
|
property != "error" &&
|
|
property != "saver"
|
|
);
|
|
|
|
return download;
|
|
};
|
|
|
|
/**
|
|
* Represents the source of a download, for example a document or an URI.
|
|
*/
|
|
export var DownloadSource = function () {};
|
|
|
|
DownloadSource.prototype = {
|
|
/**
|
|
* String containing the URI for the download source.
|
|
*/
|
|
url: null,
|
|
|
|
/**
|
|
* String containing the original URL for the download source.
|
|
*/
|
|
originalUrl: null,
|
|
|
|
/**
|
|
* Indicates whether the download originated from a private window. This
|
|
* determines the context of the network request that is made to retrieve the
|
|
* resource.
|
|
*/
|
|
isPrivate: false,
|
|
|
|
/**
|
|
* Represents the referrerInfo of the download source, could be null for
|
|
* example if the download source is not HTTP.
|
|
*/
|
|
referrerInfo: null,
|
|
|
|
/**
|
|
* For downloads handled by the (default) DownloadCopySaver, this function
|
|
* can adjust the network channel before it is opened, for example to change
|
|
* the HTTP headers or to upload a stream as POST data.
|
|
*
|
|
* @note If this is defined this object will not be serializable, thus the
|
|
* Download object will not be persisted across sessions.
|
|
*
|
|
* @param aChannel
|
|
* The nsIChannel to be adjusted.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the channel has been adjusted and can be opened.
|
|
* @rejects JavaScript exception that will cause the download to fail.
|
|
*/
|
|
adjustChannel: null,
|
|
|
|
/**
|
|
* For downloads handled by the (default) DownloadCopySaver, this function
|
|
* will determine, if provided, if a download can progress or has to be
|
|
* cancelled based on the HTTP status code of the network channel.
|
|
*
|
|
* @note If this is defined this object will not be serializable, thus the
|
|
* Download object will not be persisted across sessions.
|
|
*
|
|
* @param aDownload
|
|
* The download asking.
|
|
* @param aStatus
|
|
* The HTTP status in question
|
|
*
|
|
* @return {Boolean} Download can progress
|
|
*/
|
|
allowHttpStatus: null,
|
|
|
|
/**
|
|
* Represents the loadingPrincipal of the download source,
|
|
* could be null, in which case the system principal is used instead.
|
|
*/
|
|
loadingPrincipal: null,
|
|
|
|
/**
|
|
* Represents the cookieJarSettings of the download source, could be null if
|
|
* the download source is not from a document.
|
|
*/
|
|
cookieJarSettings: null,
|
|
|
|
/**
|
|
* Represents the authentication header of the download source, could be null if
|
|
* the download source had no authentication header.
|
|
*/
|
|
authHeader: null,
|
|
/**
|
|
* Returns a static representation of the current object state.
|
|
*
|
|
* @return A JavaScript object that can be serialized to JSON.
|
|
*/
|
|
toSerializable() {
|
|
if (this.adjustChannel) {
|
|
// If the callback was used, we can't reproduce this across sessions.
|
|
return null;
|
|
}
|
|
|
|
if (this.allowHttpStatus) {
|
|
// If the callback was used, we can't reproduce this across sessions.
|
|
return null;
|
|
}
|
|
|
|
let serializable = { url: this.url };
|
|
if (this.isPrivate) {
|
|
serializable.isPrivate = true;
|
|
}
|
|
|
|
if (this.referrerInfo && isString(this.referrerInfo)) {
|
|
serializable.referrerInfo = this.referrerInfo;
|
|
} else if (this.referrerInfo) {
|
|
serializable.referrerInfo = lazy.E10SUtils.serializeReferrerInfo(
|
|
this.referrerInfo
|
|
);
|
|
}
|
|
|
|
if (this.loadingPrincipal) {
|
|
serializable.loadingPrincipal = isString(this.loadingPrincipal)
|
|
? this.loadingPrincipal
|
|
: lazy.E10SUtils.serializePrincipal(this.loadingPrincipal);
|
|
}
|
|
|
|
if (this.cookieJarSettings) {
|
|
serializable.cookieJarSettings = isString(this.cookieJarSettings)
|
|
? this.cookieJarSettings
|
|
: lazy.E10SUtils.serializeCookieJarSettings(this.cookieJarSettings);
|
|
}
|
|
|
|
serializeUnknownProperties(this, serializable);
|
|
|
|
// Simplify the representation if we don't have other details.
|
|
if (Object.keys(serializable).length === 1) {
|
|
// serializable's only key is "url", just return the URL as a string.
|
|
return this.url;
|
|
}
|
|
return serializable;
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Creates a new DownloadSource object from its serializable representation.
|
|
*
|
|
* @param aSerializable
|
|
* Serializable representation of a DownloadSource object. This may be a
|
|
* string containing the URI for the download source, an nsIURI, 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.
|
|
* referrerInfo: represents the referrerInfo of the download source.
|
|
* Can be omitted or null for example if the download
|
|
* source is not HTTP.
|
|
* cookieJarSettings: represents the cookieJarSettings of the download
|
|
* source. Can be omitted or null if the download
|
|
* source is not from a document.
|
|
* adjustChannel: For downloads handled by (default) DownloadCopySaver,
|
|
* this function can adjust the network channel before
|
|
* it is opened, for example to change the HTTP headers
|
|
* or to upload a stream as POST data. Optional.
|
|
* allowHttpStatus: For downloads handled by the (default)
|
|
* DownloadCopySaver, this function will determine, if
|
|
* provided, if a download can progress or has to be
|
|
* cancelled based on the HTTP status code of the
|
|
* network channel.
|
|
* }
|
|
*
|
|
* @return The newly created DownloadSource object.
|
|
*/
|
|
DownloadSource.fromSerializable = function (aSerializable) {
|
|
let source = new DownloadSource();
|
|
if (isString(aSerializable)) {
|
|
// Convert String objects to primitive strings at this point.
|
|
source.url = aSerializable.toString();
|
|
} else if (aSerializable instanceof Ci.nsIURI) {
|
|
source.url = aSerializable.spec;
|
|
} else {
|
|
// Convert String objects to primitive strings at this point.
|
|
source.url = aSerializable.url.toString();
|
|
for (let propName of ["isPrivate", "userContextId", "browsingContextId"]) {
|
|
if (propName in aSerializable) {
|
|
source[propName] = aSerializable[propName];
|
|
}
|
|
}
|
|
if ("originalUrl" in aSerializable) {
|
|
source.originalUrl = aSerializable.originalUrl;
|
|
}
|
|
if ("referrerInfo" in aSerializable) {
|
|
// Quick pass, pass directly nsIReferrerInfo, we don't need to serialize
|
|
// and deserialize
|
|
if (aSerializable.referrerInfo instanceof Ci.nsIReferrerInfo) {
|
|
source.referrerInfo = aSerializable.referrerInfo;
|
|
} else {
|
|
source.referrerInfo = lazy.E10SUtils.deserializeReferrerInfo(
|
|
aSerializable.referrerInfo
|
|
);
|
|
}
|
|
}
|
|
if ("loadingPrincipal" in aSerializable) {
|
|
// Quick pass, pass directly nsIPrincipal, we don't need to serialize
|
|
// and deserialize
|
|
if (aSerializable.loadingPrincipal instanceof Ci.nsIPrincipal) {
|
|
source.loadingPrincipal = aSerializable.loadingPrincipal;
|
|
} else {
|
|
source.loadingPrincipal = lazy.E10SUtils.deserializePrincipal(
|
|
aSerializable.loadingPrincipal
|
|
);
|
|
}
|
|
}
|
|
if ("adjustChannel" in aSerializable) {
|
|
source.adjustChannel = aSerializable.adjustChannel;
|
|
}
|
|
|
|
if ("allowHttpStatus" in aSerializable) {
|
|
source.allowHttpStatus = aSerializable.allowHttpStatus;
|
|
}
|
|
|
|
if ("cookieJarSettings" in aSerializable) {
|
|
if (aSerializable.cookieJarSettings instanceof Ci.nsICookieJarSettings) {
|
|
source.cookieJarSettings = aSerializable.cookieJarSettings;
|
|
} else {
|
|
source.cookieJarSettings = lazy.E10SUtils.deserializeCookieJarSettings(
|
|
aSerializable.cookieJarSettings
|
|
);
|
|
}
|
|
}
|
|
|
|
if ("authHeader" in aSerializable) {
|
|
source.authHeader = aSerializable.authHeader;
|
|
}
|
|
|
|
deserializeUnknownProperties(
|
|
source,
|
|
aSerializable,
|
|
property =>
|
|
property != "url" &&
|
|
property != "originalUrl" &&
|
|
property != "isPrivate" &&
|
|
property != "referrerInfo" &&
|
|
property != "cookieJarSettings" &&
|
|
property != "authHeader"
|
|
);
|
|
}
|
|
|
|
return source;
|
|
};
|
|
|
|
/**
|
|
* Represents the target of a download, for example a file in the global
|
|
* downloads directory, or a file in the system temporary directory.
|
|
*/
|
|
export var DownloadTarget = function () {};
|
|
|
|
DownloadTarget.prototype = {
|
|
/**
|
|
* String containing the path of the target file.
|
|
*/
|
|
path: null,
|
|
|
|
/**
|
|
* String containing the path of the ".part" file containing the data
|
|
* downloaded so far, or null to disable the use of a ".part" file to keep
|
|
* partially downloaded data.
|
|
*/
|
|
partFilePath: null,
|
|
|
|
/**
|
|
* Indicates whether the target file exists.
|
|
*
|
|
* This is a dynamic property updated when the download finishes or when the
|
|
* "refresh" method of the Download object is called. It can be used by the
|
|
* front-end to reduce I/O compared to checking the target file directly.
|
|
*/
|
|
exists: false,
|
|
|
|
/**
|
|
* Indicates whether the part file exists. Like `exists`, this is updated
|
|
* dynamically to reduce I/O compared to checking the target file directly.
|
|
*/
|
|
partFileExists: false,
|
|
|
|
/**
|
|
* Size in bytes of the target file, or zero if the download has not finished.
|
|
*
|
|
* Even if the target file does not exist anymore, this property may still
|
|
* have a value taken from the download metadata. If the metadata has never
|
|
* been available in this session and the size cannot be obtained from the
|
|
* file because it has already been deleted, this property will be zero.
|
|
*
|
|
* For single-file downloads, this property will always match the actual file
|
|
* size on disk, while the totalBytes property of the Download object, when
|
|
* available, may represent the size of the encoded data instead.
|
|
*
|
|
* For downloads involving multiple files, like complete web pages saved to
|
|
* disk, the meaning of this value is undefined. It currently matches the size
|
|
* of the main file only rather than the sum of all the written data.
|
|
*
|
|
* This is a dynamic property updated when the download finishes or when the
|
|
* "refresh" method of the Download object is called. It can be used by the
|
|
* front-end to reduce I/O compared to checking the target file directly.
|
|
*/
|
|
size: 0,
|
|
|
|
/**
|
|
* Sets the "exists" and "size" properties based on the actual file on disk.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the operation has finished successfully.
|
|
* @rejects JavaScript exception.
|
|
*/
|
|
async refresh() {
|
|
try {
|
|
this.size = (await IOUtils.stat(this.path)).size;
|
|
this.exists = true;
|
|
} catch (ex) {
|
|
// Report any error not caused by the file not being there. In any case,
|
|
// the size of the download is not updated and the known value is kept.
|
|
if (ex.name != "NotFoundError") {
|
|
console.error(ex);
|
|
}
|
|
this.exists = false;
|
|
}
|
|
this.refreshPartFileState();
|
|
},
|
|
|
|
async refreshPartFileState() {
|
|
if (!this.partFilePath) {
|
|
this.partFileExists = false;
|
|
return;
|
|
}
|
|
try {
|
|
this.partFileExists = (await IOUtils.stat(this.partFilePath)).size > 0;
|
|
} catch (ex) {
|
|
if (ex.name != "NotFoundError") {
|
|
console.error(ex);
|
|
}
|
|
this.partFileExists = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns a static representation of the current object state.
|
|
*
|
|
* @return A JavaScript object that can be serialized to JSON.
|
|
*/
|
|
toSerializable() {
|
|
// Simplify the representation if we don't have other details.
|
|
if (!this.partFilePath && !this._unknownProperties) {
|
|
return this.path;
|
|
}
|
|
|
|
let serializable = { path: this.path, partFilePath: this.partFilePath };
|
|
serializeUnknownProperties(this, serializable);
|
|
return serializable;
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Creates a new DownloadTarget object from its serializable representation.
|
|
*
|
|
* @param aSerializable
|
|
* Serializable representation of a DownloadTarget object. This may be a
|
|
* string containing the path of the target file, an nsIFile, or an
|
|
* object with the following properties:
|
|
* {
|
|
* path: String containing the path of the target file.
|
|
* partFilePath: optional string containing the part file path.
|
|
* }
|
|
*
|
|
* @return The newly created DownloadTarget object.
|
|
*/
|
|
DownloadTarget.fromSerializable = function (aSerializable) {
|
|
let target = new DownloadTarget();
|
|
if (isString(aSerializable)) {
|
|
// Convert String objects to primitive strings at this point.
|
|
target.path = aSerializable.toString();
|
|
} else if (aSerializable instanceof Ci.nsIFile) {
|
|
// Read the "path" property of nsIFile after checking the object type.
|
|
target.path = aSerializable.path;
|
|
} else {
|
|
// Read the "path" property of the serializable DownloadTarget
|
|
// representation, converting String objects to primitive strings.
|
|
target.path = aSerializable.path.toString();
|
|
if ("partFilePath" in aSerializable) {
|
|
target.partFilePath = aSerializable.partFilePath;
|
|
}
|
|
|
|
deserializeUnknownProperties(
|
|
target,
|
|
aSerializable,
|
|
property => property != "path" && property != "partFilePath"
|
|
);
|
|
}
|
|
return target;
|
|
};
|
|
|
|
/**
|
|
* Provides detailed information about a download failure.
|
|
*
|
|
* @param aProperties
|
|
* Object which may contain any of the following properties:
|
|
* {
|
|
* result: Result error code, defaulting to Cr.NS_ERROR_FAILURE
|
|
* message: String error message to be displayed in the console, or
|
|
* null to use the message associated with the result code.
|
|
* inferCause: If true, attempts to determine if the cause of the
|
|
* download is a network failure or a local file failure,
|
|
* based on a set of known values of the result code.
|
|
* This is useful when the error is received by a
|
|
* component that handles both aspects of the download.
|
|
* localizedReason: If available, is a localized reason for the error
|
|
* that can be directly displayed in the UI.
|
|
* }
|
|
* The properties object may also contain any of the DownloadError's
|
|
* because properties, which will be set accordingly in the error object.
|
|
*/
|
|
export var DownloadError = function (aProperties) {
|
|
const NS_ERROR_MODULE_BASE_OFFSET = 0x45;
|
|
const NS_ERROR_MODULE_NETWORK = 6;
|
|
const NS_ERROR_MODULE_FILES = 13;
|
|
|
|
// Set the error name used by the Error object prototype first.
|
|
this.name = "DownloadError";
|
|
this.result = aProperties.result || Cr.NS_ERROR_FAILURE;
|
|
this.localizedReason = aProperties.localizedReason;
|
|
if (aProperties.message) {
|
|
this.message = aProperties.message;
|
|
} else if (
|
|
aProperties.becauseBlocked ||
|
|
aProperties.becauseBlockedByParentalControls ||
|
|
aProperties.becauseBlockedByReputationCheck
|
|
) {
|
|
this.message = "Download blocked.";
|
|
} else {
|
|
let exception = new Components.Exception("", this.result);
|
|
this.message = exception.toString();
|
|
}
|
|
if (aProperties.inferCause) {
|
|
let module =
|
|
((this.result & 0x7fff0000) >> 16) - NS_ERROR_MODULE_BASE_OFFSET;
|
|
this.becauseSourceFailed = module == NS_ERROR_MODULE_NETWORK;
|
|
this.becauseTargetFailed = module == NS_ERROR_MODULE_FILES;
|
|
} else {
|
|
if (aProperties.becauseSourceFailed) {
|
|
this.becauseSourceFailed = true;
|
|
}
|
|
if (aProperties.becauseTargetFailed) {
|
|
this.becauseTargetFailed = true;
|
|
}
|
|
}
|
|
|
|
if (aProperties.becauseBlockedByParentalControls) {
|
|
this.becauseBlocked = true;
|
|
this.becauseBlockedByParentalControls = true;
|
|
} else if (aProperties.becauseBlockedByReputationCheck) {
|
|
this.becauseBlocked = true;
|
|
this.becauseBlockedByReputationCheck = true;
|
|
this.reputationCheckVerdict = aProperties.reputationCheckVerdict || "";
|
|
} else if (aProperties.becauseBlocked) {
|
|
this.becauseBlocked = true;
|
|
}
|
|
|
|
if (aProperties.innerException) {
|
|
this.innerException = aProperties.innerException;
|
|
}
|
|
|
|
this.stack = new Error().stack;
|
|
};
|
|
|
|
/**
|
|
* These constants are used by the reputationCheckVerdict property and indicate
|
|
* the detailed reason why a download is blocked.
|
|
*
|
|
* @note These values should not be changed because they can be serialized.
|
|
*/
|
|
DownloadError.BLOCK_VERDICT_MALWARE = "Malware";
|
|
DownloadError.BLOCK_VERDICT_POTENTIALLY_UNWANTED = "PotentiallyUnwanted";
|
|
DownloadError.BLOCK_VERDICT_INSECURE = "Insecure";
|
|
DownloadError.BLOCK_VERDICT_UNCOMMON = "Uncommon";
|
|
DownloadError.BLOCK_VERDICT_DOWNLOAD_SPAM = "DownloadSpam";
|
|
|
|
DownloadError.prototype = {
|
|
/**
|
|
* The result code associated with this error.
|
|
*/
|
|
result: false,
|
|
|
|
/**
|
|
* Indicates an error occurred while reading from the remote location.
|
|
*/
|
|
becauseSourceFailed: false,
|
|
|
|
/**
|
|
* Indicates an error occurred while writing to the local target.
|
|
*/
|
|
becauseTargetFailed: false,
|
|
|
|
/**
|
|
* Indicates the download failed because it was blocked. If the reason for
|
|
* blocking is known, the corresponding property will be also set.
|
|
*/
|
|
becauseBlocked: false,
|
|
|
|
/**
|
|
* Indicates the download was blocked because downloads are globally
|
|
* disallowed by the Parental Controls or Family Safety features on Windows.
|
|
*/
|
|
becauseBlockedByParentalControls: false,
|
|
|
|
/**
|
|
* Indicates the download was blocked because it failed the reputation check
|
|
* and may be malware.
|
|
*/
|
|
becauseBlockedByReputationCheck: false,
|
|
|
|
/**
|
|
* If becauseBlockedByReputationCheck is true, indicates the detailed reason
|
|
* why the download was blocked, according to the "BLOCK_VERDICT_" constants.
|
|
*
|
|
* If the download was not blocked or the reason for the block is unknown,
|
|
* this will be an empty string.
|
|
*/
|
|
reputationCheckVerdict: "",
|
|
|
|
/**
|
|
* If this DownloadError was caused by an exception this property will
|
|
* contain the original exception. This will not be serialized when saving
|
|
* to the store.
|
|
*/
|
|
innerException: null,
|
|
|
|
/**
|
|
* Returns a static representation of the current object state.
|
|
*
|
|
* @return A JavaScript object that can be serialized to JSON.
|
|
*/
|
|
toSerializable() {
|
|
let serializable = {
|
|
result: this.result,
|
|
localizedReason: this.localizedReason,
|
|
message: this.message,
|
|
becauseSourceFailed: this.becauseSourceFailed,
|
|
becauseTargetFailed: this.becauseTargetFailed,
|
|
becauseBlocked: this.becauseBlocked,
|
|
becauseBlockedByParentalControls: this.becauseBlockedByParentalControls,
|
|
becauseBlockedByReputationCheck: this.becauseBlockedByReputationCheck,
|
|
reputationCheckVerdict: this.reputationCheckVerdict,
|
|
};
|
|
|
|
serializeUnknownProperties(this, serializable);
|
|
return serializable;
|
|
},
|
|
};
|
|
Object.setPrototypeOf(DownloadError.prototype, Error.prototype);
|
|
|
|
/**
|
|
* Creates a new DownloadError object from its serializable representation.
|
|
*
|
|
* @param aSerializable
|
|
* Serializable representation of a DownloadError object.
|
|
*
|
|
* @return The newly created DownloadError object.
|
|
*/
|
|
DownloadError.fromSerializable = function (aSerializable) {
|
|
let e = new DownloadError(aSerializable);
|
|
deserializeUnknownProperties(
|
|
e,
|
|
aSerializable,
|
|
property =>
|
|
property != "result" &&
|
|
property != "message" &&
|
|
property != "becauseSourceFailed" &&
|
|
property != "becauseTargetFailed" &&
|
|
property != "becauseBlocked" &&
|
|
property != "becauseBlockedByParentalControls" &&
|
|
property != "becauseBlockedByReputationCheck" &&
|
|
property != "reputationCheckVerdict"
|
|
);
|
|
|
|
return e;
|
|
};
|
|
|
|
/**
|
|
* Template for an object that actually transfers the data for the download.
|
|
*/
|
|
export var DownloadSaver = function () {};
|
|
|
|
DownloadSaver.prototype = {
|
|
/**
|
|
* Download object for raising notifications and reading properties.
|
|
*
|
|
* If the tryToKeepPartialData property of the download object is false, the
|
|
* saver should never try to keep partially downloaded data if the download
|
|
* fails.
|
|
*/
|
|
download: null,
|
|
|
|
/**
|
|
* Executes the download.
|
|
*
|
|
* @param aSetProgressBytesFn
|
|
* This function may be called by the saver to report progress. It
|
|
* takes three arguments: the first is the number of bytes transferred
|
|
* until now, the second is the total number of bytes to be
|
|
* transferred (or -1 if unknown), the third indicates whether the
|
|
* partially downloaded data can be used when restarting the download
|
|
* if it fails or is canceled.
|
|
* @param aSetPropertiesFn
|
|
* This function may be called by the saver to report information
|
|
* about new download properties discovered by the saver during the
|
|
* download process. It takes an object where the keys represents
|
|
* the names of the properties to set, and the value represents the
|
|
* value to set.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the download has finished successfully.
|
|
* @rejects JavaScript exception if the download failed.
|
|
*/
|
|
async execute() {
|
|
throw new Error("Not implemented.");
|
|
},
|
|
|
|
/**
|
|
* Cancels the download.
|
|
*/
|
|
cancel: function DS_cancel() {
|
|
throw new Error("Not implemented.");
|
|
},
|
|
|
|
/**
|
|
* Removes any target file placeholder and any partial data kept as part of a
|
|
* canceled, failed, or temporarily blocked download.
|
|
*
|
|
* This method is never called until the promise returned by "execute" is
|
|
* either resolved or rejected, and the "execute" method is not called again
|
|
* until the promise returned by this method is resolved or rejected.
|
|
*
|
|
* @param canRemoveFinalTarget
|
|
* True if can remove target file regardless of it being a placeholder.
|
|
* @return {Promise}
|
|
* @resolves When the operation has finished successfully.
|
|
* @rejects Never.
|
|
*/
|
|
async removeData() {},
|
|
|
|
/**
|
|
* This can be called by the saver implementation when the download is already
|
|
* started, to add it to the browsing history. This method has no effect if
|
|
* the download is private.
|
|
*/
|
|
addToHistory() {
|
|
if (AppConstants.MOZ_PLACES) {
|
|
lazy.DownloadHistory.addDownloadToHistory(this.download).catch(
|
|
console.error
|
|
);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns a static representation of the current object state.
|
|
*
|
|
* @return A JavaScript object that can be serialized to JSON.
|
|
*/
|
|
toSerializable() {
|
|
throw new Error("Not implemented.");
|
|
},
|
|
|
|
/**
|
|
* Returns the SHA-256 hash of the downloaded file, if it exists.
|
|
*/
|
|
getSha256Hash() {
|
|
throw new Error("Not implemented.");
|
|
},
|
|
|
|
getSignatureInfo() {
|
|
throw new Error("Not implemented.");
|
|
},
|
|
}; // DownloadSaver
|
|
|
|
/**
|
|
* Creates a new DownloadSaver object from its serializable representation.
|
|
*
|
|
* @param aSerializable
|
|
* Serializable representation of a DownloadSaver object. If no initial
|
|
* state information for the saver object is needed, can be a string
|
|
* representing the class of the download operation, for example "copy".
|
|
*
|
|
* @return The newly created DownloadSaver object.
|
|
*/
|
|
DownloadSaver.fromSerializable = function (aSerializable) {
|
|
let serializable = isString(aSerializable)
|
|
? { type: aSerializable }
|
|
: aSerializable;
|
|
let saver;
|
|
switch (serializable.type) {
|
|
case "copy":
|
|
saver = DownloadCopySaver.fromSerializable(serializable);
|
|
break;
|
|
case "legacy":
|
|
saver = DownloadLegacySaver.fromSerializable(serializable);
|
|
break;
|
|
default:
|
|
throw new Error("Unrecoginzed download saver type.");
|
|
}
|
|
return saver;
|
|
};
|
|
|
|
/**
|
|
* Saver object that simply copies the entire source file to the target.
|
|
*/
|
|
export var DownloadCopySaver = function () {};
|
|
|
|
DownloadCopySaver.prototype = {
|
|
/**
|
|
* BackgroundFileSaver object currently handling the download.
|
|
*/
|
|
_backgroundFileSaver: null,
|
|
|
|
/**
|
|
* Indicates whether the "cancel" method has been called. This is used to
|
|
* prevent the request from starting in case the operation is canceled before
|
|
* the BackgroundFileSaver instance has been created.
|
|
*/
|
|
_canceled: false,
|
|
|
|
/**
|
|
* Save the SHA-256 hash in raw bytes of the downloaded file. This is null
|
|
* unless BackgroundFileSaver has successfully completed saving the file.
|
|
*/
|
|
_sha256Hash: null,
|
|
|
|
/**
|
|
* Save the signature info as an Array of Array of raw bytes of nsIX509Cert
|
|
* if the file is signed. This is empty if the file is unsigned, and null
|
|
* unless BackgroundFileSaver has successfully completed saving the file.
|
|
*/
|
|
_signatureInfo: null,
|
|
|
|
/**
|
|
* Save the redirects chain as an nsIArray of nsIPrincipal.
|
|
*/
|
|
_redirects: null,
|
|
|
|
/**
|
|
* True if the associated download has already been added to browsing history.
|
|
*/
|
|
alreadyAddedToHistory: false,
|
|
|
|
/**
|
|
* String corresponding to the entityID property of the nsIResumableChannel
|
|
* used to execute the download, or null if the channel was not resumable or
|
|
* the saver was instructed not to keep partially downloaded data.
|
|
*/
|
|
entityID: null,
|
|
|
|
/**
|
|
* Implements "DownloadSaver.execute".
|
|
*/
|
|
async execute(aSetProgressBytesFn, aSetPropertiesFn) {
|
|
this._canceled = false;
|
|
|
|
let download = this.download;
|
|
let targetPath = download.target.path;
|
|
let partFilePath = download.target.partFilePath;
|
|
let keepPartialData = download.tryToKeepPartialData;
|
|
|
|
// Add the download to history the first time it is started in this
|
|
// session. If the download is restarted in a different session, a new
|
|
// history visit will be added. We do this just to avoid the complexity
|
|
// of serializing this state between sessions, since adding a new visit
|
|
// does not have any noticeable side effect.
|
|
if (!this.alreadyAddedToHistory) {
|
|
this.addToHistory();
|
|
this.alreadyAddedToHistory = true;
|
|
}
|
|
|
|
// To reduce the chance that other downloads reuse the same final target
|
|
// file name, we should create a placeholder as soon as possible, before
|
|
// starting the network request. The placeholder is also required in case
|
|
// we are using a ".part" file instead of the final target while the
|
|
// download is in progress.
|
|
try {
|
|
// If the file already exists, don't delete its contents yet.
|
|
await IOUtils.writeUTF8(targetPath, "", { mode: "appendOrCreate" });
|
|
} catch (ex) {
|
|
if (!DOMException.isInstance(ex)) {
|
|
throw ex;
|
|
}
|
|
// Throw a DownloadError indicating that the operation failed because of
|
|
// the target file. We cannot translate this into a specific result
|
|
// code, but we preserve the original message.
|
|
let error = new DownloadError({ message: ex.message });
|
|
error.becauseTargetFailed = true;
|
|
throw error;
|
|
}
|
|
|
|
let deferSaveComplete = Promise.withResolvers();
|
|
|
|
if (this._canceled) {
|
|
// Don't create the BackgroundFileSaver object if we have been
|
|
// canceled meanwhile.
|
|
throw new DownloadError({ message: "Saver canceled." });
|
|
}
|
|
|
|
// Create the object that will save the file in a background thread.
|
|
let backgroundFileSaver = new BackgroundFileSaverStreamListener();
|
|
backgroundFileSaver.QueryInterface(Ci.nsIStreamListener);
|
|
|
|
try {
|
|
// When the operation completes, reflect the status in the promise
|
|
// returned by this download execution function.
|
|
backgroundFileSaver.observer = {
|
|
onTargetChange() {},
|
|
onSaveComplete: (aSaver, aStatus) => {
|
|
// Send notifications now that we can restart if needed.
|
|
if (Components.isSuccessCode(aStatus)) {
|
|
// Save the hash before freeing backgroundFileSaver.
|
|
this._sha256Hash = aSaver.sha256Hash;
|
|
this._signatureInfo = aSaver.signatureInfo;
|
|
this._redirects = aSaver.redirects;
|
|
deferSaveComplete.resolve();
|
|
} else {
|
|
// Infer the origin of the error from the failure code, because
|
|
// BackgroundFileSaver does not provide more specific data.
|
|
let properties = { result: aStatus, inferCause: true };
|
|
deferSaveComplete.reject(new DownloadError(properties));
|
|
}
|
|
// Free the reference cycle, to release resources earlier.
|
|
backgroundFileSaver.observer = null;
|
|
this._backgroundFileSaver = null;
|
|
},
|
|
};
|
|
|
|
// If we have data that we can use to resume the download from where
|
|
// it stopped, try to use it.
|
|
let resumeAttempted = false;
|
|
let resumeFromBytes = 0;
|
|
|
|
const notificationCallbacks = {
|
|
QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
|
|
getInterface: ChromeUtils.generateQI(["nsIProgressEventSink"]),
|
|
onProgress: function DCSE_onProgress(
|
|
aRequest,
|
|
aProgress,
|
|
aProgressMax
|
|
) {
|
|
let currentBytes = resumeFromBytes + aProgress;
|
|
let totalBytes =
|
|
aProgressMax == -1 ? -1 : resumeFromBytes + aProgressMax;
|
|
aSetProgressBytesFn(
|
|
currentBytes,
|
|
totalBytes,
|
|
aProgress > 0 && partFilePath && keepPartialData
|
|
);
|
|
},
|
|
onStatus() {},
|
|
};
|
|
|
|
const streamListener = {
|
|
onStartRequest: function (aRequest) {
|
|
backgroundFileSaver.onStartRequest(aRequest);
|
|
|
|
if (aRequest instanceof Ci.nsIHttpChannel) {
|
|
// Check if the request's response has been blocked by Windows
|
|
// Parental Controls with an HTTP 450 error code.
|
|
if (aRequest.responseStatus == 450) {
|
|
// Set a flag that can be retrieved later when handling the
|
|
// cancellation so that the proper error can be thrown.
|
|
this.download._blockedByParentalControls = true;
|
|
aRequest.cancel(Cr.NS_BINDING_ABORTED);
|
|
return;
|
|
}
|
|
|
|
// Check back with the initiator if we should allow a certain
|
|
// HTTP code. By default, we'll just save error pages too,
|
|
// however a consumer down the line, such as the WebExtensions
|
|
// downloads API might want to handle this differently.
|
|
if (
|
|
download.source.allowHttpStatus &&
|
|
!download.source.allowHttpStatus(
|
|
download,
|
|
aRequest.responseStatus
|
|
)
|
|
) {
|
|
aRequest.cancel(Cr.NS_BINDING_ABORTED);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (aRequest instanceof Ci.nsIChannel) {
|
|
aSetPropertiesFn({ contentType: aRequest.contentType });
|
|
|
|
// Ensure we report the value of "Content-Length", if available,
|
|
// even if the download doesn't generate any progress events
|
|
// later.
|
|
if (aRequest.contentLength >= 0) {
|
|
aSetProgressBytesFn(0, aRequest.contentLength);
|
|
}
|
|
}
|
|
|
|
// If the URL we are downloading from includes a file extension
|
|
// that matches the "Content-Encoding" header, for example ".gz"
|
|
// with a "gzip" encoding, we should save the file in its encoded
|
|
// form. In all other cases, we decode the body while saving.
|
|
if (
|
|
aRequest instanceof Ci.nsIEncodedChannel &&
|
|
aRequest.contentEncodings
|
|
) {
|
|
let uri = aRequest.URI;
|
|
if (uri instanceof Ci.nsIURL && uri.fileExtension) {
|
|
// Only the first, outermost encoding is considered.
|
|
let encoding = aRequest.contentEncodings.getNext();
|
|
if (encoding) {
|
|
aRequest.applyConversion =
|
|
lazy.gExternalHelperAppService.applyDecodingForExtension(
|
|
uri.fileExtension,
|
|
encoding
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (keepPartialData) {
|
|
// If the source is not resumable, don't keep partial data even
|
|
// if we were asked to try and do it.
|
|
if (aRequest instanceof Ci.nsIResumableChannel) {
|
|
try {
|
|
// If reading the ID succeeds, the source is resumable.
|
|
this.entityID = aRequest.entityID;
|
|
} catch (ex) {
|
|
if (
|
|
!(ex instanceof Components.Exception) ||
|
|
ex.result != Cr.NS_ERROR_NOT_RESUMABLE
|
|
) {
|
|
throw ex;
|
|
}
|
|
keepPartialData = false;
|
|
}
|
|
} else {
|
|
keepPartialData = false;
|
|
}
|
|
}
|
|
|
|
// Enable hashing and signature verification before setting the
|
|
// target.
|
|
backgroundFileSaver.enableSha256();
|
|
backgroundFileSaver.enableSignatureInfo();
|
|
if (partFilePath) {
|
|
// If we actually resumed a request, append to the partial data.
|
|
if (resumeAttempted) {
|
|
// TODO: Handle Cr.NS_ERROR_ENTITY_CHANGED
|
|
backgroundFileSaver.enableAppend();
|
|
}
|
|
|
|
// Use a part file, determining if we should keep it on failure.
|
|
backgroundFileSaver.setTarget(
|
|
new lazy.FileUtils.File(partFilePath),
|
|
keepPartialData
|
|
);
|
|
} else {
|
|
// Set the final target file, and delete it on failure.
|
|
backgroundFileSaver.setTarget(
|
|
new lazy.FileUtils.File(targetPath),
|
|
false
|
|
);
|
|
}
|
|
}.bind(this),
|
|
|
|
onStopRequest(aRequest, aStatusCode) {
|
|
try {
|
|
backgroundFileSaver.onStopRequest(aRequest, aStatusCode);
|
|
} finally {
|
|
// If the data transfer completed successfully, indicate to the
|
|
// background file saver that the operation can finish. If the
|
|
// data transfer failed, the saver has been already stopped.
|
|
if (Components.isSuccessCode(aStatusCode)) {
|
|
backgroundFileSaver.finish(Cr.NS_OK);
|
|
}
|
|
}
|
|
},
|
|
|
|
onDataAvailable: (aRequest, aInputStream, aOffset, aCount) => {
|
|
// Check if the download have been canceled in the mean time,
|
|
// and close the channel and return earlier, BackgroundFileSaver
|
|
// methods shouldn't be called anymore after `finish` was called
|
|
// on download cancellation.
|
|
if (this._canceled) {
|
|
aRequest.cancel(Cr.NS_BINDING_ABORTED);
|
|
return;
|
|
}
|
|
backgroundFileSaver.onDataAvailable(
|
|
aRequest,
|
|
aInputStream,
|
|
aOffset,
|
|
aCount
|
|
);
|
|
},
|
|
};
|
|
|
|
// Wrap the channel creation, to prevent the listener code from
|
|
// accidentally using the wrong channel.
|
|
// The channel that is created here is not necessarily the same channel
|
|
// that will eventually perform the actual download.
|
|
// When a HTTP redirect happens, the http backend will create a new
|
|
// channel, this initial channel will be abandoned, and its properties
|
|
// will either return incorrect data, or worse, will throw exceptions
|
|
// upon access.
|
|
const open = async () => {
|
|
// Create a channel from the source, and listen to progress
|
|
// notifications.
|
|
let channel;
|
|
if (download.source.loadingPrincipal) {
|
|
channel = lazy.NetUtil.newChannel({
|
|
uri: download.source.url,
|
|
contentPolicyType: Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD,
|
|
loadingPrincipal: download.source.loadingPrincipal,
|
|
// triggeringPrincipal must be the system principal to prevent the
|
|
// request from being mistaken as a third-party request.
|
|
triggeringPrincipal:
|
|
Services.scriptSecurityManager.getSystemPrincipal(),
|
|
securityFlags:
|
|
Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
|
|
});
|
|
} else {
|
|
channel = lazy.NetUtil.newChannel({
|
|
uri: download.source.url,
|
|
contentPolicyType: Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD,
|
|
loadUsingSystemPrincipal: true,
|
|
});
|
|
}
|
|
if (channel instanceof Ci.nsIPrivateBrowsingChannel) {
|
|
channel.setPrivate(download.source.isPrivate);
|
|
}
|
|
if (
|
|
channel instanceof Ci.nsIHttpChannel &&
|
|
download.source.referrerInfo
|
|
) {
|
|
channel.referrerInfo = download.source.referrerInfo;
|
|
// Stored computed referrerInfo;
|
|
download.source.referrerInfo = channel.referrerInfo;
|
|
}
|
|
if (
|
|
channel instanceof Ci.nsIHttpChannel &&
|
|
download.source.cookieJarSettings
|
|
) {
|
|
channel.loadInfo.cookieJarSettings =
|
|
download.source.cookieJarSettings;
|
|
}
|
|
if (
|
|
channel instanceof Ci.nsIHttpChannel &&
|
|
download.source.authHeader
|
|
) {
|
|
try {
|
|
channel.setRequestHeader(
|
|
"Authorization",
|
|
download.source.authHeader,
|
|
true
|
|
);
|
|
} catch (e) {}
|
|
}
|
|
|
|
if (download.source.userContextId) {
|
|
// Getters and setters only exist on originAttributes,
|
|
// so it has to be cloned, changed, and re-set
|
|
channel.loadInfo.originAttributes = {
|
|
...channel.loadInfo.originAttributes,
|
|
userContextId: download.source.userContextId,
|
|
};
|
|
}
|
|
|
|
// This makes the channel be corretly throttled during page loads
|
|
// and also prevents its caching.
|
|
if (channel instanceof Ci.nsIHttpChannelInternal) {
|
|
channel.channelIsForDownload = true;
|
|
|
|
// Include cookies even if cookieBehavior is BEHAVIOR_REJECT_FOREIGN.
|
|
channel.forceAllowThirdPartyCookie = true;
|
|
}
|
|
|
|
if (
|
|
channel instanceof Ci.nsIResumableChannel &&
|
|
this.entityID &&
|
|
partFilePath &&
|
|
keepPartialData
|
|
) {
|
|
try {
|
|
let stat = await IOUtils.stat(partFilePath);
|
|
channel.resumeAt(stat.size, this.entityID);
|
|
resumeAttempted = true;
|
|
resumeFromBytes = stat.size;
|
|
} catch (ex) {
|
|
if (ex.name != "NotFoundError") {
|
|
throw ex;
|
|
}
|
|
}
|
|
}
|
|
|
|
channel.notificationCallbacks = notificationCallbacks;
|
|
|
|
// If the callback was set, handle it now before opening the channel.
|
|
if (download.source.adjustChannel) {
|
|
await download.source.adjustChannel(channel);
|
|
}
|
|
channel.asyncOpen(streamListener);
|
|
};
|
|
|
|
// Kick off the download, creating and opening the channel.
|
|
await open();
|
|
|
|
// We should check if we have been canceled in the meantime, after
|
|
// all the previous asynchronous operations have been executed and
|
|
// just before we set the _backgroundFileSaver property.
|
|
if (this._canceled) {
|
|
throw new DownloadError({ message: "Saver canceled." });
|
|
}
|
|
|
|
// If the operation succeeded, store the object to allow cancellation.
|
|
this._backgroundFileSaver = backgroundFileSaver;
|
|
} catch (ex) {
|
|
// In case an error occurs while setting up the chain of objects for
|
|
// the download, ensure that we release the resources of the saver.
|
|
backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE);
|
|
// Since we're not going to handle deferSaveComplete.promise below,
|
|
// we need to make sure that the rejection is handled.
|
|
deferSaveComplete.promise.catch(() => {});
|
|
throw ex;
|
|
}
|
|
|
|
// We will wait on this promise in case no error occurred while setting
|
|
// up the chain of objects for the download.
|
|
await deferSaveComplete.promise;
|
|
|
|
await this._checkReputationAndMove(aSetPropertiesFn);
|
|
},
|
|
|
|
/**
|
|
* Perform the reputation check and cleanup the downloaded data if required.
|
|
* If the download passes the reputation check and is using a part file we
|
|
* will move it to the target path since reputation checking is the final
|
|
* step in the saver.
|
|
*
|
|
* @param aSetPropertiesFn
|
|
* Function provided to the "execute" method.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the reputation check and cleanup is complete.
|
|
* @rejects DownloadError if the download should be blocked.
|
|
*/
|
|
async _checkReputationAndMove(aSetPropertiesFn) {
|
|
let download = this.download;
|
|
let targetPath = this.download.target.path;
|
|
let partFilePath = this.download.target.partFilePath;
|
|
|
|
let { shouldBlock, verdict } =
|
|
await lazy.DownloadIntegration.shouldBlockForReputationCheck(download);
|
|
if (shouldBlock) {
|
|
Services.telemetry
|
|
.getKeyedHistogramById("DOWNLOADS_USER_ACTION_ON_BLOCKED_DOWNLOAD")
|
|
.add(verdict, 0);
|
|
|
|
let newProperties = { progress: 100, hasPartialData: false };
|
|
|
|
// We will remove the potentially dangerous file if instructed by
|
|
// DownloadIntegration. We will always remove the file when the
|
|
// download did not use a partial file path, meaning it
|
|
// currently has its final filename.
|
|
if (!lazy.DownloadIntegration.shouldKeepBlockedData() || !partFilePath) {
|
|
await this.removeData(!partFilePath);
|
|
} else {
|
|
newProperties.hasBlockedData = true;
|
|
}
|
|
|
|
aSetPropertiesFn(newProperties);
|
|
|
|
throw new DownloadError({
|
|
becauseBlockedByReputationCheck: true,
|
|
reputationCheckVerdict: verdict,
|
|
});
|
|
}
|
|
|
|
if (partFilePath) {
|
|
try {
|
|
await IOUtils.move(partFilePath, targetPath);
|
|
} catch (e) {
|
|
if (e.name === "NotAllowedError") {
|
|
// In case we cannot write to the target file
|
|
// retry with a new unique name
|
|
let uniquePath = lazy.DownloadPaths.createNiceUniqueFile(
|
|
new lazy.FileUtils.File(targetPath)
|
|
).path;
|
|
await IOUtils.move(partFilePath, uniquePath);
|
|
this.download.target.path = uniquePath;
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Implements "DownloadSaver.cancel".
|
|
*/
|
|
cancel: function DCS_cancel() {
|
|
this._canceled = true;
|
|
if (this._backgroundFileSaver) {
|
|
this._backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE);
|
|
this._backgroundFileSaver = null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Implements "DownloadSaver.removeData".
|
|
*/
|
|
async removeData(canRemoveFinalTarget = false) {
|
|
// Defined inline so removeData can be shared with DownloadLegacySaver.
|
|
async function _tryToRemoveFile(path) {
|
|
try {
|
|
await IOUtils.remove(path);
|
|
} catch (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. This
|
|
// is likely to happen when the component that executed the download has
|
|
// just deleted the target file itself.
|
|
if (!["NotFoundError", "NotAllowedError"].includes(ex.name)) {
|
|
console.error(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.download.target.partFilePath) {
|
|
await _tryToRemoveFile(this.download.target.partFilePath);
|
|
}
|
|
|
|
if (this.download.target.path) {
|
|
if (
|
|
canRemoveFinalTarget ||
|
|
(await isPlaceholder(this.download.target.path))
|
|
) {
|
|
await _tryToRemoveFile(this.download.target.path);
|
|
}
|
|
this.download.target.exists = false;
|
|
this.download.target.size = 0;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Implements "DownloadSaver.toSerializable".
|
|
*/
|
|
toSerializable() {
|
|
// Simplify the representation if we don't have other details.
|
|
if (!this.entityID && !this._unknownProperties) {
|
|
return "copy";
|
|
}
|
|
|
|
let serializable = { type: "copy", entityID: this.entityID };
|
|
serializeUnknownProperties(this, serializable);
|
|
return serializable;
|
|
},
|
|
|
|
/**
|
|
* Implements "DownloadSaver.getSha256Hash"
|
|
*/
|
|
getSha256Hash() {
|
|
return this._sha256Hash;
|
|
},
|
|
|
|
/*
|
|
* Implements DownloadSaver.getSignatureInfo.
|
|
*/
|
|
getSignatureInfo() {
|
|
return this._signatureInfo;
|
|
},
|
|
|
|
/*
|
|
* Implements DownloadSaver.getRedirects.
|
|
*/
|
|
getRedirects() {
|
|
return this._redirects;
|
|
},
|
|
};
|
|
Object.setPrototypeOf(DownloadCopySaver.prototype, DownloadSaver.prototype);
|
|
|
|
/**
|
|
* Creates a new DownloadCopySaver object, with its initial state derived from
|
|
* its serializable representation.
|
|
*
|
|
* @param aSerializable
|
|
* Serializable representation of a DownloadCopySaver object.
|
|
*
|
|
* @return The newly created DownloadCopySaver object.
|
|
*/
|
|
DownloadCopySaver.fromSerializable = function (aSerializable) {
|
|
let saver = new DownloadCopySaver();
|
|
if ("entityID" in aSerializable) {
|
|
saver.entityID = aSerializable.entityID;
|
|
}
|
|
|
|
deserializeUnknownProperties(
|
|
saver,
|
|
aSerializable,
|
|
property => property != "entityID" && property != "type"
|
|
);
|
|
|
|
return saver;
|
|
};
|
|
|
|
/**
|
|
* Saver object that integrates with the legacy nsITransfer interface.
|
|
*
|
|
* For more background on the process, see the DownloadLegacyTransfer object.
|
|
*/
|
|
export var DownloadLegacySaver = function () {
|
|
this.deferExecuted = Promise.withResolvers();
|
|
this.deferCanceled = Promise.withResolvers();
|
|
};
|
|
|
|
DownloadLegacySaver.prototype = {
|
|
/**
|
|
* Save the SHA-256 hash in raw bytes of the downloaded file. This may be
|
|
* null when nsExternalHelperAppService (and thus BackgroundFileSaver) is not
|
|
* invoked.
|
|
*/
|
|
_sha256Hash: null,
|
|
|
|
/**
|
|
* Save the signature info as an Array of Array of raw bytes of nsIX509Cert
|
|
* if the file is signed. This is empty if the file is unsigned, and null
|
|
* unless BackgroundFileSaver has successfully completed saving the file.
|
|
*/
|
|
_signatureInfo: null,
|
|
|
|
/**
|
|
* Save the redirect chain as an nsIArray of nsIPrincipal.
|
|
*/
|
|
_redirects: null,
|
|
|
|
/**
|
|
* nsIRequest object associated to the status and progress updates we
|
|
* received. This object is null before we receive the first status and
|
|
* progress update, and is also reset to null when the download is stopped.
|
|
*/
|
|
request: null,
|
|
|
|
/**
|
|
* This deferred object contains a promise that is resolved as soon as this
|
|
* download finishes successfully, and is rejected in case the download is
|
|
* canceled or receives a failure notification through nsITransfer.
|
|
*/
|
|
deferExecuted: null,
|
|
|
|
/**
|
|
* This deferred object contains a promise that is resolved if the download
|
|
* receives a cancellation request through the "cancel" method, and is never
|
|
* rejected. The nsITransfer implementation will register a handler that
|
|
* actually causes the download cancellation.
|
|
*/
|
|
deferCanceled: null,
|
|
|
|
/**
|
|
* This is populated with the value of the aSetProgressBytesFn argument of the
|
|
* "execute" method, and is null before the method is called.
|
|
*/
|
|
setProgressBytesFn: null,
|
|
|
|
/**
|
|
* Called by the nsITransfer implementation while the download progresses.
|
|
*
|
|
* @param aCurrentBytes
|
|
* Number of bytes transferred until now.
|
|
* @param aTotalBytes
|
|
* Total number of bytes to be transferred, or -1 if unknown.
|
|
*/
|
|
onProgressBytes: function DLS_onProgressBytes(aCurrentBytes, aTotalBytes) {
|
|
this.progressWasNotified = true;
|
|
|
|
// Ignore progress notifications until we are ready to process them.
|
|
if (!this.setProgressBytesFn) {
|
|
// Keep the data from the last progress notification that was received.
|
|
this.currentBytes = aCurrentBytes;
|
|
this.totalBytes = aTotalBytes;
|
|
return;
|
|
}
|
|
|
|
let hasPartFile = !!this.download.target.partFilePath;
|
|
|
|
this.setProgressBytesFn(
|
|
aCurrentBytes,
|
|
aTotalBytes,
|
|
aCurrentBytes > 0 && hasPartFile
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Whether the onProgressBytes function has been called at least once.
|
|
*/
|
|
progressWasNotified: false,
|
|
|
|
/**
|
|
* Called by the nsITransfer implementation when the request has started.
|
|
*
|
|
* @param aRequest
|
|
* nsIRequest associated to the status update.
|
|
*/
|
|
onTransferStarted(aRequest) {
|
|
// Store a reference to the request, used in some cases when handling
|
|
// completion, and also checked during the download by unit tests.
|
|
this.request = aRequest;
|
|
|
|
// Store the entity ID to use for resuming if required.
|
|
if (
|
|
this.download.tryToKeepPartialData &&
|
|
aRequest instanceof Ci.nsIResumableChannel
|
|
) {
|
|
try {
|
|
// If reading the ID succeeds, the source is resumable.
|
|
this.entityID = aRequest.entityID;
|
|
} catch (ex) {
|
|
if (
|
|
!(ex instanceof Components.Exception) ||
|
|
ex.result != Cr.NS_ERROR_NOT_RESUMABLE
|
|
) {
|
|
throw ex;
|
|
}
|
|
}
|
|
}
|
|
|
|
// For legacy downloads, we must update the referrerInfo at this time.
|
|
if (aRequest instanceof Ci.nsIHttpChannel) {
|
|
this.download.source.referrerInfo = aRequest.referrerInfo;
|
|
}
|
|
|
|
// Don't open the download panel when the user initiated to save a
|
|
// link or document.
|
|
if (
|
|
aRequest instanceof Ci.nsIChannel &&
|
|
aRequest.loadInfo.isUserTriggeredSave
|
|
) {
|
|
this.download.openDownloadsListOnStart = false;
|
|
}
|
|
|
|
this.addToHistory();
|
|
},
|
|
|
|
/**
|
|
* Called by the nsITransfer implementation when the request has finished.
|
|
*
|
|
* @param {nsresult} status
|
|
* Status code received by the nsITransfer implementation.
|
|
* @param {string} [localizedReason]
|
|
* Optional localized error message associated with a failure
|
|
*/
|
|
onTransferFinished(status, localizedReason) {
|
|
if (Components.isSuccessCode(status)) {
|
|
this.deferExecuted.resolve();
|
|
} else {
|
|
// Infer the origin of the error from the failure code, because more
|
|
// specific data is not available through the nsITransfer implementation.
|
|
let properties = {
|
|
result: status,
|
|
inferCause: true,
|
|
localizedReason,
|
|
};
|
|
this.deferExecuted.reject(new DownloadError(properties));
|
|
}
|
|
},
|
|
|
|
/**
|
|
* When the first execution of the download finished, it can be restarted by
|
|
* using a DownloadCopySaver object instead of the original legacy component
|
|
* that executed the download.
|
|
*/
|
|
firstExecutionFinished: false,
|
|
|
|
/**
|
|
* In case the download is restarted after the first execution finished, this
|
|
* property contains a reference to the DownloadCopySaver that is executing
|
|
* the new download attempt.
|
|
*/
|
|
copySaver: null,
|
|
|
|
/**
|
|
* String corresponding to the entityID property of the nsIResumableChannel
|
|
* used to execute the download, or null if the channel was not resumable or
|
|
* the saver was instructed not to keep partially downloaded data.
|
|
*/
|
|
entityID: null,
|
|
|
|
/**
|
|
* Implements "DownloadSaver.execute".
|
|
*/
|
|
async execute(aSetProgressBytesFn, aSetPropertiesFn) {
|
|
// Check if this is not the first execution of the download. The Download
|
|
// object guarantees that this function is not re-entered during execution.
|
|
if (this.firstExecutionFinished) {
|
|
if (!this.copySaver) {
|
|
this.copySaver = new DownloadCopySaver();
|
|
this.copySaver.download = this.download;
|
|
this.copySaver.entityID = this.entityID;
|
|
this.copySaver.alreadyAddedToHistory = true;
|
|
}
|
|
await this.copySaver.execute.apply(this.copySaver, arguments);
|
|
return;
|
|
}
|
|
|
|
this.setProgressBytesFn = aSetProgressBytesFn;
|
|
if (this.progressWasNotified) {
|
|
this.onProgressBytes(this.currentBytes, this.totalBytes);
|
|
}
|
|
|
|
try {
|
|
// Wait for the component that executes the download to finish.
|
|
await this.deferExecuted.promise;
|
|
|
|
// At this point, the "request" property has been populated. Ensure we
|
|
// report the value of "Content-Length", if available, even if the
|
|
// download didn't generate any progress events.
|
|
if (
|
|
!this.progressWasNotified &&
|
|
this.request instanceof Ci.nsIChannel &&
|
|
this.request.contentLength >= 0
|
|
) {
|
|
aSetProgressBytesFn(0, this.request.contentLength);
|
|
}
|
|
|
|
// If the component executing the download provides the path of a
|
|
// ".part" file, it means that it expects the listener to move the file
|
|
// to its final target path when the download succeeds. In this case,
|
|
// an empty ".part" file is created even if no data was received from
|
|
// the source.
|
|
//
|
|
// When no ".part" file path is provided the download implementation may
|
|
// not have created the target file (if no data was received from the
|
|
// source). In this case, ensure that an empty file is created as
|
|
// expected.
|
|
if (!this.download.target.partFilePath) {
|
|
try {
|
|
// This atomic operation is more efficient than an existence check.
|
|
await IOUtils.writeUTF8(this.download.target.path, "", {
|
|
mode: "create",
|
|
});
|
|
} catch (ex) {
|
|
if (
|
|
!DOMException.isInstance(ex) ||
|
|
ex.name !== "NoModificationAllowedError"
|
|
) {
|
|
throw ex;
|
|
}
|
|
}
|
|
}
|
|
|
|
await this._checkReputationAndMove(aSetPropertiesFn);
|
|
} catch (ex) {
|
|
// In case the operation failed, ensure we stop downloading data. Since
|
|
// we never re-enter this function, deferCanceled is always available.
|
|
this.deferCanceled.resolve();
|
|
throw ex;
|
|
} finally {
|
|
// We don't need the reference to the request anymore. We must also set
|
|
// deferCanceled to null in order to free any indirect references it
|
|
// may hold to the request.
|
|
this.request = null;
|
|
this.deferCanceled = null;
|
|
// Allow the download to restart through a DownloadCopySaver.
|
|
this.firstExecutionFinished = true;
|
|
}
|
|
},
|
|
|
|
_checkReputationAndMove() {
|
|
return DownloadCopySaver.prototype._checkReputationAndMove.apply(
|
|
this,
|
|
arguments
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Implements "DownloadSaver.cancel".
|
|
*/
|
|
cancel: function DLS_cancel() {
|
|
// We may be using a DownloadCopySaver to handle resuming.
|
|
if (this.copySaver) {
|
|
return this.copySaver.cancel.apply(this.copySaver, arguments);
|
|
}
|
|
|
|
// If the download hasn't stopped already, resolve deferCanceled so that the
|
|
// operation is canceled as soon as a cancellation handler is registered.
|
|
// Note that the handler might not have been registered yet.
|
|
if (this.deferCanceled) {
|
|
this.deferCanceled.resolve();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Implements "DownloadSaver.removeData".
|
|
*/
|
|
removeData(canRemoveFinalTarget) {
|
|
// DownloadCopySaver and DownloadLegacySaver use the same logic for removing
|
|
// partially downloaded data, though this implementation isn't shared by
|
|
// other saver types, thus it isn't found on their shared prototype.
|
|
return DownloadCopySaver.prototype.removeData.call(
|
|
this,
|
|
canRemoveFinalTarget
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Implements "DownloadSaver.toSerializable".
|
|
*/
|
|
toSerializable() {
|
|
// This object depends on legacy components that are created externally,
|
|
// thus it cannot be rebuilt during deserialization. To support resuming
|
|
// across different browser sessions, this object is transformed into a
|
|
// DownloadCopySaver for the purpose of serialization.
|
|
return DownloadCopySaver.prototype.toSerializable.call(this);
|
|
},
|
|
|
|
/**
|
|
* Implements "DownloadSaver.getSha256Hash".
|
|
*/
|
|
getSha256Hash() {
|
|
if (this.copySaver) {
|
|
return this.copySaver.getSha256Hash();
|
|
}
|
|
return this._sha256Hash;
|
|
},
|
|
|
|
/**
|
|
* Called by the nsITransfer implementation when the hash is available.
|
|
*/
|
|
setSha256Hash(hash) {
|
|
this._sha256Hash = hash;
|
|
},
|
|
|
|
/**
|
|
* Implements "DownloadSaver.getSignatureInfo".
|
|
*/
|
|
getSignatureInfo() {
|
|
if (this.copySaver) {
|
|
return this.copySaver.getSignatureInfo();
|
|
}
|
|
return this._signatureInfo;
|
|
},
|
|
|
|
/**
|
|
* Called by the nsITransfer implementation when the hash is available.
|
|
*/
|
|
setSignatureInfo(signatureInfo) {
|
|
this._signatureInfo = signatureInfo;
|
|
},
|
|
|
|
/**
|
|
* Implements "DownloadSaver.getRedirects".
|
|
*/
|
|
getRedirects() {
|
|
if (this.copySaver) {
|
|
return this.copySaver.getRedirects();
|
|
}
|
|
return this._redirects;
|
|
},
|
|
|
|
/**
|
|
* Called by the nsITransfer implementation when the redirect chain is
|
|
* available.
|
|
*/
|
|
setRedirects(redirects) {
|
|
this._redirects = redirects;
|
|
},
|
|
};
|
|
Object.setPrototypeOf(DownloadLegacySaver.prototype, DownloadSaver.prototype);
|
|
|
|
/**
|
|
* Returns a new DownloadLegacySaver object. This saver type has a
|
|
* deserializable form only when creating a new object in memory, because it
|
|
* cannot be serialized to disk.
|
|
*/
|
|
DownloadLegacySaver.fromSerializable = function () {
|
|
return new DownloadLegacySaver();
|
|
};
|