Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* This component implements the XPCOM interfaces required for integration with
* the legacy download components.
*
* New code is expected to use the "Downloads.sys.mjs" module directly, without
* going through the interfaces implemented in this XPCOM component. These
* interfaces are only maintained for backwards compatibility with components
* that still work synchronously on the main thread.
*/
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
});
/**
* nsITransfer implementation that provides a bridge to a Download object.
*
* Legacy downloads work differently than the JavaScript implementation. In the
* latter, the caller only provides the properties for the Download object and
* the entire process is handled by the "start" method. In the legacy
* implementation, the caller must create a separate object to execute the
* download, and then make the download visible to the user by hooking it up to
* an nsITransfer instance.
*
* Since nsITransfer instances may be created before the download system is
* initialized, and initialization as well as other operations are asynchronous,
* this implementation is able to delay all progress and status notifications it
* receives until the associated Download object is finally created.
*
* Conversely, the DownloadLegacySaver object can also receive execution and
* cancellation requests asynchronously, before or after it is connected to
* this nsITransfer instance. For that reason, those requests are communicated
* in a potentially deferred way, using promise objects.
*
* The component that executes the download implements nsICancelable to receive
* cancellation requests, but after cancellation it cannot be reused again.
*
* Since the components that execute the download may be different and they
* don't always give consistent results, this bridge takes care of enforcing the
* expectations, for example by ensuring the target file exists when the
* download is successful, even if the source has a size of zero bytes.
*/
export function DownloadLegacyTransfer() {
this._promiseDownload = new Promise(r => (this._resolveDownload = r));
}
DownloadLegacyTransfer.prototype = {
classID: Components.ID("{1b4c85df-cbdd-4bb6-b04e-613caece083c}"),
QueryInterface: ChromeUtils.generateQI([
"nsIWebProgressListener",
"nsIWebProgressListener2",
"nsITransfer",
]),
// nsIWebProgressListener
onStateChange: function DLT_onStateChange(
aWebProgress,
aRequest,
aStateFlags,
aStatus
) {
if (!Components.isSuccessCode(aStatus)) {
this._componentFailed = true;
}
if (
aStateFlags & Ci.nsIWebProgressListener.STATE_START &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK
) {
let blockedByParentalControls = false;
// If it is a failed download, aRequest.responseStatus doesn't exist.
// (missing file on the server, network failure to download)
try {
// If the request's response has been blocked by Windows Parental Controls
// with an HTTP 450 error code, we must cancel the request synchronously.
blockedByParentalControls =
aRequest instanceof Ci.nsIHttpChannel &&
aRequest.responseStatus == 450;
} catch (e) {
if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
aRequest.cancel(Cr.NS_BINDING_ABORTED);
}
}
if (blockedByParentalControls) {
aRequest.cancel(Cr.NS_BINDING_ABORTED);
}
// The main request has just started. Wait for the associated Download
// object to be available before notifying.
this._promiseDownload
.then(download => {
// If the request was blocked, now that we have the download object we
// should set a flag that can be retrieved later when handling the
// cancellation so that the proper error can be thrown.
if (blockedByParentalControls) {
download._blockedByParentalControls = true;
}
download.saver.onTransferStarted(aRequest);
// To handle asynchronous cancellation properly, we should hook up the
// handler only after we have been notified that the main request
// started. We will wait until the main request stopped before
// notifying that the download has been canceled. Since the request has
// not completed yet, deferCanceled is guaranteed to be set.
return download.saver.deferCanceled.promise.then(() => {
// Only cancel if the object executing the download is still running.
if (this._cancelable && !this._componentFailed) {
this._cancelable.cancel(Cr.NS_ERROR_ABORT);
}
});
})
.catch(console.error);
} else if (
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK
) {
// The last file has been received, or the download failed. Wait for the
// associated Download object to be available before notifying.
this._promiseDownload
.then(download => {
// At this point, the hash has been set and we need to copy it to the
// DownloadSaver.
if (Components.isSuccessCode(aStatus)) {
download.saver.setSha256Hash(this._sha256Hash);
download.saver.setSignatureInfo(this._signatureInfo);
download.saver.setRedirects(this._redirects);
}
download.saver.onTransferFinished(aStatus);
})
.catch(console.error);
// Release the reference to the component executing the download.
this._cancelable = null;
}
},
// nsIWebProgressListener
onProgressChange: function DLT_onProgressChange(
aWebProgress,
aRequest,
aCurSelfProgress,
aMaxSelfProgress,
aCurTotalProgress,
aMaxTotalProgress
) {
this.onProgressChange64(
aWebProgress,
aRequest,
aCurSelfProgress,
aMaxSelfProgress,
aCurTotalProgress,
aMaxTotalProgress
);
},
onLocationChange() {},
// nsIWebProgressListener
onStatusChange: function DLT_onStatusChange(aWebProgress, aRequest, aStatus) {
// The status change may optionally be received in addition to the state
// change, but if no network request actually started, it is possible that
// we only receive a status change with an error status code.
if (!Components.isSuccessCode(aStatus)) {
this._componentFailed = true;
// Wait for the associated Download object to be available.
this._promiseDownload
.then(download => {
download.saver.onTransferFinished(aStatus);
})
.catch(console.error);
}
},
onSecurityChange() {},
onContentBlockingEvent() {},
// nsIWebProgressListener2
onProgressChange64: function DLT_onProgressChange64(
aWebProgress,
aRequest,
aCurSelfProgress,
aMaxSelfProgress,
aCurTotalProgress,
aMaxTotalProgress
) {
// Since this progress function is invoked frequently, we use a slightly
// more complex solution that optimizes the case where we already have an
// associated Download object, avoiding the Promise overhead.
if (this._download) {
this._hasDelayedProgress = false;
this._download.saver.onProgressBytes(
aCurTotalProgress,
aMaxTotalProgress
);
return;
}
// If we don't have a Download object yet, store the most recent progress
// notification to send later. We must do this because there is no guarantee
// that a future notification will be sent if the download stalls.
this._delayedCurTotalProgress = aCurTotalProgress;
this._delayedMaxTotalProgress = aMaxTotalProgress;
// Do not enqueue multiple callbacks for the progress report.
if (this._hasDelayedProgress) {
return;
}
this._hasDelayedProgress = true;
this._promiseDownload
.then(download => {
// Check whether an immediate progress report has been already processed
// before we could send the delayed progress report.
if (!this._hasDelayedProgress) {
return;
}
download.saver.onProgressBytes(
this._delayedCurTotalProgress,
this._delayedMaxTotalProgress
);
})
.catch(console.error);
},
_hasDelayedProgress: false,
_delayedCurTotalProgress: 0,
_delayedMaxTotalProgress: 0,
// nsIWebProgressListener2
onRefreshAttempted: function DLT_onRefreshAttempted() {
// Indicate that refreshes and redirects are allowed by default. However,
// note that download components don't usually call this method at all.
return true;
},
// nsITransfer
init: function DLT_init(
aSource,
aSourceOriginalURI,
aTarget,
aDisplayName,
aMIMEInfo,
aStartTime,
aTempFile,
aCancelable,
aIsPrivate,
aDownloadClassification,
aReferrerInfo,
aOpenDownloadsListOnStart
) {
return this._nsITransferInitInternal(
aSource,
aSourceOriginalURI,
aTarget,
aDisplayName,
aMIMEInfo,
aStartTime,
aTempFile,
aCancelable,
aIsPrivate,
aDownloadClassification,
aReferrerInfo,
aOpenDownloadsListOnStart
);
},
// nsITransfer
initWithBrowsingContext(
aSource,
aTarget,
aDisplayName,
aMIMEInfo,
aStartTime,
aTempFile,
aCancelable,
aIsPrivate,
aDownloadClassification,
aReferrerInfo,
aOpenDownloadsListOnStart,
aBrowsingContext,
aHandleInternally,
aHttpChannel
) {
let browsingContextId;
let userContextId;
if (aBrowsingContext && aBrowsingContext.currentWindowGlobal) {
browsingContextId = aBrowsingContext.id;
let windowGlobal = aBrowsingContext.currentWindowGlobal;
let originAttributes = windowGlobal.documentPrincipal.originAttributes;
userContextId = originAttributes.userContextId;
}
return this._nsITransferInitInternal(
aSource,
null,
aTarget,
aDisplayName,
aMIMEInfo,
aStartTime,
aTempFile,
aCancelable,
aIsPrivate,
aDownloadClassification,
aReferrerInfo,
aOpenDownloadsListOnStart,
userContextId,
browsingContextId,
aHandleInternally,
aHttpChannel
);
},
_nsITransferInitInternal(
aSource,
aSourceOriginalURI,
aTarget,
aDisplayName,
aMIMEInfo,
aStartTime,
aTempFile,
aCancelable,
isPrivate,
aDownloadClassification,
referrerInfo,
openDownloadsListOnStart = true,
userContextId = 0,
browsingContextId = 0,
handleInternally = false,
aHttpChannel = null
) {
this._cancelable = aCancelable;
let launchWhenSucceeded = false,
contentType = null,
launcherPath = null;
if (aMIMEInfo instanceof Ci.nsIMIMEInfo) {
launchWhenSucceeded =
aMIMEInfo.preferredAction != Ci.nsIMIMEInfo.saveToDisk;
contentType = aMIMEInfo.type;
let appHandler = aMIMEInfo.preferredApplicationHandler;
if (
aMIMEInfo.preferredAction == Ci.nsIMIMEInfo.useHelperApp &&
appHandler instanceof Ci.nsILocalHandlerApp
) {
launcherPath = appHandler.executable.path;
}
}
// Create a new Download object associated to a DownloadLegacySaver, and
// wait for it to be available. This operation may cause the entire
// download system to initialize before the object is created.
let authHeader = null;
if (aHttpChannel) {
try {
authHeader = aHttpChannel.getRequestHeader("Authorization");
} catch (e) {}
}
let serialisedDownload = {
source: {
url: aSource.spec,
originalUrl: aSourceOriginalURI && aSourceOriginalURI.spec,
isPrivate,
userContextId,
browsingContextId,
referrerInfo,
authHeader,
},
target: {
path: aTarget.QueryInterface(Ci.nsIFileURL).file.path,
partFilePath: aTempFile && aTempFile.path,
},
saver: "legacy",
launchWhenSucceeded,
contentType,
launcherPath,
handleInternally,
openDownloadsListOnStart,
};
// In case the Download was classified as insecure/dangerous
// it is already canceled, so we need to generate and attach the
// corresponding error to the download.
if (aDownloadClassification == Ci.nsITransfer.DOWNLOAD_POTENTIALLY_UNSAFE) {
Services.telemetry
.getKeyedHistogramById("DOWNLOADS_USER_ACTION_ON_BLOCKED_DOWNLOAD")
.add(lazy.DownloadError.BLOCK_VERDICT_INSECURE, 0);
serialisedDownload.errorObj = {
becauseBlockedByReputationCheck: true,
reputationCheckVerdict: lazy.DownloadError.BLOCK_VERDICT_INSECURE,
};
// hasBlockedData needs to be true
// because the unblock UI is hidden if there is
// no data to be unblocked.
serialisedDownload.hasBlockedData = true;
// We cannot use the legacy saver here, as the original channel
// is already closed. A copy saver would create a new channel once
// start() is called.
serialisedDownload.saver = "copy";
// Since the download is canceled already, we do not need to keep refrences
this._download = null;
this._cancelable = null;
}
lazy.Downloads.createDownload(serialisedDownload)
.then(async aDownload => {
// Legacy components keep partial data when they use a ".part" file.
if (aTempFile) {
aDownload.tryToKeepPartialData = true;
}
// Start the download before allowing it to be controlled. Ignore errors.
aDownload.start().catch(() => {});
// Start processing all the other events received through nsITransfer.
this._download = aDownload;
this._resolveDownload(aDownload);
// Add the download to the list, allowing it to be seen and canceled.
await (await lazy.Downloads.getList(lazy.Downloads.ALL)).add(aDownload);
if (serialisedDownload.errorObj) {
// In case we added an already canceled dummy download
// we need to manually trigger a change event
// as all the animations for finishing downloads are
// listening on onChange.
aDownload._notifyChange();
}
})
.catch(console.error);
},
setSha256Hash(hash) {
this._sha256Hash = hash;
},
setSignatureInfo(signatureInfo) {
this._signatureInfo = signatureInfo;
},
setRedirects(redirects) {
this._redirects = redirects;
},
/**
* Download object associated with this nsITransfer instance. This is not
* available immediately when the nsITransfer instance is created.
*/
_download: null,
/**
* Promise that resolves to the Download object associated with this
* nsITransfer instance after the _resolveDownload method is invoked.
*
* Waiting on this promise using "then" ensures that the callbacks are invoked
* in the correct order even if enqueued before the object is available.
*/
_promiseDownload: null,
_resolveDownload: null,
/**
* Reference to the component that is executing the download. This component
* allows cancellation through its nsICancelable interface.
*/
_cancelable: null,
/**
* Indicates that the component that executes the download has notified a
* failure condition. In this case, we should never use the component methods
* that cancel the download.
*/
_componentFailed: false,
/**
* Save the SHA-256 hash in raw bytes of the downloaded file.
*/
_sha256Hash: null,
/**
* Save the signature info in a serialized protobuf of the downloaded file.
*/
_signatureInfo: null,
};