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