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