summaryrefslogtreecommitdiffstats
path: root/toolkit/components/downloads/DownloadLegacy.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--toolkit/components/downloads/DownloadLegacy.sys.mjs503
1 files changed, 503 insertions, 0 deletions
diff --git a/toolkit/components/downloads/DownloadLegacy.sys.mjs b/toolkit/components/downloads/DownloadLegacy.sys.mjs
new file mode 100644
index 0000000000..ae38b0d26c
--- /dev/null
+++ b/toolkit/components/downloads/DownloadLegacy.sys.mjs
@@ -0,0 +1,503 @@
+/* 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, {
+ DownloadError: "resource://gre/modules/DownloadCore.sys.mjs",
+ Downloads: "resource://gre/modules/Downloads.sys.mjs",
+});
+
+/**
+ * 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,
+ aMessage
+ ) {
+ // The status change may optionally be received in addition to the state
+ // change, but if no network request actually started, it is possible that
+ // we only receive a status change with an error status code.
+ if (!Components.isSuccessCode(aStatus)) {
+ this._componentFailed = true;
+
+ // Wait for the associated Download object to be available.
+ this._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(
+ aWebProgress,
+ aRefreshURI,
+ aMillis,
+ aSameURI
+ ) {
+ // Indicate that refreshes and redirects are allowed by default. However,
+ // note that download components don't usually call this method at all.
+ return true;
+ },
+
+ // nsITransfer
+ init: function DLT_init(
+ aSource,
+ 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,
+};