Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 /**
8 * This component implements the XPCOM interfaces required for integration with
9 * the legacy download components.
10 *
11 * New code is expected to use the "Downloads.jsm" module directly, without
12 * going through the interfaces implemented in this XPCOM component. These
13 * interfaces are only maintained for backwards compatibility with components
14 * that still work synchronously on the main thread.
15 */
17 "use strict";
19 ////////////////////////////////////////////////////////////////////////////////
20 //// Globals
22 const Cc = Components.classes;
23 const Ci = Components.interfaces;
24 const Cu = Components.utils;
25 const Cr = Components.results;
27 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
29 XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
30 "resource://gre/modules/Downloads.jsm");
31 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
32 "resource://gre/modules/Promise.jsm");
34 ////////////////////////////////////////////////////////////////////////////////
35 //// DownloadLegacyTransfer
37 /**
38 * nsITransfer implementation that provides a bridge to a Download object.
39 *
40 * Legacy downloads work differently than the JavaScript implementation. In the
41 * latter, the caller only provides the properties for the Download object and
42 * the entire process is handled by the "start" method. In the legacy
43 * implementation, the caller must create a separate object to execute the
44 * download, and then make the download visible to the user by hooking it up to
45 * an nsITransfer instance.
46 *
47 * Since nsITransfer instances may be created before the download system is
48 * initialized, and initialization as well as other operations are asynchronous,
49 * this implementation is able to delay all progress and status notifications it
50 * receives until the associated Download object is finally created.
51 *
52 * Conversely, the DownloadLegacySaver object can also receive execution and
53 * cancellation requests asynchronously, before or after it is connected to
54 * this nsITransfer instance. For that reason, those requests are communicated
55 * in a potentially deferred way, using promise objects.
56 *
57 * The component that executes the download implements nsICancelable to receive
58 * cancellation requests, but after cancellation it cannot be reused again.
59 *
60 * Since the components that execute the download may be different and they
61 * don't always give consistent results, this bridge takes care of enforcing the
62 * expectations, for example by ensuring the target file exists when the
63 * download is successful, even if the source has a size of zero bytes.
64 */
65 function DownloadLegacyTransfer()
66 {
67 this._deferDownload = Promise.defer();
68 }
70 DownloadLegacyTransfer.prototype = {
71 classID: Components.ID("{1b4c85df-cbdd-4bb6-b04e-613caece083c}"),
73 //////////////////////////////////////////////////////////////////////////////
74 //// nsISupports
76 QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
77 Ci.nsIWebProgressListener2,
78 Ci.nsITransfer]),
80 //////////////////////////////////////////////////////////////////////////////
81 //// nsIWebProgressListener
83 onStateChange: function DLT_onStateChange(aWebProgress, aRequest, aStateFlags,
84 aStatus)
85 {
86 if (!Components.isSuccessCode(aStatus)) {
87 this._componentFailed = true;
88 }
90 if ((aStateFlags & Ci.nsIWebProgressListener.STATE_START) &&
91 (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
93 // If the request's response has been blocked by Windows Parental Controls
94 // with an HTTP 450 error code, we must cancel the request synchronously.
95 let blockedByParentalControls = aRequest instanceof Ci.nsIHttpChannel &&
96 aRequest.responseStatus == 450;
97 if (blockedByParentalControls) {
98 aRequest.cancel(Cr.NS_BINDING_ABORTED);
99 }
101 // The main request has just started. Wait for the associated Download
102 // object to be available before notifying.
103 this._deferDownload.promise.then(download => {
104 // If the request was blocked, now that we have the download object we
105 // should set a flag that can be retrieved later when handling the
106 // cancellation so that the proper error can be thrown.
107 if (blockedByParentalControls) {
108 download._blockedByParentalControls = true;
109 }
111 download.saver.onTransferStarted(
112 aRequest,
113 this._cancelable instanceof Ci.nsIHelperAppLauncher);
115 // To handle asynchronous cancellation properly, we should hook up the
116 // handler only after we have been notified that the main request
117 // started. We will wait until the main request stopped before
118 // notifying that the download has been canceled. Since the request has
119 // not completed yet, deferCanceled is guaranteed to be set.
120 return download.saver.deferCanceled.promise.then(() => {
121 // Only cancel if the object executing the download is still running.
122 if (this._cancelable && !this._componentFailed) {
123 this._cancelable.cancel(Cr.NS_ERROR_ABORT);
124 if (this._cancelable instanceof Ci.nsIWebBrowserPersist) {
125 // This component will not send the STATE_STOP notification.
126 download.saver.onTransferFinished(aRequest, Cr.NS_ERROR_ABORT);
127 this._cancelable = null;
128 }
129 }
130 });
131 }).then(null, Cu.reportError);
132 } else if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) &&
133 (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
134 // The last file has been received, or the download failed. Wait for the
135 // associated Download object to be available before notifying.
136 this._deferDownload.promise.then(download => {
137 // At this point, the hash has been set and we need to copy it to the
138 // DownloadSaver.
139 if (Components.isSuccessCode(aStatus)) {
140 download.saver.setSha256Hash(this._sha256Hash);
141 download.saver.setSignatureInfo(this._signatureInfo);
142 }
143 download.saver.onTransferFinished(aRequest, aStatus);
144 }).then(null, Cu.reportError);
146 // Release the reference to the component executing the download.
147 this._cancelable = null;
148 }
149 },
151 onProgressChange: function DLT_onProgressChange(aWebProgress, aRequest,
152 aCurSelfProgress,
153 aMaxSelfProgress,
154 aCurTotalProgress,
155 aMaxTotalProgress)
156 {
157 this.onProgressChange64(aWebProgress, aRequest, aCurSelfProgress,
158 aMaxSelfProgress, aCurTotalProgress,
159 aMaxTotalProgress);
160 },
162 onLocationChange: function () { },
164 onStatusChange: function DLT_onStatusChange(aWebProgress, aRequest, aStatus,
165 aMessage)
166 {
167 // The status change may optionally be received in addition to the state
168 // change, but if no network request actually started, it is possible that
169 // we only receive a status change with an error status code.
170 if (!Components.isSuccessCode(aStatus)) {
171 this._componentFailed = true;
173 // Wait for the associated Download object to be available.
174 this._deferDownload.promise.then(function DLT_OSC_onDownload(aDownload) {
175 aDownload.saver.onTransferFinished(aRequest, aStatus);
176 }).then(null, Cu.reportError);
177 }
178 },
180 onSecurityChange: function () { },
182 //////////////////////////////////////////////////////////////////////////////
183 //// nsIWebProgressListener2
185 onProgressChange64: function DLT_onProgressChange64(aWebProgress, aRequest,
186 aCurSelfProgress,
187 aMaxSelfProgress,
188 aCurTotalProgress,
189 aMaxTotalProgress)
190 {
191 // Wait for the associated Download object to be available.
192 this._deferDownload.promise.then(function DLT_OPC64_onDownload(aDownload) {
193 aDownload.saver.onProgressBytes(aCurTotalProgress, aMaxTotalProgress);
194 }).then(null, Cu.reportError);
195 },
197 onRefreshAttempted: function DLT_onRefreshAttempted(aWebProgress, aRefreshURI,
198 aMillis, aSameURI)
199 {
200 // Indicate that refreshes and redirects are allowed by default. However,
201 // note that download components don't usually call this method at all.
202 return true;
203 },
205 //////////////////////////////////////////////////////////////////////////////
206 //// nsITransfer
208 init: function DLT_init(aSource, aTarget, aDisplayName, aMIMEInfo, aStartTime,
209 aTempFile, aCancelable, aIsPrivate)
210 {
211 this._cancelable = aCancelable;
213 let launchWhenSucceeded = false, contentType = null, launcherPath = null;
215 if (aMIMEInfo instanceof Ci.nsIMIMEInfo) {
216 launchWhenSucceeded =
217 aMIMEInfo.preferredAction != Ci.nsIMIMEInfo.saveToDisk;
218 contentType = aMIMEInfo.type;
220 let appHandler = aMIMEInfo.preferredApplicationHandler;
221 if (aMIMEInfo.preferredAction == Ci.nsIMIMEInfo.useHelperApp &&
222 appHandler instanceof Ci.nsILocalHandlerApp) {
223 launcherPath = appHandler.executable.path;
224 }
225 }
227 // Create a new Download object associated to a DownloadLegacySaver, and
228 // wait for it to be available. This operation may cause the entire
229 // download system to initialize before the object is created.
230 Downloads.createDownload({
231 source: { url: aSource.spec, isPrivate: aIsPrivate },
232 target: { path: aTarget.QueryInterface(Ci.nsIFileURL).file.path,
233 partFilePath: aTempFile && aTempFile.path },
234 saver: "legacy",
235 launchWhenSucceeded: launchWhenSucceeded,
236 contentType: contentType,
237 launcherPath: launcherPath
238 }).then(function DLT_I_onDownload(aDownload) {
239 // Legacy components keep partial data when they use a ".part" file.
240 if (aTempFile) {
241 aDownload.tryToKeepPartialData = true;
242 }
244 // Start the download before allowing it to be controlled. Ignore errors.
245 aDownload.start().then(null, () => {});
247 // Start processing all the other events received through nsITransfer.
248 this._deferDownload.resolve(aDownload);
250 // Add the download to the list, allowing it to be seen and canceled.
251 return Downloads.getList(Downloads.ALL).then(list => list.add(aDownload));
252 }.bind(this)).then(null, Cu.reportError);
253 },
255 setSha256Hash: function (hash)
256 {
257 this._sha256Hash = hash;
258 },
260 setSignatureInfo: function (signatureInfo)
261 {
262 this._signatureInfo = signatureInfo;
263 },
265 //////////////////////////////////////////////////////////////////////////////
266 //// Private methods and properties
268 /**
269 * This deferred object contains a promise that is resolved with the Download
270 * object associated with this nsITransfer instance, when it is available.
271 */
272 _deferDownload: null,
274 /**
275 * Reference to the component that is executing the download. This component
276 * allows cancellation through its nsICancelable interface.
277 */
278 _cancelable: null,
280 /**
281 * Indicates that the component that executes the download has notified a
282 * failure condition. In this case, we should never use the component methods
283 * that cancel the download.
284 */
285 _componentFailed: false,
287 /**
288 * Save the SHA-256 hash in raw bytes of the downloaded file.
289 */
290 _sha256Hash: null,
292 /**
293 * Save the signature info in a serialized protobuf of the downloaded file.
294 */
295 _signatureInfo: null,
296 };
298 ////////////////////////////////////////////////////////////////////////////////
299 //// Module
301 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DownloadLegacyTransfer]);